Skip to content

Add a watchdog that keeps the notch in the topmost z-order band - #322

Merged
vinzdg merged 1 commit into
vinzdg:mainfrom
abrahaofv:always-on-top-watchdog
Sep 24, 2026
Merged

vinzdg merged 1 commit into
vinzdg:mainfrom
abrahaofv:always-on-top-watchdog

Conversation

@abrahaofv

Copy link
Copy Markdown
Contributor

Summary

Implements the fix already agreed on #304 between @PedroMendes22 (who diagnosed and measured this) and @RawJat (who confirmed the cause and the click-through interaction) — not a new direction of my own.

Root cause, as measured on the issue: tao (Tauri 2's windowing crate) only calls SetWindowPos when its own ALWAYS_ON_TOP flag changes value. tauri.conf.json sets alwaysOnTop: true at window creation, so that flag never changes again — any later window.set_always_on_top(true) is a no-op that never reasserts anything with the OS. WS_EX_TOPMOST can stay set on the notch while the real z-order has sunk it behind ordinary windows anyway (60/60 samples behind Chrome on the issue, despite the bit being set).

Two spots were already identified as affected:

  • main.rs's set_click_through, called on every pointer enter/exit over the notch, triggers tao's style rewrite with SWP_NOZORDER — explicitly not reordering.
  • dropzones.rs's drag-carry lift (notch.set_always_on_top(true), meant to raise the notch back over the drop-zone overlay) was the same no-op.

What this does

  • New topmost.rs: is_out_of_topmost_band unifies the two failure modes the issue describes into one check — the notch's own WS_EX_TOPMOST bit cleared outright, or the bit set but a visible ordinary window found ahead of it in a top-to-bottom EnumWindows walk. reassert does the direct SetWindowPos(HWND_TOPMOST, ...) that bypasses tao's diff. start_watchdog polls every 2s — the cadence @PedroMendes22 already validated ("logged 3 assertions" over a session, not one per tick) — modelled on the existing start_work_area_watch (same bare-thread-plus-sleep shape, same DRAGGING skip-guard so it doesn't fight the carry).
  • dropzones.rs: routes the drag-carry lift through the same topmost::reassert helper instead of the no-op, per @RawJat's specific ask on the issue.
  • main.rs: wires topmost::start_watchdog alongside the other two watchdogs; DRAGGING needed to go from private to pub(crate) for topmost.rs to read it.

One implementation detail not spelled out in the issue: window.hwnd() on a tauri::WebviewWindow comes back typed against whatever windows crate version Tauri itself pulled in (0.61.2 here), which differs from the one this crate depends on directly (0.58, pinned in Cargo.toml). Same workaround notchmenu.rs's give_back already uses: the raw pointer is carried across as an integer and rebuilt into this crate's own HWND before touching any Win32 call.

Test plan

  • cargo build --release and cargo test --locked — 131 passed, 0 failed, no regressions
  • cargo clippy --release — 6 pre-existing warnings elsewhere (tray.rs, glm.rs), none in the touched files
  • Forced the bug deterministically, the way the issue describes ("another process calling SetWindowPos(notch, HWND_NOTOPMOST, …)"): a throwaway script called SetWindowPos(HWND_NOTOPMOST) on the running dev build's notch window. Confirmed WS_EX_TOPMOST went from set to cleared.
  • Within the poll window, run.log printed "topmost watchdog: notch had left the topmost band, reasserted", and a follow-up check confirmed WS_EX_TOPMOST was set again — no restart needed. Visually confirmed with another window brought to front over the notch: stayed on top.
  • Drag-carry: pulled the notch by the move handle across edges: it stayed drawn above the drop-zone overlay for the whole carry.

🤖 Generated with Claude Code

@vinzdg

vinzdg commented Sep 23, 2026

Copy link
Copy Markdown
Owner

Thanks @abrahaofv. The idea is sound — a notch that loses its z-order band is a real bug — but the implementation needs two changes.

Blocker: it logs forever when the notch is hidden. topmost.rs:44,62 — when the notch is switched off via notch_visible, the window is never found, so the watchdog reasserts and logs every 2 seconds indefinitely. run.log is capped at 1 MB, so that rotates away everything useful; the first thing anyone attaches to a bug report would be nothing but this. Gate it on the notch actually being shown.

Should-fix: it shouldn't be a poller at all. TOPMOST_POLL_MS = 2000 is about 43,000 unconditional wake-ups a day, gated only by DRAGGING, sitting beside the existing 1-second work-area watch. The CPU cost is negligible (~0.01%) and that isn't really the point — it's a timer that exists because nothing was subscribed to.

The comment at :101-102 says there's nothing to subscribe to, and that isn't so: SetWinEventHook(EVENT_SYSTEM_FOREGROUND) delivers precisely this event, at zero idle cost. That's the shape I'd want — react when the foreground window changes, rather than ask every two seconds whether it did.

If the hook turns out to miss a case in practice, a slow backstop poll (30s+) behind the same visibility gate would be a fair compromise — but let's try the hook first.

Thanks for #318 and #319, both merged.

Replaces the 2-second watchdog poll with EVENT_SYSTEM_FOREGROUND on a dedicated message-loop thread. A shared visible-window and drag gate keeps hidden notches completely silent, and a 30-second backstop covers a topmost window that appears without becoming foreground.

The watchdog records one line per excursion and serializes the foreground callback with the backstop so overlapping checks cannot duplicate that diagnosis.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Co-authored-by: Codex <322868195+abrahaofvcodex@users.noreply.github.com>
@abrahaofv
abrahaofv force-pushed the always-on-top-watchdog branch from bd8d66a to fcd3f11 Compare September 23, 2026 03:45
@abrahaofv

Copy link
Copy Markdown
Contributor Author

Addressed both review points in fcd3f11.

The watchdog now returns before any z-order enumeration when the notch is hidden, so notch_visible = false produces no watchdog logging. It also records only the transition out of the band, and serializes the foreground callback with the backstop so one excursion cannot produce duplicate diagnostics.

The 2-second poll is replaced by an EVENT_SYSTEM_FOREGROUND hook on its own message-loop thread. I retained a 30-second backstop only for the case a different application creates or changes a topmost window without receiving foreground; both paths use the same visibility and drag gates. If you would prefer the initial revision to be hook-only, I am happy to remove that backstop.

Validated with the notch hidden for over five minutes, repeated foreground changes, forced loss/recovery of WS_EX_TOPMOST, and drag/carry across positions.

@abrahaofv

abrahaofv commented Sep 23, 2026 via email

Copy link
Copy Markdown
Contributor Author

@vinzdg
vinzdg merged commit 134ce37 into vinzdg:main Sep 24, 2026
4 checks passed
@vinzdg

vinzdg commented Sep 24, 2026

Copy link
Copy Markdown
Owner

Merged — thanks for turning this round so quickly. Everything I asked for is in, and I checked each one rather than taking it on trust.

EVENT_SYSTEM_FOREGROUND with a 30-second backstop, and skipped entirely while the notch is hidden — so the log-spam case is gone and the idle cost is effectively nothing. That's better than the compromise I offered.

All four CI workflows green.

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.

2 participants