Skip to content

feat(desktop-cards): pin cards to the Windows desktop - #3527

Merged
bobleer merged 9 commits into
GCWing:mainfrom
Raing5Days:feat/desktop-card-pinning
Nov 2, 2026
Merged

bobleer merged 9 commits into
GCWing:mainfrom
Raing5Days:feat/desktop-card-pinning

Conversation

@Raing5Days

Copy link
Copy Markdown
Contributor

What

Desktop cards can now be pinned to the Windows desktop: the card window is attached to the Explorer desktop layer that hosts SHELLDLL_DefView, so a pinned card sits above the desktop icons and behind other applications. This is a second display mode next to global always-on-top (they are mutually exclusive).

Pinning is available from three places, and hidden cards can be pinned too — their preference is stored and applied the next time the card is shown:

  • the card menu (Pin to desktop / Always on top (all apps))
  • the Home "Desktop cards" list, which also shows the applied mode
  • Settings → Desktop cards, one pin switch per card

How it works

Native layer (src/apps/desktop/src/desktop_cards/desktop_layer.rs, Windows only)

  • Finds the desktop host by locating SHELLDLL_DefView, then reparents the card with SetParent + WS_CHILD, keeping the original style and parent in a window property so detach is lossless.
  • Refuses to attach when the card and the desktop host have different DPI awareness contexts: cross-process SetParent with mismatched DPI can forcibly reset the DPI awareness of every OpenBitFun window. Explorer missing, a rejected SetParent, or a failed style change produce an explicit error and a rollback instead of a half-attached window.
  • Moved events persist GetWindowRect screen coordinates, because an attached window reports parent-client coordinates; otherwise detach and multi-monitor restore would jump.
  • The card window is destroyed if attaching fails right after creation, so a failed pin never leaves an invisible window behind.

Product layer

  • set_stacking applies the native change before persisting, rolls back on failure, and reports the host error (the card menu shows it via the new desktopCard.stackingFailed copy).
  • Picking always-on-top detaches a pinned card, and pinning clears always-on-top, so the two modes cannot drift apart.
  • set_visible(true) re-attaches a pinned card; bring_to_front is skipped for pinned cards so they are not promoted into the global topmost band.

Capability negotiation / compatibility

  • desktop_card_set_always_on_top keeps its name; desktopPinned is a new optional field, so an older client still sends the original payload and keeps working.
  • desktop_card_get and desktop_card_list advertise desktopPinningSupported as a runtime fact (never persisted). Older hosts omit it, and the UI then hides the pin action instead of failing.
  • desktopPinned lives in the record's forward-compatible extension map, so a rewrite by an older build keeps it (covered by a store test).

Capability catalog

  • New desktop-pinning item in src/shared/interactive-capabilities/catalog.json (classified as open / unstructuredInteraction, since the DesktopCard tool deliberately does not carry this display preference), plus regenerated docs, search, and product-control artifacts.

Verification (local)

  • cargo test --locked -p openbitfun-desktop --no-default-features --lib desktop_cards → 17 passed, 1 ignored
  • ... live_explorer_attachment_round_trip -- --ignored → passed against a real Explorer session (hidden disposable windows; Explorer itself is never modified)
  • pnpm --dir src/web-ui run test:run src/app/components/DesktopCard/DesktopCardWindow.test.tsx src/infrastructure/api/service-api/DesktopCardAPI.test.ts src/app/scenes/personal-overview/OverviewDesktopCardsSection.test.tsx src/app/scenes/settings/pages/application/DesktopCardsSettingsSection.test.tsx → 34 passed
  • pnpm --dir src/web-ui run type-check, pnpm run i18n:audit, pnpm run capabilities:check, pnpm run fmt:rs → clean

Deferred to CI and to a manual Windows pass (not reproducible headlessly): dragging/resizing a pinned card, Win+D, Explorer restart, mixed-DPI monitor moves, and hide/show switching between both modes.

Remote scenarios

Desktop cards remain LocalOnly / ControllerLocal in the Product Operation Registry; no command was added and the registry table is unchanged. Remote control, peer device, remote workspace, and detached dispatch are unaffected, and the management surfaces keep the pin action hidden when the host cannot advertise it.

Upgrade compatibility

Additive request field, additive runtime capability in get/list payloads, and an additive persisted extra field. No migration, no data removal, and older clients/hosts stay on their working path.

Raing5Days and others added 4 commits October 23, 2026 00:00
Attach a card window to the Explorer desktop layer that hosts
SHELLDLL_DefView, so a pinned card sits above the icons and behind other
applications. Attachment keeps a DPI-context guard, saves and restores the
original parent/style, rolls back on failure, and persists screen
coordinates so detaching and multi-monitor restore do not jump.

Desktop pinning and global always-on-top stay mutually exclusive: the
stacking request applies before persistence, reports a host refusal instead
of a fake success, and the card menu negotiates the advertised capability
before extending the legacy always-on-top payload.
Hidden cards have no window, so expose desktop pinning everywhere cards are
listed: the Home card list shows the applied mode and toggles it, and
Settings adds one pin switch per card. The host advertises
desktopPinningSupported in the card list, so those surfaces render the
action without reading every card first, and an older host simply keeps it
hidden. A refused change is reported and never flips the switch.

