Skip to content

fix(shell): a hover on the window in front never reaches a card window behind it - #295

Merged
ymote merged 6 commits into
mainfrom
fix/wm-hover-through
Oct 2, 2026
Merged

ymote merged 6 commits into
mainfrom
fix/wm-hover-through

Conversation

@ymote

@ymote ymote commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up to #277. Its "Found on the way" noted that hover still passes through a module tile.

Cause

News and Mail are script apps, hosted in-process as module tiles (MpModuleView, crates/shell/src/module_view.rs). The desk offers every pointer event to its tiles topmost first (desk.rs, WmDesk::handle_event). Makepad's hit tests are a first-wins claim (handled).

  1. A module tile claimed a press inside it, but never a hover. A mouse move over a part of Mail that takes no hover, such as its empty Add account page, left Mail's tile unclaimed.
  2. The tile behind handed that move to its app, as it did every move. News's widgets hit-test hovers with event.hits…, found the move unclaimed and took it. A tab button (ButtonFlat) under Mail lit up (hover.on) and asked for its hand cursor. A text field there would show its I-beam.
  3. The tile behind also handed its app moves that a window in front had already taken. Presses went the same way before fix(shell): a press on the window in front never reaches a card window behind it #277.

A process tile never had this problem. Its event.hits(cx, self.area) claims a hover over it and never yields one that another area claimed.

Fix

crates/shell/src/module_view.rs now treats a hover the way #277 treats a press. A hover is a mouse move with no button held (hover_claim).

  • Live tile:
    • A hover inside it that none of its app's widgets took belongs to the tile. The tile claims it after the root, as it claims a press, so no window behind sees it unclaimed.
    • A hover inside it that a window in front already took is let by, and the app never sees it.
    • One exception: the move that ends a hover the app still holds. hovered records that one of the app's widgets took the last move. That move still reaches the root, so the widget sees its hover end. The move is claimed, so it starts no other hover.
  • Closed face of a stopped app: it claims a hover inside it, as it claims a press.
  • Unchanged: drags (moves with a button held), releases and moves outside the tile pass as before, so captures in progress keep working.

Tests

New unit tests in crates/shell/src/module_input_tests.rs use #277's cascaded tiles and its front-to-back offer. They need no kernel and no display. The probe there now also counts the hovers that reach it and the hovers it takes. It takes them through the same hit test that a Button or a GestureView uses.

  • a_hover_on_the_window_in_front_never_reaches_the_app_behind: a hover on Mail where Mail takes none. Mail's app sees it, News's app never does, and the move leaves Mail claimed.
    • Control: where News is uncovered, its row takes the hover.
    • The move that then carries the pointer over Mail reaches News once, to end that hover. No later move reaches News.
  • a_button_behind_the_window_in_front_never_lights_up_under_it: makepad's own Button as News's root. Under Mail it neither lights up nor sets the hand cursor. Where it is uncovered, it does both.
  • a_hover_on_a_stopped_face_never_reaches_the_app_behind: the closed face takes the hover. Clear of the face, the app under it takes the hover.
  • In module_view.rs: only_a_mouse_move_with_no_button_held_is_a_hover_to_claim.

Everything below ran on the Linux build host.

#277's head plus only the new tests (no fix): all 3 new tests fail, each at its bug assertion.

  • Hover on Mail: News's app saw the hover and took it, (1, 1) where (0, 0) is expected.
  • Button: the cursor over Mail was Hand where Default is expected.
  • Closed face: the app under the face saw the hover and took it, (1, 1).

With the fix, at 9764139 on main cf526f4 (the merge of #277): the checks ran on 8accfcc1, the same change on #277's rebased head d030129. The two commits have the same tree, as main and d030129 do.

Instrument, before and after

The table is the run on main's tree (#277's rebased head d030129) and on 8accfcc1. An earlier run on #277's original head 6524358 gave the same results.

Setup:

  • The Linux build host ran its own weston --backend=headless --renderer=pixman --idle-time=0 with makepad's Wayland backend (llvmpipe).
  • The shell was cargo build --release --locked -p octosense, driven over --remote.
  • Frames come from MAKEPAD_WRITE_FRAMEBUFFER_PNG. Each run used a fresh scratch home and the mock model.

The run:

  1. Open News, then Mail, which cascades over it.
  2. Drag Mail by its title bar until its left edge (x 243) cuts News's TechMeme tab (x 189–297). The tab is a ButtonFlat that takes a 4 % dark tint while hovered. Its left part shows beside Mail and its right part lies under Mail.
  3. At each step, compare the 990 pixels of the tab's visible part with the frame taken while the pointer was on the bar.
Pointer main (#277 merged) this branch
On Mail, over the covered part of the tab (270, 298) The tab lights up under Mail: 890 of 990 pixels change, mean RGB (210.7, 215.9, 217.0) → (203.2, 207.4, 208.6) 0 pixels change
Control: on the tab beside Mail (216, 298) Lit: 890 pixels Lit: 890 pixels
Back on Mail, over the tab Stays lit: 890 pixels 0 pixels: the tab's hover ended

Not run: a real display or a physical mouse. The frames do not show the pointer, so the hand cursor is covered only by the unit test (cx.mouse_cursor()).

🤖 Generated with Claude Code

ymote and others added 6 commits October 2, 2026 13:57
The supervisor saw the kernel end (its stdout closing, or its exit
status) and read the stderr tail at once, but stderr is copied into the
tail by a task of its own. The line the kernel wrote just before it
exited could still be in the pipe, so the reason lost it: CI's
`a_kernel_that_dies_closes_its_connections_with_its_last_words` failed
with "the kernel closed its output: fake kernel up", without the
"asked to exit" the fake kernel wrote last.

The tail is now read after the kernel is stopped and the stderr task has
reached the end of the pipe (bounded at 2 s, for a process the kernel
started that still holds the pipe). Failed reasons get the same complete
tail. The test asks for the last words themselves, not just "exit".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… probes

The glance tile tests register probe system apps (os.glancetile,
os.glanceinput, os.glancequiet, os.glancesweep) in App Hub's
system-app registry. That registry is process-wide, has no removal, and
is the one `system_card_apps` lists, so a catalog test running after a
glance test in the same process saw the probes as launcher rows:
`bundled_apps_open_without_catalog_files_or_child_processes` failed its
catalog comparison 7 of 10 times beside `glance_card::` on main, and
`the_system_apps_ship_as_card_apps` compares the same list.

Tests now register their system apps through
`apps::test_system_apps::register`, which notes the id before it
registers it, and the tests of the build's own catalog leave noted ids
out. Production code is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`approvals::tests::the_relay_gets_decisions_made_before_it_was_installed`
began with `init_memory()`, which replaces the shell's process-wide
approvals with empty ones. Every consent a test running beside it had
given was gone. `app_chat::tests::the_panel_opens_a_sharing_context_and_sends_person_turns_there`
allows its app, then reads `agents::line()`, which reports a peer slug
only while the app is allowed. Beside `approvals::` it failed 50 of 50
runs with `peer_slug: null`. The same test also installed its recording
relay into the shared approvals, where it would take other tests'
decisions.

The relay seam (`approval_requested`, `set_relay`, `bus_requested`) is
now methods on `Approvals`. The free functions call them on the shell's
approvals, as before. The test builds `Approvals::memory()` and never
touches the shared one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the whole connect's

`a_jail_only_app_reads_its_jail_and_is_refused_everything_else` failed
on GitHub's macOS 14 runners with "the hub is reachable: " and nothing
after it: the sandboxed `nc -z` exited 1 without a word, because nc
prints why a connect failed only with `-v`. Its exit status says only
that the connect did not complete. A sandbox refusal and a refusal past
the sandbox (no listener on the port then, the network stack) look the
same, though only the first is what this test is about. Closing the
hub's listener before the probe reproduces the CI message exactly.

The probe now runs `nc -v -n` and the test reads what nc says. The hub
fails only on the sandbox's own refusal (EPERM from Seatbelt and
seccomp, EACCES from Landlock), or when nc never reported a connect at
all. A refusal past the sandbox is printed and passes. Any other port
must now be refused by the sandbox itself, not just fail, which the old
check could not tell apart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
octos#2675 makes session/hydrate rows name their tool calls: a tool row
carries tool_call_id and tool_name, an assistant row its tool_calls. The
shell's system chat and "Ask <app>" panel already read tool_call_id and
name/tool_name, and the broker's merged history passes rows through, so
reloaded tool rows now show their tool's name instead of "tool". The
two-lane scenario checks it against the real kernel: News's agent's
news_share call in the panel's reloaded history, and the system chat's
peer_send_input.

The pin also brings #2261, #2664, #2665, #2669, #2671 and #2672.
octos-core, octos-cli, octos-research and the kernel artifact move
together; Cargo.lock changes only the 17 octos source lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…w behind it

A module tile claimed presses but never a hover. A mouse move over a part
of a card window that takes no hover (Mail's empty page) went on to the
tile behind unclaimed, and the app there took it: a News button under Mail
lit up and showed its hand cursor. A tile behind also handed its app every
move, claimed or not, as it handed presses before #277.

The live tile now treats a hover (a mouse move with no button held) the
way #277 treats a press. A hover inside it that none of its app's widgets
took is the tile's, and one a window in front already took is let by, so
its app never sees it. The one exception is the move that leaves a widget
of the app still holding the hover: it reaches the root, so that widget
sees its hover end, and being claimed it lets no other hover start. The
closed face of a stopped app claims a hover inside it too. Moves with a
button held (drags) pass as before, so captures keep working.

Tests, in module_input_tests.rs: a hover on the window in front never
reaches the app behind; makepad's Button behind it never lights up or
shows its hand; a hover on a stopped face never reaches the app behind it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ymote

ymote commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

Merged in a merge train; local CI (Mac with rom toolchain + --linux-host) passed on the train head 72e5d55, which contains this PR.

  • tools/ci-local.sh --only all --linux-host, started 2026-10-02T14:00:45: passed. Desktop, phone and apps passed on the Mac; apps services, kernel-security and linux-host / sandbox passed on the Linux build host. Its rom.yml steps were skipped on the Linux host, and the train doesn't trigger rom.yml.
  • tools/ci-local.sh --only all on the Mac with the rom toolchain (JDK 17, Android SDK, node 24), started 2026-10-02T14:51:21: 42/42 steps passed, none skipped.

The train was #291, #293, #297, #294, #298, #295, #296, #299, #300 and #292, on main f6678b5. After the last merge, main 301be0c has the tested head's tree.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant