Skip to content

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

Merged
ymote merged 1 commit into
mainfrom
fix/wm-click-through
Oct 2, 2026
Merged

ymote merged 1 commit into
mainfrom
fix/wm-click-through

Conversation

@ymote

@ymote ymote commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Fixes B3 from the headless instrument run (scenario X1): a click on the focused top window reached the window beneath it.

Root cause

News and Mail are script apps in App Hub's Card runner, hosted in-process as module tiles (MpModuleView, crates/shell/src/module_view.rs). Open News, then Mail: Mail cascades over it (News at 54,105, Mail at 84,135, on top and focused). A click at (700, 700) lands on Mail where Mail has nothing to take it, over News's third story.

  1. The desk offers a press to its tiles topmost first (desk.rs, WmDesk::handle_event) and relies on makepad's first-wins claim (handled). Mail's tile claims a press inside it when none of Mail's widgets took it.
  2. News's tile is offered the same press next. It already did not raise itself for a claimed press, but it still handed the press to News's app, counting on the claim to keep the app's widgets off it.
  3. The claim does not do that for every widget. News's story rows are GestureView{on_tap: …} (apps/news/bundle/main.splash). makepad's GestureView hit-tests a mouse press with hits_with_capture_overload(cx, area, true) (widgets/src/gesture_view.rs), which captures a press another area has already claimed, and it reads touches straight from the raw stream. So the row captured the press and fired on_tap on release, and News opened the story. PortalList and FlatList drags, and a View's on_item_tap, co-capture the same way.

The same happens when the press does land on a Mail widget. During the Mail sign-in, the press on Mail's "Add account" button, and the presses on the host sheet's fields, opened the News story under them (the instrument saw this in every run).

A process tile never had this problem: its gate is event.hits(cx, self.area), which never yields a press another area claimed.

The cause is in the shell, not makepad. Capture overload is how makepad lets a widget share a press with the scroll view around it, and that is by design. Keeping separate windows apart is the window manager's job.

Fix

crates/shell/src/module_view.rs:

  • Live tile: a press (a button going down, or a new touch) that is already claimed when it reaches a module tile is let by, just like a press outside the tile. The tile is not raised and its app never sees the press. Moves and releases pass as before, so captures already in progress keep working.
  • Closed face of a stopped app: it takes presses the same way and claims the one it takes. A click on a window over its Restart no longer restarts the app, and a click on the face no longer also raises the window behind it.

Tests

New crates/shell/src/module_input_tests.rs draws module tiles cascaded, as the desk draws windows, and offers each press to them front to back, as the desk does:

  • a_click_on_the_window_in_front_never_reaches_the_app_behind

  • a_touch_on_the_window_in_front_never_reaches_the_app_behind

    A click, then a touch, on the front window where it takes nothing, over a probe row behind it. The probe takes presses the way GestureView does. Only the front window is raised, and the app behind never sees the press. Control: where the back window is uncovered, its row takes the click or tap.

  • a_gesture_view_behind_the_window_in_front_never_captures_its_press

    The same press with makepad's own GestureView as the row behind. It no longer captures the press, which is what its tap fires on. Control: it does capture where it is in front.

  • a_stopped_tile_takes_only_a_press_nothing_in_front_took

    A click on the window over the closed face's Restart does not restart the app. A click on the face raises only it, and the app under the face never sees the press. With nothing over it, Restart still restarts.

All of the following ran on the Linux build host, at this head (6524358, on main c56ae14e). It merges cleanly with main 4f73ab34; #274 does not touch module_view.rs or desk.rs.

Main c56ae14 plus only these tests (no fix): all 4 fail, each at its bug assertion.

  • Click and touch: News's app saw the press and tapped, (1, 1) where (0, 0) is expected.
  • GestureView: it captured the press made on Mail.
  • Closed face: [Raise(2), Restart(1)]; the click on Mail restarted the stopped app under it.

With the fix:

  • The 4 new tests and module_view's own 2 pass.
  • The shell's whole suite passes, from phone/: cargo test --locked --features mobile-apps -p octosense-shell gives 770 passed, 0 failed.
  • These checks also pass on this head:
    • From desktop/: cargo check --locked -p octosense, the same with --features mobile-apps, and bash ../tools/check-shell-graph.sh -p octosense.
    • From phone/: cargo check --locked -p octosense-home --features mobile-apps and its graph guard.
    • python3 tools/setup.py --check --cargo.

Instrument, before and after

Setup:

  • Linux build host: weston --backend=headless --renderer=pixman --idle-time=0 and makepad's Wayland backend (llvmpipe).
  • cargo build --release --locked -p octosense, driven over --remote.
  • Frames come from MAKEPAD_WRITE_FRAMEBUFFER_PNG. The home is a fresh scratch home, with the mock model.
  • The driver is the earlier run's harness, with X1 on its own plus a sign-in variant.

Layout from /snap: News's card is at 54,105 (990×611) and Mail's at 84,135, on top. News's story rows (GestureView) start at y 349, 458 and 637.

Step main (c19da8d and c56ae14) this branch
X1: one click at (700, 700), Mail's empty area over News's third row News opens that story (reader_title "" → "The Guardian" / "axios.com") reader_title stays ""; Mail stays focused
Sign-in: Mail's Add account (578, 480) over News's second row, then the sheet's address and password (both over rows) and Sign in News opens the story ("MBI Deep Dives"), while Mail signs in News stays on its list; Mail signs in
Control: one click on the same row where Mail does not cover it, (75, 700) or (75, 480) n/a (the story is already open) News is raised and opens the story

Found on the way, not changed here

  • Hover still passes through a module tile. It claims presses, not moves, while a process tile's event.hits claims hovers too. So a hover over the empty part of a window can highlight a widget, or set its cursor, in the window behind. This comes from reading the code; I did not drive it.
  • The closed face hit-tests Restart in the wrong place. It uses the rect where the button was walked, before the face's turtle centred it: draw_stopped keeps cx.walk_turtle(…)'s rect. A host-only scratch test measured it: for a face at (100, 100) 400×500, restart_rect is at (100, 134.8), but the button is drawn at (240, 349.4). So a click on the visible Restart does not restart. The test above clicks Restart where the face hit-tests it (restart_button()), so it holds either way.

🤖 Generated with Claude Code

…w behind it

A click on Mail, cascaded over News, opened the News story under the
click (instrument scenario X1, bug B3). The desk offers a press to its
tiles topmost first, and Mail's tile claims it (`handled`) where none of
Mail's widgets took it. News's tile still handed the claimed press to its
app and counted on the claim to keep the app's widgets off it. The claim
does not do that: News's story rows are GestureViews, whose makepad hit
test is `hits_with_capture_overload(.., true)`, and that captures a press
another area has already claimed (touches it reads from the raw stream).
On release the row tapped and opened its story. List drags and a View's
`on_item_tap` co-capture the same way.

A module tile now lets an already claimed press go by, as it lets one
outside it go by. Something in front of the tile took it (a window over
it, or a shell surface), so the app never sees it. A process tile has had
this gate all along through `event.hits`. The closed face of a stopped
app takes presses the same way and claims the one it takes, so a click on
a window over its Restart no longer restarts it, and a click on the face
no longer raises the window behind it.

Tests (module_input_tests.rs): two cascaded module tiles, each press
offered front to back as the desk does: a click and a touch with a probe
app that takes presses as GestureView does, makepad's own GestureView's
capture, and the closed face.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ymote
ymote force-pushed the fix/wm-click-through branch from 6524358 to d030129 Compare October 2, 2026 17:52
@ymote

ymote commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

Local CI passed on d030129 (tools/ci-local.sh --only all --linux-host, 17m07s, Darwin arm64; the ubuntu jobs on the Linux build host, Linux x86_64).

GitHub would run for this PR: desktop.yml, phone.yml, apps.yml. Merged on this local pass (macOS runner queue); GitHub CI runs on the merge commit on main.

