experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

docs(click-through): findings and step-by-step plan for OS click-through

Investigation only — no behavior change. Concrete plan documented in
docs/click-through-investigation.md:

- Existing IPC tile:window:set-ignore-mouse is already wired and gated
by the window.manage capability; no new channel needed.
- Per-window mousemove + elementFromPoint + closest(whitelist) loop
toggles setIgnoreMouseEvents true with forward when over empty
canvas, false when over a real widget.
- Whitelists for page canvas and HUD are enumerated.
- FSM-state guard avoids breaking drags and resizes.
- Wayland documented as known fragile; no Electron-layer mitigation.
- Cmd panel intentionally out of scope (click-to-dismiss conflict).

+109
+109
docs/click-through-investigation.md
··· 1 + # Click-through investigation 2 + 3 + ## Current state 4 + 5 + ### `setIgnoreMouseEvents` call sites 6 + 7 + **Only two call sites exist in the working tree:** 8 + 9 + 1. `backend/electron/tile-ipc.ts:2866` — the IPC handler for `tile:window:set-ignore-mouse`. Registered at line 2845. Gated by `window.manage` capability (enforced in `backend/electron/tile-window-enforcement.ts:164`). 10 + 11 + 2. `features/spaces/background.js:76` — sole renderer-side caller. Sets the Spaces border decoration window to permanent click-through (`ignore=true`, `forward=true`). Not a canvas page host. 12 + 13 + **No call site in `app/`.** The page canvas (`app/page/page.js`), HUD (`app/hud/hud.js`), and any other overlay never call `setIgnoreMouseEvents`. There is no hit-test loop. Greenfield problem for every canvas window. 14 + 15 + ### Transparent `BrowserWindow` constructions 16 + 17 + `backend/electron/ipc.ts`: 18 + 19 + | Line | Window | `transparent` | `frame` | Click-through? | 20 + |------|--------|--------------|---------|----------------| 21 + | 750 | Canvas page host (`useCanvas=true`, `http/https`/`peek://app/new-tab`) | `true` | `false` | No | 22 + | 1328 | Webview popup | `true` | `false` | No | 23 + 24 + `app/hud/background.js:88-95` — HUD window: `transparent: true`, `frame: false`, `alwaysOnTop: true`, `focusable: false`. `setIgnoreMouseEvents` never called. 25 + 26 + ### Candidate windows 27 + 28 + 1. **Canvas page host** — fullscreen transparent BrowserWindow. Gutters, trigger zone, drag overlay, resize handles eat clicks even when visually empty. Webview consumes its own clicks. 29 + 2. **HUD overlay** — area outside `#hud-container` is empty canvas eating clicks. 30 + 3. **Cmd panel** — transparent but click-to-dismiss. **Out of scope** — click-through breaks dismiss. 31 + 4. **Windows view / peek-card** — no dedicated overlay window; IZUI hides/shows existing windows. `peek-card` is a custom element inside other canvases. Nothing new to wire. 32 + 5. **Page widgets** — DOM elements inside the canvas with `pointer-events: auto`; the surrounding canvas window itself still eats clicks. 33 + 34 + ### Renderer-side hit-testing 35 + 36 + Doesn't exist. No `elementFromPoint` call, no `mousemove`-driven `setIgnoreMouseEvents` toggle anywhere in `app/`. 37 + 38 + ## What we want (UX-level) 39 + 40 + Clicks in visually-empty regions of overlay/canvas windows reach the OS app behind Peek. Specifically: gutters and margins of the page canvas, area outside `#hud-container` in the HUD, and any page-widget-absent region of a canvas page host. 41 + 42 + ## Approach 43 + 44 + ### IPC channel 45 + 46 + `tile:window:set-ignore-mouse` already exists (`backend/electron/tile-ipc.ts:2845`) and supports `{ forward: true }`. `api.window.setIgnoreMouseEvents(id, ignore, { forward: true })` is already in `backend/electron/tile-api.d.ts:172`. No new IPC. 47 + 48 + ### Per-window hit-test loop 49 + 50 + Each transparent canvas renderer adds a `mousemove` listener: 51 + 52 + 1. `document.elementFromPoint(e.clientX, e.clientY)`. 53 + 2. Check `el.closest('<whitelist>')`. 54 + 3. Empty canvas → `api.window.setIgnoreMouseEvents(undefined, true, { forward: true })`. 55 + 4. Real widget → `api.window.setIgnoreMouseEvents(undefined, false)`. 56 + 57 + `forward: true` keeps `mousemove` flowing while ignoring clicks, which keeps the hit-test loop alive on the next frame. Track `lastIgnore` and only send on transitions — IPC is async but fire-and-forget. 58 + 59 + ### Selector whitelists 60 + 61 + **Page canvas (`app/page/page.js`):** 62 + 63 + ``` 64 + webview, peek-navbar.visible, .resize-handle, .trigger-zone, 65 + .widget.visible, .page-info-panel.visible, .tags-panel.visible, 66 + .entities-panel.visible, .notes-panel.visible, .extensions-panel.visible, 67 + .find-bar.visible, .drag-overlay.active 68 + ``` 69 + 70 + **HUD (`app/hud/hud.js`):** 71 + 72 + ``` 73 + #hud-container 74 + ``` 75 + 76 + The hit-test does not have to handle visibility class details perfectly; if the renderer keeps an in-memory `inDragging() || inResizing()` flag (already maintained in `page.js` as FSM states `DRAGGING`, `DRAGGING_OUT_OF_MAXIMIZED`, `RESIZING`), it should hold `ignore=false` while any of those are active to avoid breaking pointer capture mid-drag. 77 + 78 + ## Files to touch 79 + 80 + | File | Change | 81 + |------|--------| 82 + | `app/page/page.js` | Add `mousemove` hit-test loop after DOM ready; call `api.window.setIgnoreMouseEvents` | 83 + | `app/hud/hud.js` | Same pattern with `#hud-container` whitelist | 84 + | `backend/electron/tile-ipc.ts` | No change — handler exists | 85 + | `backend/electron/tile-api.d.ts` | No change — surface declared | 86 + 87 + ## Cross-platform notes 88 + 89 + - **macOS**: fully supported. `forward: true` is required; without it `mousemove` stops delivering to the renderer and the hit-test loop goes blind. 90 + - **Linux X11**: works. X11 compositors honour the `_NET_WM_STATE` hints Electron sets. 91 + - **Linux Wayland**: fragile. Many compositors (sway, some gnome-shell configurations) do not honour the input-region protocol Electron uses. Expect click-through to silently not work. No Electron-layer mitigation; document as known limitation. 92 + - **Windows**: works when `transparent: true` is set on the BrowserWindow. All canvas page hosts already do; no extra work. 93 + 94 + ## Risks and edge cases 95 + 96 + 1. **Drag operations**: starting a drag in a real widget then moving cursor into an empty region would fire `setIgnoreMouseEvents(true)` mid-drag and break pointer capture. Guard with the FSM state — while `inDragging() || inResizing()` (page.js), hold `ignore=false`. 97 + 2. **Trigger zone**: must remain a real-widget region even when visually empty, so hovering there still reveals the navbar. Include `.trigger-zone` unconditionally. 98 + 3. **Webview focus**: `setIgnoreMouseEvents(false)` when over the webview is correct — guest process handles the click, host renderer just doesn't discard. 99 + 4. **DevTools**: may interfere if DevTools is docked. Canvas page hosts use detach mode by default; low risk, but verify. 100 + 5. **IPC latency**: `setIgnoreMouseEvents` is async. There may be a 1–2 frame gap between entering an empty region and click-through activating. Acceptable. 101 + 6. **`forward: true` on Windows**: requires `transparent: true`. Already set everywhere we'd touch. 102 + 103 + ## Out of scope 104 + 105 + - **Cmd panel**: transparent but click-to-dismiss; click-through would break it. 106 + - **Settings, diagnostic, datastore-viewer**: not overlay windows; backgrounds aren't meaningfully transparent. 107 + - **Windows view**: no separate overlay window in this codebase. 108 + - **Wayland fix**: no Electron-level workaround exists today. 109 + - **`-webkit-app-region` on HUD**: existing macOS drag-region CSS isn't a substitute; both can coexist.