mirror of
https://github.com/wgtunnel/Nucleus.git
synced 2026-10-09 14:40:54 -07:00
main
14
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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>
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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> |
||
|
|
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> |
||
|
|
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. |
||
|
|
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`.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |