Skip to content

Let the tray panel be moved and resized again - #758

Merged
Finesssee merged 2 commits into
nesszer:mainfrom
yozu:fix/tray-panel-move-resize
Oct 6, 2026
Merged

Finesssee merged 2 commits into
nesszer:mainfrom
yozu:fix/tray-panel-move-resize

Conversation

@yozu

@yozu yozu commented Oct 6, 2026 •

Copy link
Copy Markdown

Summary

Since 0.70.0 (#712) the tray-panel flyout is the only dashboard, and it can no longer be moved or (in practice) resized:

  • No way to move it. The retired PopOut window was draggable; the flyout re-anchors to the tray on every open and has no drag region.
  • Resizing from the frame closes it. Tauri covers the outer ~4 px of an undecorated resizable window with its own native resize border (TAURI_DRAG_RESIZE_BORDERS, tauri-runtime-wry/src/undecorated_resizing.rs). Pressing it starts a native size loop, Windows moves focus off the WebView, and the flyout's blur-dismiss hides the panel instantly. Only the hidden 10 px top/left grips worked, because they arm the gesture guard first.

This PR makes the panel movable and resizable again:

  • Move strip along the top (small grab bar, startDragging). The spot is recorded when the Win32 move/size loop ends (WM_EXITSIZEMOVE). Windows only runs that loop for user drags, never for SetWindowPos, so the flyout's own re-anchoring is never mistaken for a move. WM_SIZING tells a resize from a move; comparing bounds can't, because a move onto a monitor with another DPI rescales the window. After that, opens and content-driven resizes keep the panel at the spot, clamped into the work area it overlaps most. Double-clicking the strip forgets the spot and anchors the panel to the tray again (reset_flyout_position).
  • Resize grips on every edge and corner, since a moved panel can face open screen on any side.
  • Blur while pressing on the panel doesn't dismiss it. In Focused(false), the backend checks whether a mouse button is held and the window under the cursor belongs to the flyout. That covers native frame resizes and the strip/grips. The strip and grips therefore no longer arm the 15 s gesture guard, which also swallowed the next real outside click after a resize. A click on another topmost window covering the panel still dismisses it.
  • Mixed-DPI setups: the remembered size is now stored in logical px under a new key (flyoutLogical). The old physical-px flyout size only fit the monitor it was taken on. It is ignored, so users who resized fall back to the legacy trayPanel size or auto-fit once, following the precedent of geometry_store::migrate dropping physical sizes. A DPI rescale is no longer treated as a user resize, which would have frozen an auto-fit panel. A placed panel is re-clamped once a rescale has been applied.
  • The flyout is built with maximizable(false), so dragging it to a screen edge doesn't snap or maximize it.

Related issue

No issue filed; reported directly by a user on 0.70.0.

Affected areas

  • Tray panel
  • Settings UI
  • Config file / settings persistence (window_geometry.json: new entries.flyout position and size_entries.flyoutLogical)
  • CLI
  • Provider-specific behavior
  • Installer / release packaging
  • Startup / background behavior
  • Documentation (docs/ARCHITECTURE.md, CHANGELOG.md)
  • Other: new locale key TrayMoveHandleHint (all 9 locales)

Validation

  • cargo fmt --all --check
  • cargo clippy --manifest-path apps/desktop-tauri/src-tauri/Cargo.toml --all-targets -- -D warnings: clean
  • cargo test --manifest-path apps/desktop-tauri/src-tauri/Cargo.toml: 633 passed (new shell::flyout_placement tests cover move vs resize, the cross-DPI move, and work-area choice)
  • cargo test --manifest-path rust/Cargo.toml: 3691 passed, 1 failed (providers::codex::api::tests::external_oauth_gate_fails_closed_without_last_refresh). It reads the real user settings, and this machine has codex_external_oauth_sources_allowed = true. Unrelated to this diff, which doesn't touch rust/src/providers.
  • pnpm test: 666 passed, 1 failed (QuotaBurndownChart expects Sep; this machine's OS locale is ja-JP). The full-suite runs also show the existing intermittent TrayPanel.test.tsx failures. Upstream main shows them too (density tests): a component's unmount cleanup runs after the describe-level vi.restoreAllMocks(), and the error is attributed to the next test. The new tests unmount explicitly so they don't add to it. A mutation check confirmed the new DPI-rescale test fails without the fix.
  • pnpm run lint, pnpm run check-locale (994 keys match), tsc --noEmit, pnpm run check:anti-slop
  • scripts\local-check.ps1: not run as a whole; the steps above cover the Rust and frontend parts of the slice
  • Thermo-nuclear review: not run with that skill; the diff got an independent code review pass, and its findings are addressed in the second commit

UI / tray proof

  • Not applicable
  • CUA Driver visual proof attached
  • CUA Driver could not be used; equivalent manual proof and explanation attached

The CUA drag tool posts window messages, so it never enters the native move/size loop these fixes depend on. Instead I drove the real cursor from a per-monitor-DPI-aware PowerShell script (SetCursorPos + mouse_event) against fresh tauri:build:debug builds, and read the window rect with GetWindowRect before and after. Setup: Windows 11, primary 2560×1440 @100% with a bottom taskbar, second monitor above it at 3840×2560 @225%.

Check Upstream main This PR
Drag right frame edge outward 80 px panel hides (visible=False) 300→380 wide, stays open
Drag bottom frame edge panel hides 873→813 tall, stays open
Bottom-right grip n/a 380×813 → 340×773
Drag top strip by (−700, −300) n/a moved exactly; entries.flyout = new top-left
Focus another app, reopen n/a hidden on blur; reopens at the saved spot
Double-click strip n/a back at tray anchor; entries.flyout removed
Move onto the 225% monitor, restart the app n/a opens there at 873×2295 physical (= saved 388×1020 logical), inside that work area
Drag strip to the screen's left edge n/a no snap, size unchanged
Focus another app right after a drag n/a hides immediately (no 15 s guard)

Notes for reviewers

  • shell/flyout_placement.rs installs a second comctl32 subclass on the flyout HWND. It goes through run_on_main_thread, because SetWindowSubclass fails across threads. Side note: the existing dwm::force_dark_caption_resizable call for the flyout runs off-thread too, and the log shows dwm: subclass installed=0 for it. I left that alone.
  • Windows applies the DPI change of a window dragged onto another monitor lazily: often not during the drag, but on the next user move or the next programmatic placement. Logical size storage plus the post-rescale re-clamp make that converge. Expect the panel to render at the old scale until then.
  • stored_position() reads window_geometry.json on each reanchor, the same file-read pattern the size store already uses. Happy to cache it if you prefer.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • The tray panel can now be moved by dragging its top handle, and its position is remembered. Double-click the handle to return the panel beside the tray.
    • Resize the panel from any edge or corner. Its size is remembered across display scaling changes.
  • Bug Fixes
    • Starting a move or resize no longer causes the panel to close unexpectedly. Panel placement stays within the available screen area.

yozu and others added 2 commits October 6, 2026 12:39
Since 0.70.0 the tray-panel flyout is the only dashboard and it could not be
moved. Resizing from its outer frame (Tauri's built-in undecorated resize
border) blurred the WebView before the native size loop started, so the
blur-dismiss handler hid the panel; only the hidden 10px top/left grips worked.

- Add a move strip along the top (startDragging). The spot the user drags the
  panel to is recorded when the Win32 move/size loop ends (WM_EXITSIZEMOVE,
  with WM_SIZING telling resizes from moves) and every later open or
  content-driven resize keeps it there, clamped into the monitor's work area.
  Double-clicking the strip anchors the panel to the tray again.
- Add resize grips on every edge and corner.
- Keep the panel open on blur while a mouse button is held with the cursor
  inside it, which covers native frame moves/resizes.
- Re-clamp a placed panel after a DPI change rescales it.
- Store the remembered panel size in logical px (new key) so it fits
  monitors with different scaling; the old physical-px size is ignored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Treat a DPI rescale as programmatic so moving an auto-fit panel onto a
  monitor with other scaling doesn't freeze it at a fixed size.
- Build the flyout without a maximize box so dragging it to a screen edge
  can't snap or maximize it.
- Pick the monitor for a remembered spot by the panel's overlap with each
  work area (nearest one when it overlaps none) instead of its corner.
- Re-clamp a placed panel on the Resized that follows a DPI change, when the
  new size is in place, and never reposition it during a user drag.
- Check the window under the cursor rather than the panel's bounds, so a
  click on another topmost window covering the panel still dismisses it.
- Stop arming the 15s gesture blur guard from the move strip and grips; the
  held-button check covers them and the guard swallowed the next real
  outside click.
- Re-anchor after every settled user resize, react to primary presses only
  on the grips, and remove the subclass on WM_NCDESTROY.

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

coderabbitai Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: nesszer/Win-CodexBar/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 516e2198-4aa1-45b9-9d79-064affbffd07
📥 Commits

Reviewing files that changed from the base of the PR and between 5d978a0 and f1b99be.

📒 Files selected for processing (26)
  • CHANGELOG.md
  • apps/desktop-tauri/src-tauri/src/commands/surface.rs
  • apps/desktop-tauri/src-tauri/src/geometry_store.rs
  • apps/desktop-tauri/src-tauri/src/main.rs
  • apps/desktop-tauri/src-tauri/src/shell/activation.rs
  • apps/desktop-tauri/src-tauri/src/shell/flyout_placement.rs
  • apps/desktop-tauri/src-tauri/src/shell/flyout_window.rs
  • apps/desktop-tauri/src-tauri/src/shell/mod.rs
  • apps/desktop-tauri/src/hooks/useTrayPanelController.ts
  • apps/desktop-tauri/src/hooks/useTrayPanelLayout.ts
  • apps/desktop-tauri/src/i18n/keys.ts
  • apps/desktop-tauri/src/lib/tauri.ts
  • apps/desktop-tauri/src/styles.css
  • apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx
  • apps/desktop-tauri/src/surfaces/TrayPanel.tsx
  • docs/ARCHITECTURE.md
  • rust/src/locale.rs
  • rust/src/locale/en-US.ftl
  • rust/src/locale/es-MX.ftl
  • rust/src/locale/ja-JP.ftl
  • rust/src/locale/ko-KR.ftl
  • rust/src/locale/pt-BR.ftl
  • rust/src/locale/ru-RU.ftl
  • rust/src/locale/tr-TR.ftl
  • rust/src/locale/zh-CN.ftl
  • rust/src/locale/zh-TW.ftl

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review.


📝 Walkthrough

Walkthrough

The tray panel now supports dragging, resizing from all edges and corners, and remembering its position and size. Saved sizes use logical pixels. Users can double-click the move strip to reset the panel beside the tray.

Changes

Tray panel geometry

Layer / File(s) Summary
Store and track user placement
apps/desktop-tauri/src-tauri/src/shell/flyout_placement.rs, apps/desktop-tauri/src-tauri/src/shell/activation.rs, apps/desktop-tauri/src-tauri/src/geometry_store.rs, apps/desktop-tauri/src-tauri/src/shell/mod.rs
Windows move and resize loops record eligible panel positions. Saved positions are clamped to a work area, and position entries can be removed.
Apply placement and logical sizing
apps/desktop-tauri/src-tauri/src/shell/flyout_window.rs, apps/desktop-tauri/src-tauri/src/commands/surface.rs, apps/desktop-tauri/src-tauri/src/main.rs, apps/desktop-tauri/src/lib/tauri.ts, apps/desktop-tauri/src/hooks/useTrayPanelController.ts, apps/desktop-tauri/src/hooks/useTrayPanelLayout.ts, apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx
The flyout uses saved placement when available, avoids repositioning during active moves, and reclamps after DPI changes. A command resets placement. Frontend size persistence and restoration use logical pixels.
Expose move and resize controls
apps/desktop-tauri/src/surfaces/TrayPanel.tsx, apps/desktop-tauri/src/styles.css, apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx, apps/desktop-tauri/src/i18n/keys.ts, rust/src/locale.rs, rust/src/locale/*.ftl, docs/ARCHITECTURE.md, CHANGELOG.md
The panel provides a move strip and resize grips on all edges and corners. Tests cover the controls. Locale strings and documentation describe the interactions.

Priority: ⬇️ Low

Estimated code review effort: 4 (Complex) | ~60 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant TrayPanel
  participant TauriWindow
  participant flyout_placement
  participant geometry_store
  User->>TrayPanel: Drag the move strip
  TrayPanel->>TauriWindow: Start native window drag
  TauriWindow->>flyout_placement: Report end of move/size loop
  flyout_placement->>geometry_store: Save eligible position
  User->>TrayPanel: Double-click the move strip
  TrayPanel->>geometry_store: Reset saved position through Tauri command
Loading

Suggested reviewers: finesssee

Merge Risk: ⚪ Minimal · up to f1b99

No actionable issue is established that should block merging. Native move and reset interactions still warrant normal Windows validation.

Security Architecture Review

Security architecture risk: 🔵 Low · up to f1b99

The reviewed effects are limited to panel placement, sizing, and remembered window geometry. No increased access to sensitive data or privileges was identified. Interrupted or overlapping window actions still need runtime validation.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The inspected entrypoints affect the local flyout and a shared file of window geometry. They do not accept arbitrary filesystem paths, resource identities, or window targets. Interrupted whole-file writes could affect other remembered window geometries, but the inspected file contains geometry rather than credentials or security policy.

Security Findings and Attack Paths

  • inferred — A compromised application renderer could invoke the new reset command without a caller-label check and reset flyout placement. This adds a bounded geometry operation, not demonstrated privilege escalation: the base already exposed flyout dismissal, size changes, and reanchoring without equivalent caller checks. No external attacker reachability was established.

Trust Boundaries and Controls

  • observed — Native flyout handling rejects events for other window labels. The default capability still names main, settings, floatbar, and flyout, with unchanged dragging and resizing permissions. The new reset command resolves a fixed flyout target but does not validate the invoking window; native event identity checks must not be mistaken for IPC caller authorization.
🚥 Pre-merge checks | ✅ 7 | ❓ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Ui Changes Include Windows Proof ❓ Inconclusive The PR changes visible tray-panel UI. The diff adds a move handle and resize grips in apps/desktop-tauri/src/styles.css and updates TrayPanel.tsx to render them. The supplied description says manu… Provide the full PR description, including any omitted screenshots or fresh-Windows-build proof note. Then verify whether it satisfies the custom check.
✅ Passed checks (7 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is a concise imperative summary that clearly describes restoring tray-panel movement and resizing.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Provider Data Stays Siloed ✅ Passed The PR changes tray-panel positioning, sizing, UI, and locale files. No files under rust/src/providers/ or rust/src/core/provider_factory.rs changed. The added diff contains no provider-ID branchi…
Secrets Handled Safely ✅ Passed The PR adds no token, cookie, API key, OAuth credential, or authorization-header handling. New logs report geometry persistence or window move/resize errors; the new reanchor diagnostic contains only …
No Unapproved Dependencies ✅ Passed The reviewed diff changes no Cargo.toml, package.json, or npm/yarn/pnpm lockfile. It also contains no packageManager version change. The dependency check passes.
Full details: Ui Changes Include Windows Proof

Explanation

The PR changes visible tray-panel UI. The diff adds a move handle and resize grips in apps/desktop-tauri/src/styles.css and updates TrayPanel.tsx to render them. The supplied description says manual Windows testing replaced CUA visual proof, but it is marked truncated. The complete description was not available in the checkout, so I cannot determine whether omitted text contains screenshots or a proof note from a fresh Windows build.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Finesssee

Copy link
Copy Markdown
Collaborator

Maintainer validation at head f1b99bed (already based on main 5d978a05).

Local CI (scripts/local-check.ps1 -Slice ci steps, Rust 1.98.0): all 11 steps pass. That covers fmt, clippy -D warnings, 3692 + 633 Rust tests, oxlint, anti-slop, frontend tests, type check/build and the interaction-guard tests. Hosted CircleCI doesn't run for fork PRs here.

UI proof: a fresh debug build of this head, driven over WebView2 CDP with a synthetic Codex-only profile (Hide Personal Info on), on a two-monitor setup. 19 of 19 checks pass.

Scenario Checks
Anchored, no stored spot Move strip at the top with grab bar and the "Drag to move. Double-click to put it back by the tray." hint; resize grips on all 4 edges and 4 corners; a programmatic SetWindowPos move is not stored as a placed position
Remembered spot on the second monitor Opens exactly at the stored entries.flyout (-3000,-900), inside that monitor's work area
Stored spot now off-screen Clamped back into the nearest monitor's work area
Double-click the move strip Re-anchors to the default spot and removes entries.flyout from window_geometry.json
Old physical size entry (2000x2000) plus flyoutLogical 360x640 Logical size applied (outer 360x640 at 100%); the old physical entry is ignored
Privacy No email, account or profile-path text in the flyout

Not re-proven here: a real mouse drag or edge resize (recording on WM_EXITSIZEMOVE, not dismissing during a frame resize), because that needs the real cursor. That relies on your real-cursor proof table and the unit tests.

Note for later, not caused by this PR: with no tray anchor (as in proof mode), re-anchoring falls back to the window's current monitor. Main has the same fallback chain.

@Finesssee
Finesssee merged commit 1b4dbcd into nesszer:main Oct 6, 2026
2 checks passed
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