Code-reviewer flagged the clamp-before-render ordering nit; brainstormer ranked the layered (validate + clamp) approach over topology-hash / per-monitor pinning alternatives.
5.5 KiB
Code Review: Bubble Off-Screen Clamp Fix
Scope: Proposed bug-fix for v0.1.7 "widget enabled but not shown" — saved positions on disconnected monitor.
Files: src/bubble.rs (create, clamp_into_work_area, set_user_visible, default_position), src/app.rs (spawn_bubble, toggle_widget_visibility, reset_positions).
Overall Assessment
Fix is correct and minimal. Ship it with two small refinements. Root-cause matches code (verified: bubble.rs:143-160 passes saved position straight into CreateWindowExW; clamp_into_work_area at :770 only wired into WM_SETTINGCHANGE at :486). Approach is the right shape: clamp post-create, pre-show.
Critical Issues
None.
High Priority
- Call order — clamp must run BEFORE
render(hwnd)atbubble.rs:220, not just beforeShowWindowat:222.rendercallsGetWindowRect(:1108) for theUpdateLayeredWindowdestination point. If clamp runs afterrender, the first frame paints at the off-screen coords; second paint only happens on next update_data tick. Moveclamp_into_work_area(hwnd)to between line:218(state insert) and:220(render).
Medium Priority
-
MonitorFromWindowon a not-yet-shown off-screen window — verified safe. Win32 sets the window rect immediately atCreateWindowExWreturn (visibility is irrelevant toGetWindowRect). WithMONITOR_DEFAULTTONEARESTand a window whose entire rect lies on a disconnected monitor, the OS computes intersection with each currently attached monitor's rect; none intersect → falls back to nearest by Euclidean distance → returns the primary on a single-monitor setup. Saved[2407,1282]on a 1920-wide primary → nearest = primary → clamp pulls to(1920-w, …). Correct. -
Multi-monitor edge case is preserved. If the saved position is on a still-connected secondary,
MonitorFromWindowreturns that secondary monitor and clamps within its work area — no unwanted pull to primary. Good. -
Partial off-screen.
clamp_into_work_areaonly adjusts when fully outside (clamps each axis independently to[wa.left, wa.right-w]). A window whose top-left is on-screen but bottom-right spills off → it pulls the whole window inside. Behaviour is fine; matchessnap_to_edge(:644-645). -
DPI mismatch (saved from 4K → 1080p primary): the saved coords are physical pixels but the new bubble's
width_px/height_pxare recomputed against the current primary DPI (:140-142). Clamp uses the new size against the new monitor's work area — correct. No DPI bug. -
default_positioncase: no-op (already inside work area). Safe.
Low Priority
-
Log levels are appropriate.
info!increate(fires once per bubble creation),set_user_visible(fires only on user-toggle — verified atapp.rs:1152only called fromtoggle_widget_visibility), andtoggle_widget_visibility(one event per click). None on the render hot path. Approved. -
Alternative call site (clamp in
app::spawn_bubble): Less attractive.spawn_bubbledoesn't own the HWND lifecycle and would need a freshGetWindowRectround-trip. Keeping the clamp insidebubble::createkeeps the bubble module the sole owner of window geometry and means future call sites (e.g. tests, a hypothetical re-create-on-DPI-change) also benefit for free. The "bubble module stays position-agnostic" argument is weak — it already callsdefault_position,snap_to_edge, andclamp_into_work_area. Position-aware is the status quo.
Side Effects
- No callers of
bubble::createassert the returned HWND is at the exact requested coords.app::spawn_bubble(:277) ignores position post-create;reset_positions(:1156) destroys + recreates. Safe. position(hwnd)(:322) reads liveGetWindowRect, so any subsequenton_bubble_movedsave reflects the clamped coords — this self-heals the persisted bad value on first drag.
Positive Observations
- Clamp helper already exists and is correct (
:770-809). - Fix is one line + three log statements; minimal blast radius.
- Persisted-corruption auto-heal via first interaction is a nice property.
Recommended Actions
- MUST: Place
clamp_into_work_area(hwnd)betweenlock_bubbles().insert(...)(:218) andrender(hwnd)(:220) — not afterrender. - SHOULD: Add an
info!inclamp_into_work_areathat fires only whennx != r.left || ny != r.top(i.e. the actual reposition path). Free diagnostic for future "bubble moved itself" reports. - CONSIDER: Persist the clamped position immediately after
createsosettings.jsonis self-healed on next launch, not only after a drag. Trade-off: writes settings on every startup; current behaviour writes only on user action. Probably YAGNI — drift gets repaired on first interaction.
Unresolved Questions
- Should we also persist the corrected position eagerly (action 3)? Default to no per YAGNI; flag for user.
- Does Windows ever defer
CreateWindowExWwindow-rect commit untilShowWindow? Per MSDN and verified by existingsnap_to_edgeusing the same pattern inWM_EXITSIZEMOVE, no — rect is committed synchronously.
Status: DONE_WITH_CONCERNS
Summary: Fix is correct and small. One ordering bug: clamp must precede render, not just ShowWindow, otherwise the first paint targets the off-screen coords.
Concerns: Action 1 (clamp before render) is a real correctness issue — the proposal as written ("after CreateWindowExW succeeds and before ShowWindow") technically permits ordering after render, which would defeat the fix until the next data update.