14 Commits
Author SHA1 Message Date
takkeandClaude Fable 5.1 76a10c06f0 fix(macos): make GraalVM native bundles signable and notarizable with Developer ID
Developer ID signing of a GraalVM native-image .app failed in
packageGraalvm<Format> ("code object is not signed at all / In
subcomponent: Contents/MacOS/.nucleus-executable-type"), and even after
that Gatekeeper rejected the notarized bundle. Three independent causes:

1. The executable-type marker was written into Contents/MacOS/, which
   codesign treats as nested code. Write it to Contents/Resources/ on
   macOS and let ExecutableRuntime look in both places (old location
   first for compatibility).
2. resignApp() only re-signed Contents/runtime and Contents/Frameworks,
   so the GraalVM dylibs in Contents/MacOS/ and Contents/MacOS/lib/ kept
   their ad-hoc signature and notarization rejected them. Sign them
   inside-out like the JVM runtime directory.
3. fontconfig.bfc shipped in Contents/MacOS/lib/. Gatekeeper treats
   every file below Contents/MacOS/ as code, so the notarized, stapled
   bundle still got "spctl: rejected". Ship it in Contents/Resources/
   and point the JDK at it via sun.awt.fontconfig from
   GraalVmInitializer.

Verified with a real app: notarytool Accepted, stapled, all 16 Mach-O
files Developer ID signed with hardened runtime, and
`spctl -a -t exec` now answers "accepted / Notarized Developer ID".
The JVM (jpackage) pipeline is unaffected.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 10:26:12 +09:00
takke 82c6131ff3 feat(nsis): add menuCategory and shortcutName to the NSIS targets
Follow-up to #629. The NSIS targets already expose most installer
settings, but not menuCategory / shortcutName, so a start menu group
could not be declared at all: the shortcut always landed in the start
menu root. This blocked projects that are migrating their MSI packaging
to the recommended NSIS format while keeping the start menu folder
they had under jpackage.

Add the two remaining CommonWindowsInstallerConfiguration options to
NsisSettings and emit them for nsis and nsis-web. Both default to null
(nothing emitted), so generated installers are unchanged unless a
project opts in - with the same intended exception as the MSI target:
windows.menuGroup, the jpackage-era name for the same concept, acts as
the default for nsis.menuCategory.

The "nsis target is unaffected by the msi settings" test asserted that
menuCategory never appears for NSIS; it now asserts the actual concern,
that msi {} values do not leak into the nsis block.
2026-09-01 00:55:49 +09:00
takke 7e7363cbe6 fix(msi): forward the Windows installer settings to electron-builder
The MSI target only passed upgradeCode and perMachine to
electron-builder, so every other Windows installer setting fell back to
an electron-builder default: windows.menuGroup was ignored and the
shortcut landed in the start menu root, oneClick defaulted to true so
the installer ran without showing any UI, and runAfterFinish defaulted
to true so the app was launched as soon as the install finished.
Projects migrating from jpackage-based packaging lost all three
silently, with no way to set them from a build script.

Add the remaining CommonWindowsInstallerConfiguration options to
MsiSettings and emit them. Each defaults to the electron-builder
default so generated MSIs are unchanged unless a project opts in,
except that windows.menuGroup - the jpackage-era name for the same
concept - now acts as the default for msi.menuCategory, restoring the
start menu folder for projects that already declare one.

windows.menu / windows.shortcut are deliberately not mapped: their
jpackage defaults (false) are the opposite of electron-builder's
(true), so mapping them would silently drop shortcuts from MSIs that
are fine today. msi.createStartMenuShortcut / createDesktopShortcut
give explicit control instead.

generateWindowsConfig becomes internal so the rendered YAML can be
asserted in unit tests, matching generateLinuxConfig which is already
internal for the same reason.
2026-08-31 22:48:12 +09:00
takkeandClaude Fable 5 16b4b97488 review: guard touch dispatch and fix handler-thread KDoc
Touch input ran user pointer-input code without the window's exception
handler getting a look, so a throwing `clickable` under a touchscreen
tap behaved differently from the same failure under a mouse click:

- Windows: `TaoComposeSceneHostWindows.registerTouchInput` now wraps the
  `window.onTouchInput` listener in `catchExceptions`. A rethrow still
  unwinds into `EventDispatcher.guarded`, i.e. the fatal path, like
  every other input entry on this wire.
- Linux: the `NativeTaoLinuxTouchBridge` callbacks (`onTouchEvent` and
  `onTrackpadGesture` — same bridge, same gap) run outside
  `EventDispatcher.guarded` entirely, so a plain `catchExceptions` wrap
  would leave a rethrow unwinding into the JNI callback frame. The new
  `guardBridgeCallback` reproduces both of the mouse wire's layers: the
  window's handler first, then `TaoApplication.reportFatal` for
  anything it rethrows.

Also rewords the `WindowExceptionHandlerFactory.exceptionHandler` KDoc:
handlers are invoked on the thread the failure occurred on, not on the
Tao main thread.

Refs #625 review

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 13:15:30 +09:00
takkeandClaude Fable 5 d9b68b134d feat(tao): add WindowExceptionHandlerFactory for Tao windows
The Tao backend had no equivalent of Compose Desktop's
LocalWindowExceptionHandlerFactory / WindowExceptionHandler, so an app had
no way to intercept a failure in its own window: an exception thrown
during a frame (composition, layout, draw) or while dispatching input
either froze the window silently or, since #622, took the whole app down.
The AWT backends let apps swallow the recoverable CMP bugs (the "A partial
image was generated" family) and keep running; Tao could not.

Mirror the AWT design, with TaoWindow where java.awt.Window sits, and
chain it in front of the #622 fatal path:

- New public API in dev.nucleusframework.window.tao:
  WindowExceptionHandlerFactory, DefaultWindowExceptionHandlerFactory and
  LocalWindowExceptionHandlerFactory. Handlers reuse the existing
  androidx.compose.ui.window.WindowExceptionHandler, so the contract is
  the AWT one: returning normally swallows and continues, `throw it`
  propagates. Opt-in via @ExperimentalComposeUiApi, as upstream.
- DecoratedWindow reads the local in the parent composition and hands the
  handler to the scene host as a plain field — a CompositionLocal read
  cannot cover exceptions that break the composition itself.
- Frames are guarded in TaoSceneBundle.render, the single seam all three
  hosts and every popup layer render through; a swallowed failure drops
  the frame and re-arms the frame scheduler so the window stays live.
- Input dispatch, IME callbacks (imePreedit / imeCommit / imeReplaceCommit),
  ComposeScene.setContent and the debounced accessibility walk are wrapped
  at their entry points on macOS, Windows and Linux. No Rust changes.
  Rethrowing from any of them unwinds into the `guarded { }` Tao event
  dispatch (or the render loop's fatal coroutine handler), i.e. into #622.
- Popup layers get the owner window's handler through TaoPopupHost /
  TaoPopupHostWindows / TaoPopupHostLinux, the same forwarding
  WindowComposeSceneLayer does, so nativePopupLayers = true windows are
  covered too.

The scene's CoroutineExceptionHandler is now a single chain
(TaoSceneExceptionRouter) that replaces #622's sceneFatalHandler:
teardown (closed) logs at SEVERE as before; otherwise the window's handler
is asked first, and a rethrow — including the default factory's — falls
through to TaoApplication.reportFatal, so the native dialog and clean exit
of #622 still happen. The default factory only rethrows now: reportFatal
already logs the throwable at SEVERE, and logging in both would double
every entry.

A composition failure is not survivable, and the chain says so instead of
leaving a silently frozen window: Compose intercepts it in
Recomposer.processCompositionError, logs it, and rethrows into the
runCatching inside withFrameNanos, which ends the recomposition loop (the
recomposer derives to Inactive the moment the error state is recorded).
Nothing outside androidx.compose.runtime can restart it.
TaoSceneBundle.isRecomposerAlive reports that state; a handler that
swallows such a failure gets a SEVERE explanation rather than the fatal
dialog it explicitly opted out of, and render stops re-arming frames for a
scene that can never change again.

TaoSceneExceptionHandlerTest drives composition / layout / draw / input
failures through the scene harness; TaoSceneExceptionRouterTest covers the
chain itself with an injected fatal step, so the routing is verified
without touching TaoApplication's process-wide fatal state. Both are
registered in TaoSceneTestBattery.

Refs #621, builds on #622/#623

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 07:31:51 +09:00
takke 7bc9ea6020 fix(tao): keep the Linux IME candidate window off the caret
The candidate window tracked the caret and covered it: Mozc's "Tab to select"
hint sat exactly on the composition it was describing, so the underlined
preedit was never visible. Three separate mistakes stacked up to land it there.

The first is that the caret was reduced to a point. IMM32 takes a point and
hangs the candidate list off it, which is all `set_ime_position` can carry, so
the Windows implementation passes the caret's bottom edge and is done. GTK is
area-based instead: `gtk_im_context_set_cursor_location` is given the region
the cursor covers and the input method keeps its own windows off that area. A
1x1 rect tells it there is nothing to avoid. `WindowExtUnix` grows
`set_ime_cursor_area`, mirroring the winit API of the same name, and
`set_ime_position` becomes a zero-sized area at the point — the documented
behaviour of the cross-platform call is unchanged.

The second is the coordinate space: GDK works in logical pixels and the caret
was handed over in physical ones. Invisible at scale 1, wrong on HiDPI.

The third is what actually put the window on top of the text. The caret arrives
in client-area coordinates — what `set_ime_position` documents — but GTK wants
it relative to the toplevel GdkWindow, which on a client-side-decorated window
also spans the invisible resize border and drop shadow. Measured here that is
(26, 23), so the input method placed the caret a shadow's height too high and
drew its list over the line being typed. Translating by the content widget's
allocation is the same correction the Wayland subsurface already applies to
position the Compose surface, so the two now agree on where the caret is.

Verified on GNOME 46 (Wayland session) with ibus + Mozc: the hint and the
candidate list both sit below the composition and follow the caret across
lines.
2026-08-29 00:22:14 +09:00
takke 105b6c9e3d test(tao): cover the Linux IME state machine
The decisions the Linux input-method path makes were spread across GTK
closures: whether a key event reaches Compose, whether committed text replaces
a preedit or stands on its own, whether the modifier state needs republishing.
All three are about *sequences* — a press withheld but its release delivered, a
modifier whose release never arrives, a commit that lands with no composition
open — and none of them could be reproduced from a unit test while the state
lived in the closures.

Move that state into `platform_impl::linux::ime::ImeState` and leave the
signal handlers doing nothing but translating GTK callbacks into calls on it.
The Windows backend draws the same line with its `ImeSource` trait; Linux needs
no trait, because there is nothing to read back — GTK pushes everything, so
plain data suffices.

The tests replay sequences taken from real traces on GNOME 46 with ibus + Mozc:

  - `release_without_a_press_is_dropped` — Wayland withholds the press of a key
    the input method consumed and delivers the release anyway
  - `filtered_press_withholds_its_release` — the X11 shape of the same thing,
    where `filter_keypress` claims the press in-process
  - `confirming_return_does_not_reach_compose` — the observed failure: the
    Return that confirmed a conversion also activated the focused control
  - `lost_modifier_release_recovers_on_the_next_key` and
    `asymmetric_modifier_reports_settle` — a swallowed Control release, and the
    `Meta_L` press answered by an `Alt_L` release that the same traces show
  - `shortcuts_pass_through` — Ctrl+C and friends must survive the key gate

No behaviour change; this is the same logic addressed through one owner.
Run with `cargo test --lib` from `src/main/native/vendor/tao`.
2026-08-29 00:21:30 +09:00
takke 241ff17674 fix(tao): derive the Linux modifier state from every key event
`ModifiersChanged` was only ever emitted when the key *itself* was a modifier,
so Compose's view of Ctrl / Shift / Alt / Super was built up from press and
release bookkeeping. That holds only as long as every release arrives, and it
does not: put an input method in the event path on X11 and modifier releases go
missing, because ibus re-injects what it forwards rather than passing the
original events through. A dropped Control release then leaves Compose
believing Ctrl is held for the rest of the session — a plain Return reads as
Ctrl+Return, and the app fires whatever it binds to that instead of inserting a
newline. Pressing and releasing Ctrl by hand was the only way out.

GDK reports the live modifier mask on every event, so derive the state from the
event each time and publish it whenever it changed. That self-heals on the very
next keystroke no matter which release was lost, and it costs nothing when
nothing changed. It also handles the asymmetric reports seen in the same
traces, where a `Meta_L` press is answered by an `Alt_L` release.

Found while verifying #558 on the XWayland path, but it is not IME-specific:
any lost modifier release had the same effect. It only stayed hidden because
the Linux backend never connected to a system input method until now.
2026-08-28 14:28:25 +09:00
takke 3d58c52793 fix(tao): forward Linux IME preedit into Compose
The Linux backend pinned `gtk::IMContextSimple` — GTK's built-in fallback,
which only knows Compose sequences and Ctrl+Shift+U. It never reaches the
system input method, so CJK input was not merely degraded on Linux, it was
impossible: typing `aiueo` and confirming left a single stray `a` behind.
`connect_preedit_changed` was never wired either, and `filter_keypress` ran
*after* the key had already been handed to the app, with its result discarded.

Switch to `gtk::IMMulticontext`, which resolves the platform module the same
way GTK's own text widgets do (ibus / fcitx5 through the GTK immodule on X11,
the text-input-v3 client on Wayland), and route its signals into the
`ImePreedit` / `ImeCommit` events added for macOS in #595 and extended to
Windows in #558:

  - `preedit-changed` -> ImePreedit, `preedit-end` -> ImePreedit("")
  - `commit` -> ImeCommit while composing, ReceivedImeText otherwise
    (the same split as the Windows backend's GCS_RESULTSTR vs. plain WM_CHAR)
  - the input context now follows the window's focus instead of latching
    `focus_in` once at construction

Keys an input method consumes must not also reach Compose, or the Return that
confirms a conversion goes on to insert a newline and the BackSpace that edits
the composition deletes committed text. `filter_keypress` now runs first and
stops propagation — but on its own that is not enough, because only the *press*
is withheld: on Wayland the compositor filters those keys out of the stream
before the client sees them (text-input-v3 has no notion of "filtered"), while
the matching release arrives regardless. Compose fires `clickable`'s onClick on
KeyUp, so a release with no press behind it activates whatever holds focus.
Track the keycodes whose press was actually forwarded and drop releases that
have no match; this covers the X11 path too, where `filter_keypress` withholds
the press in-process for the same effect.

Finally, implement `set_ime_position`, which was a `//TODO` stub, so the
candidate window follows the caret (#558). The input context lives on the
event-loop side, so the caret goes through the window request channel like
every other window mutation, and `platform/linux/ime.rs` supplies the
`nativeSetImeRect` JNI entry point the JVM side already calls on macOS and
Windows — same contract, the caret's bottom edge in window-local physical
pixels.

Verified on GNOME 46 (Wayland session) with ibus + Mozc, on both the Wayland
path and the XWayland one (`NUCLEUS_TAO_LINUX_RENDERER=x11`): inline preedit,
no newline from the confirming Return, BackSpace confined to the composition,
committed text inserted exactly once, the candidate window tracking the caret,
and no regression in ASCII input or in Ctrl+C / Ctrl+V / Ctrl+Return.
2026-08-28 14:27:47 +09:00
takkeandClaude Opus 5 b15a1e0a64 test(tao): cover the Windows IME state machine
Puts a seam in front of the three IMM32 reads `ImeHandler` makes — the
composition string, the committed string, and whether a commit is already
queued — so the state machine can be driven through real message sequences
with no IME installed and no window focus.

The interesting behaviour all lives in the ordering, and none of it was
reachable from a test while the reads went straight to IMM32:

- `commits_then_ends` — the Japanese/Chinese order, including the message
  that carries `GCS_RESULTSTR` and `GCS_COMPSTR` at once (ATOK, Google
  Japanese Input), where the commit has to be taken first.
- `defers_to_a_queued_commit` — the Korean order, where
  `WM_IME_ENDCOMPOSITION` precedes the commit and must not tear the
  composition down. Removing the queue check makes exactly this one fail.
- `ends_without_commit_clears_the_preedit` — an abandoned composition takes
  its preedit with it.
- `swallows_the_commit_replay` / `a_missing_replay_does_not_eat_the_next_key`
  — the replayed commit is dropped once, and a replay that never arrives
  does not eat the keystrokes that follow.
- `empty_lparam_clears_the_preedit_without_ending_the_composition` — an
  empty `lparam` clears the preedit but leaves the composition live.
- `joins_surrogate_pairs` — a non-BMP character typed outside a composition
  arrives as two `WM_CHAR` messages.

`WM_IME_SETCONTEXT` stays outside the seam: its handling *is* the Win32
call, so there is nothing underneath to test.

Run with `cargo test --lib` from `src/main/native/vendor/tao`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 02:27:43 +09:00
takkeandClaude Opus 5 096172a236 fix(tao): harden the Windows IME state machine
Review follow-ups on the Windows IME support.

**Korean commit order.** The Korean IME confirms with Space by sending
`WM_IME_ENDCOMPOSITION` *before* the `WM_IME_COMPOSITION` carrying
`GCS_RESULTSTR`, the reverse of the Chinese and Japanese order. The previous
code claimed in a comment to leave `composing` set for that path but cleared
it unconditionally, so the end of composition raced the text it was about to
receive and took the preedit down first. `commit_is_queued` now peeks the
queue (`PM_NOREMOVE`, filtered to `WM_IME_COMPOSITION`) and defers the whole
end-of-composition to the queued message, matching Chromium (crrev 504d51fc,
after Firefox). With that in place `WM_IME_ENDCOMPOSITION` no longer has to
read `GCS_RESULTSTR` itself, so its handling collapses to "if nothing
committed, drop the preedit".

**Swallowing the commit replay.** After a commit the IME replays the
committed text as `WM_IME_CHAR` / `WM_CHAR`, which has to be swallowed since
the text already arrived through `ImeCommit`. Counting the code units was
wrong: nothing obliges an IME to replay at all — the composition message is
answered with 0 — and a counter left standing would silently eat the next
keystrokes. The expected units are now matched against what actually
arrives, and a mismatch drops the expectation and handles the key normally.
It is also cleared when a new composition starts.

**`lparam == 0`.** Treating it as a definite cancel was too strong: it says
the composition string is empty and nothing else, and it is not a reliable
cancel signal. It still clears the preedit, but no longer forces `composing`
false — whether the composition is live is decided by
START/ENDCOMPOSITION and by `GCS_COMPSTR`.

Verified on Windows 11: Japanese unchanged on both ATOK and MS-IME
(including committing a character and immediately typing that same
character, the boundary case for matching the replay), and Korean MS-IME
composes, commits on Space, backspaces through the jamo, and shows the hanja
candidate list at the caret.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 01:54:15 +09:00
takkeandClaude Opus 5 327ce6a6fc fix(tao): forward Windows IME preedit into Compose
The Windows backend had no IME support beyond the committed string. IME
handling was `minimal_ime.rs`, which watched `WM_IME_ENDCOMPOSITION` and then
concatenated the `WM_CHAR` messages that followed. The composition itself was
never forwarded, so the embedder could not draw it and the IME's own floating
window had to. With no caret position ever pushed to the input context, that
window sat near the top-left corner of the window (#558).

Worse, the raw keystrokes the IME had already swallowed still reached the app.
Windows reports such a key with `VK_PROCESSKEY`, but the `lparam` keeps the
physical scancode, and `keyboard.rs` builds its `KeyEvent` from the scancode —
so the key the IME consumed was resurrected. On a Japanese IME that means the
Enter that ends a conversion also inserts a newline, the arrows that pick a
candidate also move the caret, and the Backspace that edits the preedit also
deletes committed text. Same class of bug as macOS #595, and the same fix
shape as #596: emit the composition, drop the raw key.

- `ime.rs` replaces `minimal_ime.rs`: `ImeContext` reads the composition with
  `ImmGetCompositionStringW` (RAII around `ImmGetContext`/`ImmReleaseContext`),
  and `ImeHandler` turns the messages into `ImePreedit` / `ImeCommit` /
  `ReceivedImeText`. `GCS_RESULTSTR` is taken before `GCS_COMPSTR` because
  Google Japanese Input and ATOK set both flags on one message.
- `WM_IME_COMPOSITION` returns 0 instead of deferring to the default handler,
  and `WM_IME_SETCONTEXT` clears `ISC_SHOWUICOMPOSITIONWINDOW`, so the IME
  stops painting a composition window the embedder now draws inline.
- After a commit the IME replays the committed text as `WM_IME_CHAR` /
  `WM_CHAR`. It already reached the app through `ImeCommit`, so those are
  swallowed — otherwise every committed character lands twice.
- `keyboard.rs` drops `VK_PROCESSKEY` key events (both down and up).
- `Window::set_ime_position` now also sets `CANDIDATEFORM`, not just
  `COMPOSITIONFORM`. The composition form alone never moved the candidate
  list, which is what #558 is about.
- `WindowsTaoPlatformContext` implements `startInputMethod`, mirroring the
  caret rect into `nativeSetImeRect` for as long as a field owns the input.
  `TaoImeSession` is reused unchanged; `replaceCommit` stays macOS-only
  (PressAndHold has no Windows counterpart).
- Ordinary typing outside a composition now joins surrogate pairs. The old
  code only concatenated `WM_CHAR` on the IME path, so a typed non-BMP
  character was dropped.

Verified on Windows 11 with ATOK: the preedit renders inline, conversion
Enter no longer breaks the line, Backspace during conversion no longer eats
committed text, the candidate list follows the caret, committed text is
inserted exactly once, and plain ASCII typing plus shortcuts are unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 00:47:40 +09:00
takkeandClaude Opus 5 11888a6d74 fix(build): pin javac/javadoc encoding to UTF-8
`javac` and `javadoc` read sources in the *platform* default encoding, not
UTF-8. On Windows with a non-Western system locale that is a legacy code
page — windows-31j on Japanese, GBK on Simplified Chinese, and so on — so a
non-ASCII character in a Java source file cannot be decoded.

`decorated-window-tao` has one: the em dash in the KDoc of
`TaoTransferableAccess`. `javac` reports it as a warning, but `javadoc`
treats it as an error, which fails `:decorated-window-tao:javadoc` and with
it `publishToMavenLocal`:

    TaoTransferableAccess.java:8: error: unmappable character (0x94)
                                  for encoding windows-31j

CI never sees this because Linux and macOS default to UTF-8.

Setting `org.gradle.jvmargs=-Dfile.encoding=UTF-8` does not help: `javadoc`
runs in its own process, not in the Gradle daemon. The encoding has to be
stated on the tasks themselves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 23:55:38 +09:00
takkeandClaude Fable 5 4caf086fb5 fix(tao): gate the PressAndHold heuristic while an IME composition is live
A marked-text IME (Japanese, Chinese, ...) commits through insertText:
while the view still holds the preedit. That is never PressAndHold: its
picker only engages on a plain committed letter, outside any composition
(JDK-8074882 flow).

Without the gate, a segment commit that lands while the next romaji key
is down is registered as a PressAndHold "base letter"; the IME's own
selectedRange queries then arm the picker flag, and the *next* commit is
hijacked into the replace-commit path — deleting a code point, skipping
ImeCommit, and leaving the preedit stranded. On real ATOK prediction /
Kotoeri live conversion this shows up as duplicated segments
(「二重二重に…」「しますします」) and U+F7xx tofu, verified with
native-side instrumentation.

Also:
- `TaoImeSession.replaceCommit` filters Apple corporate characters, so a
  hijacked function-key payload can no longer delete a real character
  and insert tofu.
- Register the new `TaoSceneImeTest` case in `TaoSceneTestBattery`
  (`TaoSceneTestBatteryDriftTest` was failing on this branch).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 21:38:51 +09:00