- a probe or raster-stall failure record now also downgrades sessions
that are already running: main_set_local_option broadcasts the
fallback for failed-* health writes, which also closes the hole where
the watchdog's idempotence guard no-oped after the probe had already
written the record
- probe success requires a frame timing newer than the first push:
'consumed' advances inside the plugin callback before the GL/Metal
upload, so a raster thread hanging in the driver no longer counts as
a pass (and cannot clear a previous failure)
- watchdog failures are tagged with the failing backend
(failed-watchdog-rgba/gpu); the rgba-only probe clears only the rgba
class, so a working pixel-buffer path can no longer re-enable a
broken D3D shared-handle path every launch
- the watchdog pauses while the session's window is hidden (new
session_set_render_visible FFI wired to the window minimize/restore
events): a display registered in a minimized window no longer records
a false global failure; observation now counts pushes within the
window rather than since registration
- probe failure verdicts additionally require a resumed lifecycle,
matching the raster-stall monitor
- linux plugin: deferred-unref grace lengthened to 10s (no raster-side
completion barrier exists; documented as heuristic), ref bumped
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- watchdog: compare the plugin's cumulative consumed counter against a
snapshot taken when arming (re-arm was a no-op before), and judge on
cumulative pushes + elapsed time so sparse damage-driven streams are
still detected
- failure handling is idempotent (one record per breakage) and updates
every render session like main_set_local_option does, not only the
failing one; the fallback toast is not claimed for multi-display
windows the soft path cannot rescue
- probe: skip when the consumed API is missing (old plugin) or a
raster-stall is recorded (compositing could hang the main window);
a fail verdict requires fresh frame timings and a non-minimized
window; a pass never clears failed-raster-stall; toast only when
texture rendering was effectively on; probe pixel is transparent
- raster-stall monitor: judge on frame-timing staleness (a mid-episode
hang was undetectable before), gate on lifecycle/minimized/idle with
a moving quiet anchor, threshold 30s, shared with the camera page;
clear stateGlobal minimized flag on plain window restore
- adopt mismatched frame sizes only in multi-ui-session mode (legacy
mode pairs frames loosely and could ping-pong between displays)
- drop the now-dead closeSession parameter from texture destroy();
trim comments to repo style
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root cause of #15848 (and the long-standing macOS #6296 / Linux #3343
class): raw native texture pointers are shared across the platform
thread, the engine raster thread and the video thread, with teardown
ordered by a 100 ms sleep - or, when moving a tab to a new window, by
nothing at all. A lost race frees the texture while it is still in use:
the raster thread parks on a destroyed lock (frozen/black view, a
never-presented 'transparent' hole, every later session black) and the
video thread hangs while holding session locks (app half-dead until
restart, still reported as Responding).
- unregister textures with compare-and-clear (new session_unregister_*
FFI): a late clear can no longer wipe a new window's registration
(#8016) and Rust never keeps pushing into a freed texture; the 100 ms
sleeps are gone (the plugins now drain in-flight pushes and defer
object deletion until the raster thread is done)
- guard the async texture create path against destroy racing it (#13596)
- per-display locks: the per-frame plugin call no longer holds
session-level locks, so a stalled plugin or driver call cannot freeze
every window's UI thread
- adopt the frame size after 30 consecutive mismatches instead of
dropping frames forever (silent black screen on a live connection)
- watchdog: frames pushed but never consumed by the engine fall the
session back to software rendering live, record texture-render-health,
and flip the effective default off; toggling the option clears the
record and re-arms validation
- Dart raster-stall monitor records a hung raster thread for the next
launch (rendering cannot be rescued in-process in that state)
- startup probe: render one frame through a 1x1 texture in the main
window each launch; failure disables texture rendering before the
first session goes black, a pass self-heals a stale failure record
Platform defaults are unchanged (macOS off, Win10+ on, Linux on).
Requires flutter_texture_rgba_renderer ad4c37e and
flutter_gpu_texture_renderer 767bb9f (pinned in pubspec).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Remove scrollback lines through the index-aware buffer operation so
deleted anchors are detached and retained lines are reindexed.
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix(terminal): send SGR mouse wheel reports with the button codes apps expect
xterm.dart 4.0.0 encodes the wheel buttons as 64+4..64+7 rather than
64+0..64+3, so the low bits land on the modifier field and every wheel
report the terminal emits reads as wheel-with-Shift. Strict full-screen
applications reject the modified event, which is why neither the mouse
wheel nor the trackpad scrolls anything once the peer application takes
over the alternate screen.
Install a mouse handler that keeps every upstream reporting decision and
only re-encodes the wheel buttons as 64..67. Non-wheel reports pass
through untouched, and the emitted bytes stay identical once upstream
ships the same fix, so this can be dropped without a behavior change.
Upstream: TerminalStudio/xterm.dart#238
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(terminal): correct the wheel report row, drop the wasted report build
Address review feedback on the wheel button fix:
- The X10/utf row was encoded as `32 + y + 1` while y is already 1-based, so
every normal-mode report pointed one row too low and the `y > limit` guard
disagreed with what it emitted.
- Gate the wheel path on `mouseMode.reportScroll` and the button state instead
of building and discarding a full report string from `defaultMouseHandler`
on every scroll tick. This also makes the hardcoded SGR 'M' provably right,
since a wheel release now returns before the report is built.
- Derive the wire code as `id - 4` and drop `_wheelButtonId`, whose `default`
branch was unreachable and defeated enum exhaustiveness.
- Assign `mouseHandler` after construction so the `Terminal(...)` line stays
untouched.
Cover the utf, urxvt, null-byte overflow and click-only branches, and assert
that TerminalModel actually installs the handler.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(web): zero-readback WebCodecs video path
Decoded VideoFrames from js/src/webcodecs.js are handed to Flutter via
window.onVideoFrame and imported GPU-side with createImageFromTextureSource;
any failure unregisters the hook so the JS side falls back to RGBA readback.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(web): load bundled terminal font when Google CDNs are unreachable
In air-gapped deployments GoogleFonts.robotoMono() cannot download the
terminal font; when index.html signals offline mode, load the copy bundled
with the web app under the family name google_fonts registers.
Part of the fix for rustdesk/rustdesk-server-pro#996.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ci: bump windows arm64 to Flutter 3.44.8, add web build patch script
apply_flutter_3.44_web_patches.sh prepares a 3.44.x web build on top of the
shared source patches: qr_code_scanner's web impl needs dart:ui_web for the
removed platformViewRegistry, and flutter/web/fonts is refreshed to the font
paths the 3.44 engine requests. The disabled build-rustdesk-web job runs it
automatically once FLUTTER_VERSION moves to 3.44.x, and version-guarded
'Patch flutter' steps no longer fail when the guard does not match.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(web): prevent stale WebCodecs frames across sessions
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix(web): harden WebCodecs reconnect and Flutter 3.44 patches
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix(ci): harden Flutter 3.44 patch input validation
Validate required files before checking patch state,
parameterize the theme-range validator, and prevent
missing inputs from satisfying NO_MATCHES checks.
Signed-off-by: fufesou <linlong1266@gmail.com>
* Remove unused code
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix(web): retry font loading and dispose stale decoded images
Signed-off-by: fufesou <linlong1266@gmail.com>
* remove unused code
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix(web): Bad state: RenderBox was not laid out
Signed-off-by: fufesou <linlong1266@gmail.com>
---------
Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: fufesou <linlong1266@gmail.com>
* fix(flutter): show error and retry when fetching login options fails
The third-party login section of the login dialog was silently hidden
whenever /api/login-options could not be fetched (e.g. TLS handshake
aborted by a router/ISP scam filter, discussion #15700), leaving users
staring at a dialog with no feedback. The pure-Dart HTTP path also had
no timeout, so a black-holed connection could hang indefinitely.
- let transport errors propagate from queryOidcLoginOptions instead of
swallowing them; a non-JSON response still means "no third-party
login" so self-hosted servers without this API keep the old behavior
- show network_error_tip, a Retry button, and the underlying error in
the login dialog so users and supporters can see what failed
- bound the Dart HTTP branch with a 15s timeout; the Rust branch keeps
its own bounded per-attempt timeouts and is awaited to completion so
a retry never races the URL-keyed ASYNC_HTTP_STATUS entry of an
abandoned in-flight request
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(flutter): surface currentUser refresh failures that were only logged
Non-transport failures of the token auto-login (/api/currentUser) -- a
bad HTTP status, a filter's HTML block page, or an error field in the
body -- were only debugPrinted, so the address book / group tabs showed
nothing and offered no retry. Reuse the existing networkError channel
so netWorkErrorWidget shows the error with its Retry button.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(flutter): keep retry row visible with progress while refetching login options
Review follow-ups: clicking Retry used to clear the error and hide the
row with no pending feedback, which could read as a dead click while
the Rust fallback chain runs; keep the row, disable the button, and
show the usual LinearProgressIndicator instead. Also raise the Dart
HTTP branch timeout to 30s so large web address book pulls on slow
links do not newly time out; it still bounds the previously unbounded
hang and stays above the Rust side's 12s per-attempt timeout.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: update webpki-roots to latest Mozilla root store
0.26.9 -> 0.26.11 (now a forwarding shim over 1.x, used by tungstenite)
1.0.4 -> 1.0.9 (used by reqwest / hyper-rustls / hbb_common)
The 0.26.9 line carried its own root snapshot frozen in early 2025, so
the websocket TLS path was building against a stale bundle.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ci: weekly workflow to PR webpki-roots root store updates
webpki-roots is a transitive dependency, so dependabot's cargo version
updates would not cover it. A scheduled job runs cargo update for every
webpki-roots instance in each lockfile and opens a PR when the pinned
Mozilla root snapshot is behind, keeping root store changes reviewable
instead of baking them silently into release builds.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(flutter): hide network tip for server-reported currentUser errors
Review follow-up: when /api/currentUser fails with an error the server
itself reported (an error field in a JSON body, or an unexpected
schema), "Please check your network connection" was misleading. Track
whether the surfaced error came from a server response and skip the
network tip for those; FormatException (a non-JSON body such as a
filter's block page) keeps it, since that still indicates a network or
middlebox problem.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(flutter): close timed-out HTTP clients
* fix(flutter): flag server-reported errors at the throw site
Review follow-up (CodeRabbit). Classifying by `e is! FormatException`
mislabeled ambiguous failures: a middlebox block page returning 200
with valid-but-wrong-shape JSON throws a TypeError from fromJson and
was shown without the check-your-network tip, though it is a network
artifact. Set networkErrorFromServer only at the one site that is
certainly server-reported (an error field in the body); every other
failure keeps the network tip plus the raw error text.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ci: serialize webpki-roots update runs, null-delimit lockfile paths
Review follow-up (CodeRabbit). A manual dispatch overlapping the weekly
cron could have an older run force-push over the newer branch state;
queue runs via a concurrency group without cancel-in-progress. Also
iterate lockfiles with git ls-files -z so a path with spaces cannot be
word-split, and keep the loop failing the step on any cargo error.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(flutter): improve login retry feedback
Use the theme primary color for the Retry button and hide stale
error messages while a retry is in progress.
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix(flutter): surface login option response errors
Signed-off-by: fufesou <linlong1266@gmail.com>
---------
Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: fufesou <linlong1266@gmail.com>
* fix(linux): load librustdesk.so relative to the executable
The runner and the Dart FFI init loaded the core library by bare name,
relying on the runner's $ORIGIN/lib RPATH. Repackaged installs (CachyOS
repo, AUR) can lose that RPATH, making the app fail to start with
"Failed to load librustdesk.so" unless users add the lib directory to
ld.so.conf. Resolve lib/librustdesk.so next to the executable first,
then fall back to the loader search path.
https://github.com/rustdesk/rustdesk/discussions/14407
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(linux): harden bundled librustdesk.so resolution
Address review: bail out when readlink() may have truncated the
executable path, and widen the Dart try block so any failure probing
the bundled library falls back to the loader search path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(recording): add visibility and service storage options
- support hide-recording-button in Flutter and Sciter
- allow a custom save directory for Windows service recordings
- sanitize peer IDs used in recording filenames
Tested:
- with hide-recording-button=Y and allow-auto-record-outgoing=Y,
outgoing sessions are recorded automatically while the recording button
remains hidden and cannot be stopped from the UI; verified on Flutter
desktop, Sciter, and Android
- windows-service-video-save-directory takes effect when the Windows client
runs as an installed service
- the Windows controlling side can save recordings for direct IP:port
connections
Signed-off-by: 21pages <sunboeasy@gmail.com>
* update hbb_common
Signed-off-by: 21pages <sunboeasy@gmail.com>
* fix(recording): validate configured save directories
- trim configured recording directory paths
- reject non-absolute paths and fall back to defaults
- warn when a non-empty path is invalid
Signed-off-by: 21pages <sunboeasy@gmail.com>
* fix(recording): validate configured save directories
Signed-off-by: 21pages <sunboeasy@gmail.com>
---------
Signed-off-by: 21pages <sunboeasy@gmail.com>
* Fix Adjust Window sizing across DPI and fullscreen transitions
Signed-off-by: 21pages <sunboeasy@gmail.com>
* Use visible screen frame for Adjust Window
Signed-off-by: 21pages <sunboeasy@gmail.com>
* Tolerate floating-point errors in Adjust Window sizing
Signed-off-by: 21pages <sunboeasy@gmail.com>
* Fix review, fix Adjust Window async metric guards
Capture the current screen before awaiting window geometry so one target-frame calculation uses consistent screen metrics.
Return early when adjusting without a context and the Flutter view list is empty, instead of calling views.first after the window or engine may have been torn
down.
Clarify the platform coordinate units used for Adjust Window scaling.
* Fix Adjust Window for maximized Linux windows
Unmaximize Linux remote windows before applying Adjust Window because native setFrame may be ignored while the window is maximized.
* Fix Adjust Window screen refresh guards
Refresh screen metrics before checking Adjust Window availability and again after exiting fullscreen so target-frame calculation uses current window geometry.
Hide Adjust Window on web because resizing relies on desktop window APIs.
* Fix review, handle missing window frame in Adjust Window
Return null when WindowController.getFrame fails so Adjust Window availability checks and resize attempts skip cleanly if the window is hidden or disposed.
---------
Signed-off-by: 21pages <sunboeasy@gmail.com>
* Feature: Restore the last viewed monitor on auto reconnect
Remembers the users last manually selected remote monitor and returns to it after an auto reconnect.
In memory, reconnect only, and bounds checked against the current display count.
It is skipped in "use all my displays" mode.
Signed-off-by: StealUrKill <35749471+StealUrKill@users.noreply.github.com>
* Address review on reconnect monitor restore
Avoid a crash if the session closes during a reconnect.
Don't overwrite the remembered monitor on auto restore.
Defer the switch until the view is ready so a monitor with a different size renders correctly.
Signed-off-by: StealUrKill <35749471+StealUrKill@users.noreply.github.com>
* Guard all-displays reconnect restore against empty display list
* Harden reconnect monitor restore against races and multi-UI sessions
Cancel a queued restore when the user manually selects a monitor, so a
newer choice is not overridden by a stale pending restore.
Compare the remembered monitor against the reconnect event's display
instead of the stale _pi.currentDisplay, which is intentionally left
unchanged when the peer has multiple sessions.
Add a frame-independent fallback so a multi-UI tab that never receives
the first-image event (its display is filtered to the owning tab) still
restores the remembered monitor.
Signed-off-by: StealUrKill <35749471+StealUrKill@users.noreply.github.com>
* Harden reconnect monitor restore: fallback timer, lifecycle, cursor
Follow-up hardening on the auto-reconnect monitor restore:
- Cancel the fallback timer synchronously once this tab owns the restore,
so it can no longer fire while onEvent2UIRgba is awaiting canvas setup
and switch displays before the canvas is ready (the offset the deferred
restore exists to avoid). The multi-UI no-frame fallback stays intact.
- Apply the restore in a finally so a throwing canvas init still runs it
instead of stranding a queued restore with the timer already cancelled.
- Cancel the fallback timer on a manual monitor switch, so a newer user
selection supersedes a queued restore instead of racing it.
- Restore with updateCursorPos: false, matching other programmatic
display switches so an auto-restore does not reposition the cursor.
---------
Signed-off-by: StealUrKill <35749471+StealUrKill@users.noreply.github.com>
* Feature: add monitor-switch buttons to remote toolbars
Add buttons to cycle through the remote displays from the toolbars:
- A main-toolbar button and a minimized-handle button, both using a shared SVG icon with the current monitor number overlaid.
- Two opt-in settings under Settings/Other. The minimized-toolbar option is nested under the main-toolbar option.
- The minimized button only appears once the toolbar is collapsed.
- Cycling does not move the remote cursor, matching the existing in-toolbar monitor buttons.
Signed-off-by: StealUrKill <35749471+StealUrKill@users.noreply.github.com>
* fix: respect individual-window display mode when cycling
In "Show displays as individual windows" mode, route the cycle button through openMonitorInNewTabOrWindow like the monitor selector, so each display keeps its own window instead of repurposing the current one.
Signed-off-by: StealUrKill <35749471+StealUrKill@users.noreply.github.com>
---------
Signed-off-by: StealUrKill <35749471+StealUrKill@users.noreply.github.com>
* Feature: add monitor-switch buttons to remote toolbars
Add a one-click "switch to next monitor" control to both desktop toolbars:
- Main toolbar: always shown when the remote has more than one monitor,
styled to match the existing blue icon buttons (white screen, black number).
- Minimized (draggable show/hide) toolbar: off by default, toggled via a new
"Show monitor switch on minimized toolbar" checkbox in the Display menu and
persisted as a local option.
Signed-off-by: StealUrKill <35749471+StealUrKill@users.noreply.github.com>
* Update remote_toolbar.dart
* refact: unify monitor-switch button icons, share tooltip
Addressing the review feedback on the monitor-switch toolbar buttons:
- Add assets/display_switcher.svg and use it for both the main and minimized buttons. This replaces the hand-drawn glyph (Containers + magic numbers) on the main toolbar. The icon scales with DPI/theme and the two toolbars stay visually consistent.
- Flip the minimized button's label to white for contrast, since the new icon has a solid screen.
- Move the tooltip string into a shared _MonitorCycle.tooltip getter so both buttons use one source of truth.
- Use const Offstage() for consistency with the surrounding returns.
Signed-off-by: StealUrKill <35749471+StealUrKill@users.noreply.github.com>
* Improve monitor-switch settings and toolbar behavior
- Nest the minimized-toolbar option under the main one in settings only show when the main option is enabled.
- Only show the minimized switch button on the collapsed toolbar handle, so it no longer duplicates the main switch while the toolbar is expanded.
Signed-off-by: StealUrKill <35749471+StealUrKill@users.noreply.github.com>
---------
Signed-off-by: StealUrKill <35749471+StealUrKill@users.noreply.github.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
* fix(arm64-linux): fix CJK font rendering on flutter-elinux
The flutter-elinux engine used for ARM64 Linux builds is compiled without
--enable-fontconfig, so Flutter's text shaper cannot discover system fonts.
This causes CJK characters to render as tofu boxes even when fonts such as
Noto Sans CJK are installed. See flutter/flutter#139293.
Fix by loading a CJK font at startup via FontLoader (bypassing fontconfig)
and propagating it through two paths so all text widgets are covered:
1. MyTheme.applyFontFallback() — updates textTheme on both light and dark
ThemeData so Material components receive the fallback through the theme.
2. _mergeCjkFallback() in GetMaterialApp builders — wraps child widgets in
DefaultTextStyle.merge so bare Text() widgets and those with inherit:true
also render CJK characters correctly.
Font discovery queries fc-list for zh, ja, and ko separately, preferring
fonts present in all three sets (true pan-CJK fonts such as NotoSansCJK or
SourceHanSans) over Chinese-only fonts that may lack Japanese kana or Korean
hangul glyphs. Falls back to a hardcoded search-path list covering
Debian/Ubuntu, Fedora/RHEL, Arch Linux, and WenQuanYi font layouts.
This is an app-level workaround. The engine-level fix is tracked at
flutter/flutter#180235 (open as of 2026-06).
Fixes#10666
Signed-off-by: Bia503 <yinwenche189@gmail.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---------
Signed-off-by: Bia503 <yinwenche189@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
RemotePage.dispose() only reaches sessionClose at the tail of gFFI.close(),
behind several awaits (canvas save, image update, the enable_soft_keyboard
platform call). If the app is backgrounded while the page is disposing,
dispose can be suspended before that runs, so the session is never torn down.
The next reconnect re-attaches to the leaked session (mobile reuses a constant
sessionId) and is stuck on "Connecting..." forever while the orphaned io_loop
keeps streaming.
Dispatch sessionClose at the start of dispose so teardown happens synchronously
on route pop, before backgrounding can interrupt it. The sessionClose in
gFFI.close() becomes a no-op once the session is already removed.
Fixes#15060
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Drag whole toolbar; snap to all four edges
Today the drag handle on the remote-session toolbar repositions only
the handle row -- the icons themselves stay centered at the top. This
change applies the position to the entire toolbar wrapper so dragging
the handle moves the whole thing, and extends snapping from top-only
to any of the four window edges.
When docked left/right the toolbar reflows vertically. A live ghost
preview shows where the toolbar will land while you drag, with a small
hysteresis bias to keep the preview from flickering near corners.
The legacy 'remote-menubar-drag-x' session option is read as a fallback
on first load so existing users keep their saved horizontal position;
new option keys are 'remote-menubar-edge' and 'remote-menubar-frac'.
Tested locally on Windows. macOS / Linux / web desktop use the same
shared widget with no platform-specific calls, but I did not verify
them.
* Load edge independently and clamp loaded fraction
Addresses CodeRabbit review on #15051: parse the saved edge regardless
of whether the new fraction option is present so a partial write of
frac doesn't reset the toolbar back to top, and clamp the loaded
fraction to the kOptionRemoteMenubarDragLeft/Right contract so a
corrupted or out-of-range saved value can't bypass the bounds until
the user drags again.
* Require edge activation zone to switch dock; preserve horizontal slide
Per review feedback on #15051: nearest-edge-wins made a low-intent
horizontal slide too easy to escalate into a high-impact orientation
change (vertical reflow on left/right dock). The default drag now
keeps the toolbar on its current dock edge and just updates the
fraction along that edge -- the prior horizontal-slide behavior.
An alternate edge is only previewed/committed when the cursor enters
its 32 px activation zone; once previewed, the cursor has to move
back 64 px before reverting (hysteresis at the zone boundary).
* Gate multi-edge docking behind a settings toggle; default = horizontal slide
Replaces the activation-zone approach with an explicit opt-in setting
in Settings -> Other ("Allow docking remote toolbar to any window
edge"). This addresses the concern that a low-intent horizontal drag
shouldn't be able to trigger a high-impact orientation change, while
still letting users who want multi-edge docking opt in cleanly.
Default (toggle off):
- The original horizontal slide is preserved.
- The bug fix from the first commit still applies: dragging the
handle moves the whole toolbar, and the position persists across
collapse/expand (no more re-center on re-open).
- Draggable is axis-locked to horizontal so the feedback widget
stays on the top line during drag.
Opt-in (toggle on):
- Full nearest-edge wins with the live preview ghost and corner
hysteresis; toolbar reflows vertically on left/right docks.
- Draggable is unlocked for 2D drag.
Reads the option via mainGetLocalBoolOptionSync so the toolbar's
default state matches what the settings checkbox shows; the option
key uses the allow- prefix so unset defaults to off.
Takes effect on next session (setting is read at session init).
The setting key (allow-multi-edge-toolbar-dock) is read by the
existing local-options machinery and persists per-install without
needing to be registered in libs/hbb_common's KEYS_LOCAL_SETTINGS.
Can add that registration in a parallel hbb_common PR if preferred.
* Fix remote toolbar drag positioning & persistence
Align drag fraction calculation with the toolbar's actual travel range,
keep preview sizing stable during drag, and preserve legacy horizontal
position storage when multi-edge docking is disabled.
Signed-off-by: fufesou <linlong1266@gmail.com>
* Remote toolbar snap edges
1. Translations
2. Apply option to remote windows on changed
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix: avoid remote toolbar docking jumps on setting reload
Signed-off-by: fufesou <linlong1266@gmail.com>
* Fix remote toolbar docking updates and drag sync
Signed-off-by: fufesou <linlong1266@gmail.com>
* refact: translation key
Signed-off-by: fufesou <linlong1266@gmail.com>
* feat(toolbar-snap-edges): test web
Signed-off-by: fufesou <linlong1266@gmail.com>
* Fix remote toolbar docking sync and vertical layout
Signed-off-by: fufesou <linlong1266@gmail.com>
* Fix remote toolbar monitor controls on side docks
Signed-off-by: fufesou <linlong1266@gmail.com>
---------
Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: fufesou <linlong1266@gmail.com>