Job Step Result Time
ci-local / drift The local mapping fits the workflows PASS -
desktop.yml / desktop Prepare the pinned framework sources PASS 1s
desktop.yml / desktop Compile the desktop with the shared runtime PASS 2m03s
desktop.yml / desktop One copy of every shell source PASS -
desktop.yml / desktop Desktop tools and scripts PASS 8s
desktop.yml / desktop One Makepad, App Hub, octos and Rinx in the locked graph PASS 4s
desktop.yml / desktop The native apps' generated places match native-apps.json PASS 0s
phone.yml / home Prepare pinned sources and the reviewed runtime patch PASS 0s
phone.yml / home Compile Home and its bundled modules PASS 1m36s
phone.yml / home Verify the single runtime graph PASS 1s
phone.yml / home The native apps' generated places match native-apps.json PASS -
phone.yml / home The pinned octos revision PASS -
phone.yml / home Build octos (unless cached) PASS -
phone.yml / home Build Home, App Hub admission and runtime policy tests PASS 2m17s
phone.yml / home Test Home, App Hub admission and runtime policy PASS 7m04s
phone.yml / home Two-lane scenario (real kernel, scripted model) PASS 28s
apps.yml / services (linux) python3 tools/setup.py PASS 0s
apps.yml / services (linux) Kernel service PASS 11s
apps.yml / services (linux) App peers PASS 24s
apps.yml / services (linux) AI providers config PASS 4s
apps.yml / services (linux) L0 in-card chat (sys.chat, host side) PASS 9s
apps.yml / services (linux) System toolbox (workflow templates) PASS 1m31s
apps.yml / kernel-security (linux) python3 tools/setup.py PASS 1s
apps.yml / kernel-security (linux) The pinned octos revision PASS -
apps.yml / kernel-security (linux) Build octos (unless cached) PASS -
apps.yml / kernel-security (linux) Real-kernel tests PASS 32s
apps.yml / apps python3 tools/setup.py PASS 0s
apps.yml / apps Host services (Mail, AI providers, News) PASS 1m03s
apps.yml / apps Shell AI services (crates/ai-host) PASS 1m48s
apps.yml / apps AppCard runtime setup safeguards PASS 0s
apps.yml / apps AppCard clippy (own crates; shared runtime compiles) PASS 19s
apps.yml / apps AppCard transport and store PASS 12s
apps.yml / apps One Makepad, App Hub, octos and Rinx in the locked graph PASS 2s
rom.yml / product (linux) python3 -m unittest discover -s tests -v SKIPPED (!) -
rom.yml / product (linux) Check generated Agent Binder client SKIPPED (!) -
rom.yml / product (linux) bash -n scripts/stage-forks.sh scripts/publish-release.sh scripts/apply-to-tree.sh scripts/build-rom.sh PASS -
rom.yml / web-installer (linux) npm ci --ignore-scripts --no-audit --no-fund SKIPPED (!) -
rom.yml / web-installer (linux) npm test SKIPPED (!) -
rom.yml / web-installer (linux) npx playwright install --with-deps chromium SKIPPED (!) -
rom.yml / web-installer (linux) npm run test:browser SKIPPED (!) -
rom.yml / web-installer (linux) python3 -m unittest discover -s tests -p test_installer_manifest.py -v SKIPPED (!) -
rom.yml / web-installer (linux) bash -n scripts/release.sh SKIPPED (!) -
linux-host / sandbox (linux) python3 tools/setup.py PASS 0s
linux-host / sandbox (linux) Process sandbox on Linux (Landlock, seccomp) PASS 1m19s

Skipped (these did NOT pass; (!) marks a skip that blocks tools/ci-local-merge.sh):

  • rom.yml / product (linux): python3 -m unittest discover -s tests -v: javac -version does not work here: install a JDK (17 or newer) and put its bin/ on PATH or set JAVA_HOME
  • rom.yml / product (linux): Check generated Agent Binder client: needs build-tools/35.0.0/aidl, platforms/android-35/framework.aidl in the Android SDK (no Android SDK found): install the SDK packages 'build-tools;35.0.0' and 'platforms;android-35' (sdkmanager) and set ANDROID_HOME
  • rom.yml / web-installer (linux): npm ci --ignore-scripts --no-audit --no-fund: no node, npm, npx on PATH (the workflow uses node 24); put node on PATH
  • rom.yml / web-installer (linux): npm test: no node, npm, npx on PATH (the workflow uses node 24); put node on PATH
  • rom.yml / web-installer (linux): npx playwright install --with-deps chromium: no node, npm, npx on PATH (the workflow uses node 24); put node on PATH
  • rom.yml / web-installer (linux): npm run test:browser: no node, npm, npx on PATH (the workflow uses node 24); put node on PATH
  • rom.yml / web-installer (linux): python3 -m unittest discover -s tests -p test_installer_manifest.py -v: no node, npm, npx on PATH (the workflow uses node 24); put node on PATH
  • rom.yml / web-installer (linux): bash -n scripts/release.sh: no node, npm, npx on PATH (the workflow uses node 24); put node on PATH

Notes:

  • GitHub runs Python 3.12; this run used Python 3.9.6
  • GitHub runs Python 3.12; the Linux host used Python 3.14.4

@ymote
ymote merged commit cf526f4 into main Oct 2, 2026
5 checks passed
ymote added a commit that referenced this pull request Oct 2, 2026
…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 added a commit that referenced this pull request Oct 2, 2026
…ot at its top left

The closed face draws its line and Restart button centred, but it kept
the rect `walk_turtle` returned for the button as Restart's hit rect. The
face's turtle centres its content when it ends, which moves the drawn
button after it was walked, so the kept rect stayed at the face's top
left. A press on the visible button only raised the window, and a press
at the top left, where nothing is drawn, restarted the app.

The face now hit-tests Restart by the button's drawn area, as any widget
is hit-tested: clipped, at the time of the press. It keeps no rect of its
own.

Tests, in module_input_tests.rs: Restart responds where it is drawn (a
click and a tap on the visible button restart the app, a press at the top
left only raises the window), and #277's stopped-face test now presses the
drawn Restart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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