ubuntu-latest (24.04, glibc 2.39) is a moving target. Pin every workflow to
ubuntu-22.04 / ubuntu-22.04-arm, like the natives job (#699), so anything built
or packaged on Linux keeps loading on Debian 12, Devuan 5 and Ubuntu 22.04.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Include apps that take a single module (notifications, tray, packaging)
alongside full-stack desktop apps, so the list shows the runtime is usable
piecewise.
Closes#670.
The X11 and Wayland attach paths asked for one thing only — a GL 3.3
compatibility-profile context — and returned 0 when the driver refused, which
aborts the window on the JVM side. Mesa r600 on an AMD RV610 (Debian 13) caps
the compat profile at 3.0, so the app crashed on launch.
- `create_gl_context()` (shared by both paths) asks for 3.3 compat, then 3.3
core. Core is strictly more available (a 3.2+ driver must offer it, compat
is optional), nothing here or in Skia needs the legacy entry points, and
r600 reaches 3.3 in core — so this GPU stays hardware-accelerated.
- When neither profile is available, `fall_back_to_software()` sets
`LIBGL_ALWAYS_SOFTWARE=1`, terminates the display and the attach re-enters
itself once from `eglGetPlatformDisplay`: Mesa reads the variable in
`eglInitialize` (driver load), not at context creation, so a bare retry of
`eglCreateContext` would not pick llvmpipe up. Process-wide and one-shot;
skipped when the user already set the variable.
- Wayland tears its subsurface chain down before retrying (flag consumed at
`fail_after_subsurface`).
Both branches log one line on stderr so the chosen path is visible in a
bug report. Existing hardware that accepts the compat request is untouched.
One screen in nucleus-demo to test the macOS trackpad work by hand:
a root inspector (every Scroll / PanStart / PanMove / PanEnd with gaps,
counters and one summary per gesture incl. how long after the last move
the PanEnd came), vertical and horizontal strips for sign and magnitude,
a map canvas that pans on Pan and zooms on Scroll, a scrollable
DropdownMenu (inline, or NSPanel via "Open with native popup layers"),
and an embedded WKWebView whose page draws its own scrollY / wheel HUD.
The header shows density, px per wheel unit and the trackpadPanEvents
flag. NUCLEUS_DEMO_TAB=<tab> opens the demo straight on a tab.
Adds the composewebview dependency to nucleus-demo (in-tree Nucleus
modules excluded, as in tao-demo).
- native_view.m: an AppKit event is marked spent only once its delta has
actually travelled (replayed, or synthesised by the step that carries it),
so an Ended-with-delta arriving with no pan open (PanStart + PanMove from
one event) no longer loses the finger movement; per-child gesture state,
keyed on the child handle rather than a raw NSView*, keeps a deferred
PanEnd from sending a second terminal phase and resets the sub-point
residue at gesture boundaries
- NativeView hands pan offsets over in scene px; the macOS host converts
them back with its own scale, so an app-level LocalDensity override no
longer skews the embedded view's distance. dispatchScrollToNative is back
to its original signature (Windows / Linux untouched)
- TaoTrackpadPanRouter: the pan end is a deadline, one timer in flight;
finger and momentum steps only move the deadline, the grace re-schedules
once (earlier), the timer re-arms for the remainder — no coroutine, wake
and cancel per 120 Hz step
- TaoSceneScrollRouter: a zero-delta orphaned tail end is not turned into a
Scroll (AWT drops zero deltas); once-only announce on a plain volatile;
the redundant sceneBundle guard in onPointerScroll is gone (the router's
cancelled flag is the contract)
- TaoPopupSceneLayer documents why the pan uses the layer density while the
surface uses host.scale; TaoWindow uses import aliases for the ABI alias
constants; the injector checks the main thread before its static init
- headful: shared ScrollableColumn / ScrollableRow fixtures and a harness
awaitUntilOrTimeout replace the copies; the wheel-notch baseline is taken
right before the injection; the library drift test no longer reads the
demo source
- native_view.m remembers the AppKit event it last handed to a child
(NSApp.currentEvent is not cleared while the app idles, and one event can
yield two Compose steps), so a deferred PanEnd or a PanStart+PanMove pair
never applies a delta twice; the synthesised fallback keeps a per-view
sub-point residue instead of rounding slow drags to zero
- popup_panel.m and the vendored view.rs test NSEventPhase bits instead of
switching on the NS_OPTIONS mask
- TaoSceneScrollRouter leaves the pan position alone for MayBegin (its
PanEnd belongs to the previous gesture); an orphaned momentum tail is
delivered as AWT-shaped Scroll instead of dropped; once-only CAS behind a
plain read; scrollRouter declared with the rest of the host state
- TaoStandalonePopupHostMac.dispose() is idempotent on the invalid path too
- TaoWindow keeps AWT_PIXEL_TO_ROTATION / MACOS_AWT_SCROLL_AMOUNT as
aliases of the shared constants (public statics in the validated ABI)
- demo ScrollTestScreen finalises a gesture inline on PanEnd / PanStart
- TaoScrollWireDriftTest also guards the kNv* native-view wire and the
10-units-per-wheel factor in events.rs, native_view.m and the demo;
TaoSceneScrollTest pins MacOSCocoaConfig's 10 dp per unit on macOS;
harness routers get one timer slot each
- NativeView forwards PanStart / PanMove / PanEnd with a phase; native_view.m
replays the live AppKit event only when its phase class matches and
otherwise synthesises a phased scroll, so an embedded scroll view sees the
gesture begin and end (rubber-band, scroller fade) — Windows / Linux ignore
the phase
- popups keep the gesture phase whatever hasPreciseScrollingDeltas says,
like the Rust window path
- TaoTrackpadPanRouter: momentum steps only continue an open pan; a tail
arriving after the grace already closed it is dropped instead of stacked on
Compose's fling
- TaoSceneScrollRouter: lazily created timer scope with
TaoNonFatalCoroutineExceptionHandler (a broken deferred PanEnd costs one
gesture, not the app); TaoScrollGesturePhase.fromWire is a map lookup
- TaoStandalonePopupHostMac: finishPan() runs inside the frame pump; the
invalid-host dispose path also shuts the render executor down
- nativeDiagInjectScrollWheel targets the NSView it is handed, is nil-safe on
the primary screen, inlined; documented as whole-point only (CGEvent delta
fields are integers — verified) and the headful momentum tail uses whole
points
- demo ScrollTestScreen closes a gesture on PanEnd / next PanStart and names
the 10 dp factor; harness closes the pan after the cursor-move guard like
the host; TaoScrollWireDriftTest resolves its sources per test with a
readable failure; stray @Suppress dropped
- TaoTrackpadPanRouter: an open pan always has an end timer (150 ms grace
after the finger Ended, 1 s stall watchdog otherwise); MayBegin during the
momentum tail closes the pan at once; finishNow() for clicks / wheel
notches; TaoScrollGesturePhase is now an enum (wire codes attached),
distinct from the public TaoTrackpadPhase, unknown codes degrade to a
plain precise scroll
- loop: the phase decides the route for the whole gesture — a step that
arrives in lines is scaled to its point equivalent instead of being
dropped as a stray wheel Scroll; dispatch_scroll_gesture loses its bogus
dead_code allow; view.rs drops the dead ViewState binding
- TaoSceneScrollRouter: pan position / modifiers come from gesture steps
only (PanEnd must hit the node that got the PanMoves), a click or a wheel
notch closes the pan, cancel() makes late events no-ops, the timer scope
carries TaoFatalCoroutineExceptionHandler; TaoPopupSceneLayer reads the
live layer density; TaoStandalonePopupHostMac drops the native callback
before cancelling the router; first Pan routing logs one CONFIG line
- NativeView consumes PanStart / PanEnd over an embedded view so the
ancestor scrollable never opens a session of its own
- popup_panel.m: NucleusScrollGesture enum instead of literals;
TaoScrollWireDriftTest compares Rust, ObjC and Kotlin wire codes plus the
JNI descriptors
- nativeDiagInjectScrollWheel is inert unless NUCLEUS_TAO_INPUT_INJECTION=1
(set by taoHeadfulTest) and main-thread only
- README / CLAUDE.md document the Pan behaviour and
-Dnucleus.tao.trackpadPanEvents=false
Review follow-ups on #652 / #653 / #654:
- TaoSceneScrollRouter: single front door for wheel / trackpad input shared
by the window host and both NSPanel popup hosts; popup_panel.m now forwards
the AppKit phase so a two-finger swipe over a popup list behaves like the
window behind it. `-Dnucleus.tao.trackpadPanEvents=false` restores
AWT-style Scroll events for handlers that only know PointerEventType.Scroll
- tao patch 0007: PixelDelta carries AppKit's logical points, so the loop no
longer locks WINDOWS per event nor round-trips through two scale caches
- TaoTrackpadPanRouter: terminal steps with a delta still pan when no gesture
is open; momentum grace 150 ms, tunable via
-Dnucleus.tao.trackpadMomentumGraceMillis; isPanning removed
- NativeView forwards only non-zero PanMove (PanStart / PanEnd replayed
NSApp.currentEvent, stale for the deferred end)
- host detach cancels the router's timer scope and ignores scrolls after
the scene is gone
- TaoWindow: one preciseScrollEvent() shaping helper, shared AWT constants
(apiDump: the two leaked private-companion fields are gone)
- headful cases: recorder reset per run, display-mode restore even when the
window never reports the new scale; JNI descriptor drift guard for
popup_panel.m; demo ScrollTestScreen logs Pan steps too
Horizontal trackpad scrolling was reversed and precise deltas were scaled
by the display factor, both relative to the AWT backend; trackpad gestures
were indistinguishable from wheel notches.
- vendored tao (patch 0007): stop negating scrollingDeltaX in the macOS
scroll_wheel (AppKit already follows the MouseScrollDelta convention, as
winit does) and carry the full AppKit phase / momentumPhase on
WindowEvent::MouseWheel as `scroll_phase: ScrollPhase`
- loop: hand SCROLL_PIXEL over in logical points (AWT never applies the
display scale to preciseWheelRotation) and route phased steps to a new
EventCallback.onScrollGesture
- host: TaoTrackpadPanRouter turns the gesture stream into Compose
PanStart / PanMove / PanEnd with panOffset = AWT delta x 10 dp, keeping
the pan open across AppKit's momentum tail (deferred PanEnd) so Compose
does not stack its own fling; wheel notches and phase-less precise
scrolls stay Scroll events; NativeView forwards Pan to the native view
- popups: appKitWheelToAwtScrollDelta flips both axes and drops the scale
- headful e2e: MacOsTrackpadScrollHeadfulCases inject real scrollWheel:
NSEvents into the tao content view (nativeDiagInjectScrollWheel), the
#653 case flipping the display to its HiDPI twin; plus router, scene and
wire unit tests
The window and dialog openers had their composition target inferred, so a
single non-UI-targeted composable called in the `nucleusApplication` scope
(the Compose compiler bakes `@ComposableTarget` onto any unmarked factory
forwarding a target-marked content lambda) reclassified the whole scope —
nested windows included — and every `@UiComposable` call in it warned, which
is fatal under `-Werror`.
Declare each opener `@ComposableOpenTarget(-1)` with a `@UiComposable`
content lambda, the way Compose Desktop's own `Window`/`Dialog` are open:
they are callable from any applier and always compose UI content in the new
window's own composition, so neither direction of the applier leak survives.
Covers `DecoratedWindow`/`DecoratedDialog`, the `NucleusWindowHost` /
`NucleusDialogHost` interfaces with their default implementations,
`HostedWindow`/`HostedDialog`, and Tao's `DecoratedWindow`,
`DecoratedDialog` and `TaoStandalonePopup`. The Material/Jewel wrappers
inherit the same scheme through inference (`[_[UiComposable]]`).
`COMPOSE_APPLIER_CALL_MISMATCH` is only a warning, so both test compilations
escalate it to an error and a fixture per module compiles the reported shape
(non-UI factory in the application scope, UI content in the windows).
Two more instances of the #640 bug class, both fixed upstream and ported by
hand — a non-reentrant lock held across a call that pumps or synchronously
sends messages, so a nested window-procedure dispatch re-locks it and the
event-loop thread parks in `WaitOnAddress` for good.
- IME: the window-state lock was held across `ImeHandler::process_message`,
which peeks the queue on `WM_IME_ENDCOMPOSITION` (the Korean IME sends the
committed string after the end message, so the queue decides whether to
defer). The peek now happens in `ime::peek_commit_queued` before the lock is
taken, and the result is passed in — mirroring upstream, where "IME
character-pending detection happens before acquiring window state locks".
Wider blast radius than #640: there only the keyboard arm took the lock,
here nearly every arm does, so any nested message deadlocked.
Upstream: tauri-apps/tao#1215.
- `TaskbarCreated`: the window-state lock was held across `set_skip_taskbar`,
which goes through `CoCreateInstance` + `ITaskbarList`. An STA COM call pumps
the message queue while it waits, so the window procedure is re-entered.
The flag is now read out before the call. Upstream: tauri-apps/tao#1264.
`Imm32Source` carries the precomputed flag instead of peeking on demand;
`ImeSource` is unchanged, so the IME state-machine tests keep driving the same
sequences through their stub.
`PeekMessageW` delivers pending cross-thread *sent* messages inline: the
kernel re-enters the window procedure through `KiUserCallbackDispatcher`
before the peek returns. Two non-reentrant `parking_lot::Mutex`es were held
across such a peek, so the nested dispatch deadlocked the event-loop thread
against itself — it parked in `WaitOnAddress` and never pumped a message
again, leaving the window permanently "Not Responding".
- `event_loop.rs`: the keyboard callback held the global `KEY_EVENT_BUILDERS`
map across `KeyEventBuilder::process_message`, which peeks. The builder is
now taken out of the map for the duration and put back afterwards (only if
the slot still exists, so a window dropped re-entrantly is not resurrected).
Its map value became an `Option` slot to make that take/put-back possible.
- `keyboard.rs`: the key-press and key-release arms held `LAYOUT_CACHE` across
their peek, while every other arm — and `update_modifiers`, reached from any
mouse or focus message — locks it too. The guard is now scoped to end before
the peek and re-acquired after it.
Reported as a freeze when moving a window between Windows 11 virtual
desktops: that path is keyboard-triggered and makes the shell send messages
to the window mid-peek. The reporter's stack shows the two nested window
procedure chains around `NtUserPeekMessage` and the park in `WaitOnAddress`.
Only injected or sent key messages can nest here — real keyboard input is
posted to the queue rather than sent, and the peeks use `PM_NOREMOVE` — so a
nested key message now finds an empty builder slot and is dropped instead of
hanging the process.
Three layers were losing WS_EX_TOPMOST on Windows:
- DecoratedWindowComposable created every window with a hardcoded
alwaysOnTop=false and only applied the real value from a later
LaunchedEffect, which loses the race against creation-time style
rewrites (acrylic, skip-taskbar). The real initial value is now passed
at creation.
- tao only issues the z-order SetWindowPos when its WindowFlags cache
diffs — and then with SWP_ASYNCWINDOWPOS — so once an external rewrite
drops the topmost band while the cache still says ALWAYS_ON_TOP, no
path ever repairs it. TaoWindow.setAlwaysOnTop now also applies the
z-order directly and synchronously via the new deco-bridge
nativeApplyTopmost (SetWindowPos without SWP_ASYNCWINDOWPOS), keeping
tao's cache in sync for its own future rewrites.
- New TaoWindow.reassertAlwaysOnTop() repairs the band after the known
clobber sites: every RESIZED event, and the WindowsBackdrop
apply/teardown/survivor-restore paths.
Adds a nativeIsTopmost probe (real GWL_EXSTYLE bit, mirroring
nativeIsBackdropActive) and a headful regression case driving the
issue's scenario: enable topmost, apply acrylic, resize, toggle the
flag both ways, tear the backdrop down — asserting WS_EX_TOPMOST at
each step.
Any snapshot write anywhere in the process wakes every scene's Recomposer
through the global apply observer, and the Windows host rendered and
presented those frames unconditionally. Each eglSwapBuffers (interval 1)
parks the shared event-loop thread until the next VBlank, so every
visible window re-presented at the animating window's rate — with two
windows open, VBlank waits serialize and input starves (dashboard lag
while a small always-on-top window is visible).
TaoSceneBundle now tracks a visualDirty flag fed by the scene's
invalidateLayout/invalidateDraw callbacks, and the Windows host skips
nativePresent when the frame carried no visual change. Presents that
must swap regardless of scene dirtiness are kept: a size/scale apply,
the OS modal resize/move loop, a resolved clear-colour change (read
outside the composition), and the first swap after show() — DWM does
not reliably retain a pre-show present, so TaoWindow.show() arms a
one-shot force through the new showHook.
Replace the compact MessageBoxW with an in-memory DLGTEMPLATE dialog run
on a fresh thread: error icon + message, the stack trace in a read-only
multiline Consolas EDIT with both scrollbars, and Copy / Close buttons
(Copy = select-all + WM_COPY, keeps the dialog open like macOS/Linux).
Falls back to the compact MessageBoxW when the dialog cannot be created.
The trace goes through dwInitParam + SetWindowTextW instead of the
template: DialogBoxIndirectParamW returns -1 outright for large item
strings (a VerifyError toString() is a ~100 KB bytecode dump). The
compact-fallback summary is capped at 2000 chars for the same reason —
MessageBoxW silently never shows on a string that large; this also
protects the macOS CFUserNotification fallback.
Parity with the Linux GTK dialog: the primary macOS path is now an
NSAlert with the stack trace in a scrollable monospace accessory view
and a Copy button, run by an osascript -l JavaScript child — a fresh
out-of-process NSApp, since ours cannot host a modal after tao latches
[NSApp stop:] on loop exit. The compact CFUserNotificationDisplayAlert
remains as fallback when the child cannot run (MDM-blocked osascript,
spawn failure, JXA error).
The fatal-error dialog now receives the full stack trace as a separate
detail parameter. On Linux the title no longer doubles as the primary
line (it showed 'Fatal Error' twice on WMs that draw dialog titlebars)
and the trace renders in a bounded scrollable monospace view with a
Copy button instead of an unbounded secondary label that sized the
dialog to the whole screen. macOS and Windows keep the compact alert:
message plus the throwable toString extracted from the detail.
buildAotJavaArgs now injects -Dnucleus.tao.fatalErrorDialog=false so a
training crash cannot hang in the #622 native dialog. Update the
argument-order assertions to match.
Show a modal GtkMessageDialog after the loop exits, install a shared
coroutine exception handler on every scene, and recheck pending fatals
before exitProcess(0) so a composition-side crash cannot quit with 0.
Unattended runs (headful tests, AOT training) disable the dialog via
-Dnucleus.tao.fatalErrorDialog=false.
Implements the Windows arm of the native fatal-error dialog: task-modal
MessageBoxW (MB_OK | MB_ICONERROR | MB_SETFOREGROUND), no owner HWND
since the Tao loop has exited and every window is destroyed.
The box must run on a dedicated thread: called directly on the ex-loop
thread it dismisses itself instantly (leftover quit/thread message in
that queue after tao's run_return) - the Windows analog of the stopped
NSApp problem that pushed macOS to CFUserNotification. A fresh thread
gets a fresh message queue; join() preserves the blocking contract.
Verified e2e on Windows: injected fatal in swing-tao-demo, dialog stays
open until dismissed, then rethrow and exit 1 from the caller.
Linux remains a silent no-op.
An unhandled exception on the Tao main thread used to be cleared at the
JNI boundary, leaving a frozen, silent, unclosable window. Default path
now: SEVERE log with the full stack trace, blocking native error dialog,
clean loop exit, rethrow from TaoApplication.run (taoApplication turns it
into exitProcess(1); 0 stays the normal-quit code).
- nativeShowErrorDialog in the existing nucleus_tao crate. macOS only for
now via CFUserNotificationDisplayAlert - out-of-process, any-thread, no
NSApp/run-loop dependency, so it is safe after the loop teardown where
tao's latched [NSApp stop:] makes an NSAlert runModal session return
immediately, and it cannot re-enter tao's non-reentrant handler mutex
the way a modal pumped inside a callback frame can (Dock-reopen / deep
link). Windows and Linux are silent no-ops until their implementations
land.
- TaoApplication.reportFatal: first report wins (AtomicReference), exit()
posted before logging so even a throwing formatter cannot freeze the
loop; dialog + rethrow happen in run() after the loop exits. State is
reset on run() entry so the loop can be re-run.
- taoApplication installs a CoroutineExceptionHandler routing composition
and effect failures (recomposition, LaunchedEffect, setContent) to the
fatal path - previously they hit stderr and the process exited 0. The
macOS FrameDispatcher render loop gets the same handler (a frame
exception silently stopped repainting under the SupervisorJob).
- Non-fatal native->JVM upcalls (deep link, a11y actions) and
TaoMainDispatcher's swallowed block failures now log at SEVERE instead
of vanishing / printStackTrace.
- -Dnucleus.tao.fatalErrorDialog=false skips the dialog for unattended
runs (CI, AOT training).
- swing-tao-demo catches the documented rethrow and exits 1 instead of
leaving a zombie process on the non-daemon EDT.
Verified end-to-end on macOS: raw-run fatal (dialog + rethrow, exit 42
from the caller) and Compose-composition fatal (CEH -> dialog -> exit 1).
A scrollbar drag in a TrayApp popup scrolls synchronously inside the
pointer event (LazyListState.onScroll -> forceRemeasure), subcomposing
new lazy items whose LaunchedEffects dispatch into the scene dispatcher.
The standalone hosts' FlushingDispatcher schedules a frame on every
dispatch, and StandaloneFramePump rendered it inline: measureAndLayout
re-entered while the first pass was still on the stack, throwing
'performMeasureAndLayout called during measure layout' and corrupting
the scrollbar drag state ('entered drag with non-zero pending scroll').
The pump's re-entrancy guard only covered renders it started itself.
Extend it with nonReentrant {}: every scene entry point of the three
standalone hosts (pointer, scroll, key dispatch, setContent) now runs
inside it, so a schedule() arriving mid-pass is posted to the Tao main
queue instead of rendered inline - one event-loop turn of latency at
worst, no re-entry ever.
Covered by a scene-harness repro (real thumb drag over a LazyColumn in
the hosts' mirrored pump wiring, plus a negative test pinning the
re-entrant failure mode) and pump unit tests.
Three `gradle` jobs ran the full 6h Actions default before being killed —
`:launcher-linux:test` blocks in `g_bus_get_sync(G_BUS_TYPE_SESSION)` on a
runner with no session bus (the tao-headful job dbus-launches one, the gradle
job does not), and `:launcher-linux:check` never reports. The hang itself is
untouched here; the caps just stop one from holding a concurrency slot for six
hours. Every job observed takes 2-10 min, so the limits are 3-10x headroom.
test-packaging and test-graalvm already carried their own.
Also fix the #576 baseline chrome estimate, which has failed tao-headful on
Linux CI since 2026-08-27. Under Xvfb + openbox `outerBoundsPx()` reports a 1px
outer height for a while after mapping, so the baseline mode came out at -359
and every drift gate below it measured noise (reported 367px against a real
chrome of 8). Estimate only from frames where the outer box actually covers the
scene — an invariant on every platform — and fail with the measured numbers
instead of a bogus verdict when no such frame exists.
TaoScenePointerSlopTest reproduces the issue's parent-drag + child-clickable
shape: 0.2 px of press/release jitter must click (not drag) at 1x and 2x,
real motion must still drag, suppressed moves must never reach the scene,
press must dispatch at the last dispatched position, and touchSlop must be
18.dp in physical px. The harness mirrors the hosts' deadband dispatch, and
the new class is registered in the native-image test battery.
Fixes#615 — selections needing two clicks on the Tao backend.
Tao ships 1/1024-px cursor positions and macOS emits a CursorMoved before
every mouseDown/mouseUp, so a click drifting a fraction of a pixel between
press and release produces a real Move delta. Compose's mouse slop is only
touchSlop x (0.125dp / 18dp), so ~0.2 px of drift starts any parent drag
gesture, which consumes the move; the child clickable then sees a consumed
change in the Final pass and cancels the tap. The AWT backend never sees
this because it quantizes positions to integer points — a de-facto 1 dp
deadband.
Two changes, shipped together:
- TaoPointerDeadband: Moves under 1 dp from the last *dispatched* position
are suppressed; press/release/exit/scroll dispatch at the last dispatched
position (anything else and SyntheticEventSender re-injects the suppressed
delta as a synthetic Move); motion past the deadband keeps its exact
sub-pixel position. Wired into all three window hosts, including the
Windows blending-overlay stream and the Linux overlay scroll dispatcher.
- TaoPlatformContextBase.viewConfiguration: density-scaled touchSlop
(18.dp, AWT/Android parity) instead of PlatformContext.Empty's 18 raw px,
which halved every drag/scroll threshold on 2x displays. sceneScale is
provided live by each host and popup layer.
AWT Robot input injection ran on the Tao event-loop thread. On Wayland the
JDK routes it through the XDG RemoteDesktop portal, and when the compositor
refuses the session ("Session is not allowed to call NotifyPointer methods")
mousePress never returns from the native call — the loop froze and the global
watchdog halted the JVM, losing every result of the run.
- HeadfulRobot drives gestures on Dispatchers.IO under a timeout and latches
the first failure so later calls fail fast instead of parking another thread
on the same wedged native lock. new Robot() succeeds on such a host, so
constructing one was never a usable availability probe.
- Apply TaoWindowTestCase.timeoutMillis, which was declared but never used:
a driver that stalls now fails its own case instead of running out the
global watchdog. TimeoutCancellationException is caught before the
CancellationException rethrow so it is not mistaken for app teardown.
- Fix the #576 tremble metric. maxSceneVsInner was overwritten by an
outer-derived value, duplicating maxSceneVsOuter and never comparing the
scene against the reported inner size; a sample-count guard now keeps that
gate from passing vacuously. GTK CSD frame extents are not a constant inset
either (52/53/56 across one resize animation, outer leading the resize event
by a frame), so the scene-vs-outer drift must survive consecutive frames to
count as tremble.
Follow-ups from review of the two commits above.
Correctness:
- The `ImeReplaceCommit` branch did not set `key_triggered_ime`, so the
key that picked the accent was also delivered as a raw key event. An app
shortcut bound to a digit fired while the user was only choosing an
accent. Both sibling IME branches already set it; the #595 invariant is
that a key the input method consumed is not double-delivered.
- `attributedSubstringForProposedRange:` could underflow: `NSMaxRange`
wraps on an overflowing proposed range, slips past both guards, and
`end - loc` became a huge length, raising `NSRangeException` from inside
an AppKit callout.
- Document-cache invalidation was global, so focus moving between windows
(the old session tears down after the new one starts) wiped the cache
the newly focused field had just installed. It is now scoped to the
owning view, resets the marked-text anchor, and `detach()` invalidates
explicitly so a freed view's address cannot be inherited by a later view
allocated at the same address.
- A stale `markedText` ivar pinned the marked-text anchor from a previous
field; an invalid cache now forces a re-anchor.
- `replaceCommit` ends the composition before committing: `commitText`
replaces the composing region when one exists, which would have silently
ignored the selection it had just set.
- The no-session fallback hardcoded a single Backspace regardless of the
replaced length; it now deletes what the range asked for, bounded.
Fidelity and cost, both on the keystroke hot path:
- `nativeSetImeDocument` read the JVM string through modified UTF-8 and a
lossy decode — two extra copies, and a length that can drift from the
offsets the JVM computed (unpaired surrogates). It now reads the UTF-16
directly via `GetStringRegion`, as its comment always claimed.
- The pushed window drops from ±512 to ±128 UTF-16 units, closer to the
±100 Chromium ships. The only reader is
`attributedSubstringForProposedRange:`, which is only ever asked near
the caret.
- `dispatch_ime_replace_commit` reuses the shared JNI helper instead of
duplicating its attach/exception handling.
Tests:
- The probe left `e` held down when the layout-sanity check bailed out.
Key state lives in the window server and outlives the child process, so
that leaked a stuck key into the next scenario and the rest of the
session.
- The repeat scenario asserted an exact six characters; the claim is that
the key repeats, not how many synthetic autorepeats survive coalescing.
`TaoPressAndHoldE2ETest` (opt-in, `NUCLEUS_TAO_SMOKE=1`) replays real
keyboard traffic — CGEvents at the session tap, `kCGKeyboardEventAutorepeat`
stamped so AppKit sees a held key — against a focused Compose field. Each
scenario runs in its own child JVM so `ApplePressAndHoldEnabled` can be
set per scenario through the process argument domain, and because
`taoApplication` ends in `exitProcess(0)`.
The expectations are not invented: they are the behaviour recorded from
AppKit driving a reference `NSTextView` and a document-backed
`NSTextInputClient` on the same machine — what Chrome, Notes and TextEdit
do.
- press-and-hold disabled by the user: a held letter repeats
- press-and-hold enabled: the held letter never goes dead and the next
keystroke is not eaten
- the picker's `insertText:"é" replacementRange:{0, 1}` replaces the base
letter
- key roll-over while typing `xcode` is not misread as an accent pick
All four fail on the previous implementation: 'e' instead of 'eeeeee',
'x' instead of 'e…x', a watchdog timeout on the replacement commit, and
'xcde' instead of 'xcode'.
Stage-1 coverage for the replacement commit itself lands in
`TaoSceneImeTest`: in-range replacement, surrounding text left intact
with the caret after the accent, and an out-of-bounds range clamped
rather than thrown.
Two traps worth knowing, both hit while writing this:
- A scenario that engages the real picker must dismiss it with Escape
before the next keystroke and at scenario end — the bubble is a system
window and outlives the child process, so it steals the following
scenario's keys.
- CGEvent posting inherits the machine keyboard layout, so the probe
sanity-checks the first keystroke and reports `skip=layout` instead of
failing on a non-Latin layout.
`nucleus_tao_post_key_to_view` gained an autorepeat flag and
`nucleus_tao_inject_insert_text` a replacement range; both default to the
previous behaviour for existing callers.
Nucleus forced `ApplePressAndHoldEnabled` on in four domains — app,
argument volatile, CFPreferences, registration — from both `+load` and
runtime, then guessed when the accent picker was active from a state
machine (`g_letter_key_down`, `g_press_and_hold_queried`, `g_base_text`).
Both halves were wrong:
- Forcing the default suppresses key repeat, but does not guarantee the
picker engages. Where it cannot (non-Apple keyboards, Karabiner virtual
devices), a held letter did nothing at all — no repeat, no picker,
strictly worse than either OS behaviour and invisible to the user
(#612).
- The heuristics misread ordinary fast typing: with the previous key
still physically down, a `selectedRange` query armed the picker flag
and the next commit was routed into the replace path, deleting the
character that had just been committed ("Xcode" landing as "Xcde")
(#611).
Chromium does neither. `ApplePressAndHoldEnabled` has zero occurrences
in its tree and its history; the picker works because
RenderWidgetHostViewCocoa answers `selectedRange` /
`attributedSubstringForProposedRange` over a cached window of committed
text and honors `replacementRange` in `insertText:`. Recording AppKit
against an `NSTextView` and a document-backed `NSTextInputClient`
confirms the protocol: the accent pick arrives as `insertText:"é"
replacementRange:{caret-1, 1}` — UTF-16, document-absolute — and the
range carries everything. No client-side detection is involved.
Same model here:
- Never read, set or register the user default. The OS decides whether a
held letter repeats or opens the picker.
- The host pushes the focused field's committed text (a bounded window),
its document-absolute offset and the selection through
`nativeSetImeDocument`, mirroring Chromium's renderer-to-browser
selection + surrounding-text push. The swizzled getters serve
document-absolute answers from that cache; the marked-text anchor is
maintained optimistically in a `setMarkedText:` swizzle, the same
fallback chain Chromium uses for `_markedRange`.
- `insert_text` routes a valid `replacementRange` outside a composition
to `WindowEvent::ImeReplaceCommit`, and `TaoImeSession.replaceCommit`
applies it select-then-insert — Blink's `ReplaceTextAndKeepSelection`
semantics — instead of a blind `deleteSurroundingTextInCodePoints(1, 0)`.
Routing the pick through tao's event queue also removes a re-entrant
`EVENT_CALLBACK` lock: the previous direct upcall from the AppKit callout
deadlocked when the JVM re-entered native on the same thread.
Repeat keyDowns still reach `interpretKeyEvents:` (unchanged), which is
what lets the picker engage where the OS allows it.
Closes#612.
Pin the x11-services ordering that keeps a login-started app from
freezing sun.java2d.uiScale at 1.0, and cover the ExecStart quoting of
paths containing spaces and double quotes, which had no coverage.
buildUnitContent becomes internal to be testable; the enclosing object is
already internal, so the public ABI is unchanged.
graphical-session.target is reached before gsd-xsettings writes Xft.dpi
into the X resource database. Toolkits that sample the display scale once
at startup - skiko's autodpi freezes sun.java2d.uiScale at library load -
then probe an empty database and stay at 1.0 for the life of the process,
so a HiDPI app auto-started at login renders at half size while a manual
launch is always correct.
Order the generated unit after gnome-session-x11-services-ready.target,
the synchronization point declared by org.gnome.SettingsDaemon.XSettings
via Before=. The non-"ready" target is ordered ahead of that service, so
it is listed for documentation only and does not close the gap on its own.
systemd ignores ordering dependencies on units that are not loaded, so
this is a no-op on non-GNOME desktops. XSettings.service is Type=dbus, so
the race is narrowed to sub-second rather than provably closed.
Closes#614
Tests no longer mutate TaoMainDispatcher.taoMainThread or swallow leaked fallback frames. Nested work is a drained queue; a posted frame after dispose is dropped.
Make disposed volatile, document that rendering is main-thread-only, and add mapper tests so Windows/Linux/macOS scrollAmount policy cannot be silently unified.
Feed tray/overlay/owner-based popup wheel events through the same AWT-shaped dispatch as TaoWindow, apply Windows lines-per-notch via LocalScrollConfig, and paint standalone popup frames inline when already on the main thread so a wheel flood cannot starve MainEventsCleared.
The local outside-click monitor treated mouseDown on the NSStatusItem
(and the native context menu) as outside the panel. Right-click therefore
hid the tray popup before the menu appeared. Skip those windows for
standalone panels; left-click toggle stays with the tray callback.
Ownerless NSPanels had default collection behavior, so Sonoma+
"Click wallpaper to reveal desktop" Exposé-slides the tray popup
while Compose is already fading it. Mark the standalone panel
stationary (and join-all-spaces / fullscreen-auxiliary / ignores-cycle)
so we dismiss it ourselves.
Popup NSPanels forwarded raw AppKit scrollingDelta. TaoWindow already
reshapes those to AWT preciseWheelRotation (negate Y, physical /10 for
precise deltas). Tray and overlay popups skipped that path, so scroll
was inverted and trackpad magnitude disagreed with the main window.
Route both hosts through dispatchAppKitScroll.
TaoStandalonePopup on Linux is a raw override-redirect X11 window, so
GTK gtk_drag_dest_set cannot attach. Implement XDND 5 on the panel
event thread — the creating client, which is the only one that receives
mask-0 ClientMessages — and dispatch through TaoSceneDnD like the
Windows/macOS hosts.
Closes#605
Popup and overlay WndProcs report raw Win32 wheel units. TaoWindow
already negates those to AWT preciseWheelRotation; standalone and
owned popup layers forwarded the raw sign, so TrayApp scrolling
was inverted.
NSTextInputClient was lying to Kotoeri/ATOK (markedRange off-by-one,
selectedRange NSNotFound, nil substring). Live conversion then committed
a U+F7xx function-key char, which wiped the Compose preedit.
Programmatic height animation left the HWND/NSWindow a frame ahead of the Compose scene, and native resize echoes wrote WindowState.size backwards. Apply inner size synchronously, push EVENT_RESIZED in the same turn, and ignore in-flight programmatic echoes (#576).
CJK composition was invisible and the committing Enter leaked as a
newline. Forward setMarkedText/unmarkText as preedit, commit via
commitText, and drop keys the IME consumed on the native side.
The taoHeadfulTest javaagent path hardcoded kover-jvm-agent-0.9.8.jar,
so the Kover 0.9.9 catalog bump left the JVM pointing at a missing jar
and the headful suite failed to start on every OS.
An app.id with characters illegal in a D-Bus name (e.g. "Music Radio")
produced an invalid default MPRIS bus name. g_bus_own_name() rejected it
without invoking on_name_acquired/on_name_lost, so nativeStartListening()
waited on the ready condition forever — blocking the caller (often the
Tao UI thread during first composition) and leaving the app windowless
while it still owned the single-instance lock.
- MediaControlService: sanitize NucleusApp.appId into a legal D-Bus name
for the default dbusName (illegal chars -> "_", digit-leading elements
prefixed, empty elements dropped, 255-char limit respected)
- native: validate the name with g_dbus_is_name() before spawning the
service thread and skip MPRIS with a warning instead of blocking
- native: fail open (g_ready = -1 + broadcast) if g_bus_own_name()
returns 0 without invoking any callback
- native: bound the ready wait with a 10 s timeout as a backstop so a
stalled session bus can never block the UI thread indefinitely
- NativeLinuxBridge: log a JUL warning when MPRIS registration fails
Fixes#548
DialogTitleBar drew its own close button with hardcoded Win10 colors and
no hover fade. Delegate to the shared WindowsWindowControl so the dialog
close button is pixel-identical to DecoratedWindow's caption buttons:
Win11 WinUI tokens, snap-in / 150ms fade-out animation, active/inactive
and dark-mode glyph variants, custom TitleBarStyle colors, and native
HTCLOSE hit-zone publication.
Fixes#584
gpg --import/--verify daemonize a gpg-agent for the throwaway homedir; with
no idle timeout it inherits the app's systemd scope and keeps it reported as
running forever, and the trailing exec bypassed the EXIT trap so the
root-owned work dir leaked too.
Verify with gpgv (never starts an agent) against a gpg --dearmor'd keyring
(pure filter), and drop the execs so the EXIT trap cleans the work dir —
set -e still propagates dpkg/rpm exit codes unchanged.
Fixes#567