Document desktop pinning in the interactive capability catalog and
regenerate the docs, search, and product-control artifacts.
The capability contract check requires an unstructuredInteraction reason to
quote its own item title, so a reviewer can match the reason to the
capability without cross-referencing. Quote the title in both languages and
regenerate the docs, search, and product-control artifacts.
# Conflicts:
#	docs/interactive-capabilities/README.md
#	docs/interactive-capabilities/capabilities.json
#	docs/interactive-capabilities/technical/product-control-open-audit.json
#	docs/interactive-capabilities/technical/tauri-command-map.json
#	src/apps/desktop/Cargo.toml
#	src/crates/contracts/product-domains/src/generated/product-control-catalog.json
#	src/web-ui/src/app/global-search/generated/interactive-capabilities.json
#	src/web-ui/src/infrastructure/api/generated/productControl.ts
@bobleer

bobleer commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator

Merged current main into this branch to clear the conflicts (f293115):

  • src/apps/desktop/Cargo.toml: kept main's reduced windows feature list (computer-use moved to its own crate) plus the Win32_UI_HiDpi feature this PR needs.
  • Interactive-capability generated files: took main's copies and regenerated them with pnpm run capabilities:generate.

Checks run on macOS after the merge: cargo test --locked -p openbitfun-desktop --no-default-features --lib desktop_cards (15 passed; the Windows-only native tests are cfg-gated off here), the 4 DesktopCard/Overview/Settings/API vitest files (59 passed), type-check, i18n:audit, capabilities:check/capabilities:test and check:core-boundaries. All passed. The Windows build is still covered only by Windows CI.

Before this merges, a few things need a real Windows check:

  1. Explorer restart. A pinned card is a child of an Explorer window, so when Explorer restarts the card window is destroyed with its parent. Nothing recreates it, and Home/Settings still say "Shown". Please either re-attach or re-open the card when the shell comes back (e.g. on TaskbarCreated), or mark the card as not shown, instead of failing silently.
  2. Startup / autostart before Explorer's desktop exists. sync_card_runtime destroys the window and only logs a warning, with no retry and nothing shown to the user.
  3. Cross-process parenting joins the input queues of OpenBitFun's UI thread and Explorer. A stall on our main thread can then freeze the desktop. Please confirm this in the manual pass along with Win+D, mixed-DPI moves and drag/resize.
  4. Minor: unpinning always sends alwaysOnTop: false, so a card that was always-on-top before pinning does not go back to that mode. If that is intended, a one-line note in the PR would help.

- Recreate a pinned card when Explorer destroys its desktop parent and
  reconcile pinned cards on the TaskbarCreated broadcast.
- Retry pinning with bounded backoff (about 31 s) when the desktop is not
  available yet, then unpin the card, show it as a normal window, and
  record desktopPinFailure so the card, Home and Settings report it.
- Restore the always-on-top mode a card had before it was pinned.
- Refuse to attach to a hung desktop and set WS_EX_NOPARENTNOTIFY so
  destroying a pinned card never sends WM_PARENTNOTIFY into Explorer.
@bobleer

bobleer commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator

Pushed 529869a to address the Windows review points:

  1. Explorer restart. A pinned card is a child of Explorer's desktop window, so Explorer's exit destroys it. The host now handles the card window's Destroyed event and also subclasses the main window for the TaskbarCreated broadcast. Both start a per-card recovery that recreates or reattaches the card from its record. Hide, delete, and disable update the record before closing the window, so they do not trigger recovery, and nothing runs once a desktop exit has started.
  2. Startup before the desktop exists. A failed pinned open starts the same recovery. It checks for the desktop with bounded backoff (0.5 s to 8 s, about 31 s in total) and only recreates the WebView once a desktop is available. If every attempt fails, the card is unpinned, its earlier always-on-top mode is restored, and it is shown as a normal window. The host also writes desktopPinFailure to the record, so the card, Home, and Settings each show an explicit notice. If the normal window cannot open either, the card is marked hidden. It is never left "Shown" without a window.
  3. Unpin. Pinning saves the card's earlier always-on-top mode in desktopPinRestoreAlwaysOnTop, and unpinning restores it. Records pinned before this change still unpin with always-on-top off, as they did before.
  4. Desktop freeze risk. The module sends no Progman 0x052C / WorkerW spawn message and makes no other synchronous cross-process SendMessage. It now refuses to attach while IsHungAppWindow reports the desktop as hung, and it sets WS_EX_NOPARENTNOTIFY so destroying a card never sends WM_PARENTNOTIFY into Explorer. Remaining risk: a cross-process parent/child relationship still attaches the input queues of OpenBitFun's UI thread and Explorer's desktop thread. While a card is pinned, a long stall on our UI thread can therefore still stall desktop input. This cannot be fully removed without moving off SetParent.

Checks on macOS: cargo check -p openbitfun-desktop, cargo test --locked -p openbitfun-desktop --no-default-features --lib desktop_cards (19 passed, including new tests for stacking restore, legacy records, and the failure notice), the 4 DesktopCard/Overview/Settings/API vitest files (36 passed), i18n:audit, and the Web UI type-check (no new errors). No Cargo features were added: everything used comes from the existing Win32_UI_Shell and Win32_UI_WindowsAndMessaging features.

Not verified: none of this has run on Windows. Windows CI will be the compile evidence. A manual pass is still needed for taskkill /f /im explorer.exe with a card pinned, autostart before Explorer is ready, Win+D, mixed-DPI moves, and drag/resize while pinned. In particular, nobody has yet observed tao/WebView2 emit Destroyed when Explorer destroys the parent; the TaskbarCreated path is the backstop for that case.

…op hosts

The explicit desktop capability list introduced for CLI peer hosts dropped
SshProxyCommandV1, which left the checked-in generated registry stale on main.
# Conflicts:
#	src/crates/contracts/product-domains/src/remote_surface/capabilities.rs
@bobleer
bobleer merged commit 9ed388b into GCWing:main Nov 2, 2026
14 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