From 4521c503810dece62c4af478c599ffdf2baa47df Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 23 May 2026 20:39:50 +0800 Subject: [PATCH] fix: avoid remote toolbar docking jumps on setting reload Signed-off-by: fufesou --- .../lib/desktop/widgets/remote_toolbar.dart | 139 ++++++++++++++---- 1 file changed, 107 insertions(+), 32 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index e7f55bcf2..ab57c44b5 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -167,6 +167,46 @@ BorderRadius _collapseHandleBorderRadius(_ToolbarEdge edge) { } } +class _ToolbarDockingOptions { + _ToolbarDockingOptions({ + required this.edge, + required this.fraction, + required this.multiEdgeEnabled, + }); + + _ToolbarEdge edge; + double fraction; + bool multiEdgeEnabled; +} + +final _toolbarDockingOptionsBySession = {}; + +String _toolbarDockingCacheKey(SessionID sessionId) => sessionId.toString(); + +_ToolbarDockingOptions? _cachedToolbarDockingOptions(SessionID sessionId) => + _toolbarDockingOptionsBySession[_toolbarDockingCacheKey(sessionId)]; + +void _cacheToolbarDockingOptions({ + required SessionID sessionId, + required _ToolbarEdge edge, + required double fraction, + required bool multiEdgeEnabled, +}) { + final key = _toolbarDockingCacheKey(sessionId); + final cached = _toolbarDockingOptionsBySession[key]; + if (cached == null) { + _toolbarDockingOptionsBySession[key] = _ToolbarDockingOptions( + edge: edge, + fraction: fraction, + multiEdgeEnabled: multiEdgeEnabled, + ); + return; + } + cached.edge = edge; + cached.fraction = fraction; + cached.multiEdgeEnabled = multiEdgeEnabled; +} + class ToolbarState { late RxBool _pin; @@ -405,6 +445,7 @@ class _RemoteToolbarState extends State { // the user has opted into multi-edge docking with nearest-edge snap. // Kept in sync after settings-triggered rebuilds. final _multiEdgeEnabled = false.obs; + final _dockingOptionsInitialized = false.obs; bool _pendingDockingOptionSync = false; int _dockingOptionSyncSerial = 0; @@ -440,32 +481,43 @@ class _RemoteToolbarState extends State { // diverge from how _OptionCheckBox displays the same key. final multiEdgeEnabled = mainGetLocalBoolOptionSync(kOptionAllowMultiEdgeToolbarDock); - final wasMultiEdgeEnabled = _multiEdgeEnabled.value; - if (!force && wasMultiEdgeEnabled == multiEdgeEnabled) return; + final cached = _cachedToolbarDockingOptions(widget.ffi.sessionId); + if (cached == null && pi.isSet.isFalse) { + return; + } + final hadDockingOptions = cached != null; + final wasMultiEdgeEnabled = + cached?.multiEdgeEnabled ?? _multiEdgeEnabled.value; + if (!force && + hadDockingOptions && + wasMultiEdgeEnabled == multiEdgeEnabled) { + _pendingDockingOptionSync = false; + return; + } final savedFraction = await bind.sessionGetOption( sessionId: widget.ffi.sessionId, arg: kOptionRemoteMenubarFraction); // Backward compat: legacy horizontal-only position. final legacyFraction = await bind.sessionGetOption( sessionId: widget.ffi.sessionId, arg: _legacyRemoteMenubarDragX); - if (syncSerial != _dockingOptionSyncSerial) return; + if (!mounted || syncSerial != _dockingOptionSyncSerial) return; var nextEdge = _edge.value; var savedFractionForNextEdge = savedFraction; var keepCurrentPosition = false; if (!multiEdgeEnabled) { nextEdge = _ToolbarEdge.top; - } else if (force || wasMultiEdgeEnabled) { + } else if (force || wasMultiEdgeEnabled || cached == null) { final edgeStr = await bind.sessionGetOption( sessionId: widget.ffi.sessionId, arg: kOptionRemoteMenubarEdge); - if (syncSerial != _dockingOptionSyncSerial) return; + if (!mounted || syncSerial != _dockingOptionSyncSerial) return; nextEdge = _parseToolbarEdge(edgeStr); } else { // The setting changed from top-only to multi-edge while this toolbar is // already visible. Keep its current position instead of jumping to the // last saved multi-edge dock. - nextEdge = _ToolbarEdge.top; - savedFractionForNextEdge = _fraction.value.toString(); + nextEdge = cached.edge; + savedFractionForNextEdge = cached.fraction.toString(); keepCurrentPosition = true; } @@ -486,12 +538,19 @@ class _RemoteToolbarState extends State { final nextFraction = (double.tryParse(rawFraction) ?? 0.5) .clamp(dragLeft, dragRight) .toDouble(); - if (syncSerial != _dockingOptionSyncSerial) return; + if (!mounted || syncSerial != _dockingOptionSyncSerial) return; _edge.value = nextEdge; _fraction.value = nextFraction; _multiEdgeEnabled.value = multiEdgeEnabled; + _dockingOptionsInitialized.value = true; + _cacheToolbarDockingOptions( + sessionId: widget.ffi.sessionId, + edge: nextEdge, + fraction: nextFraction, + multiEdgeEnabled: multiEdgeEnabled, + ); _pendingDockingOptionSync = false; - if (keepCurrentPosition) { + if (!multiEdgeEnabled || keepCurrentPosition) { bind.sessionPeerOption( sessionId: widget.ffi.sessionId, name: kOptionRemoteMenubarEdge, @@ -516,8 +575,20 @@ class _RemoteToolbarState extends State { initState() { super.initState(); + final cached = _cachedToolbarDockingOptions(widget.ffi.sessionId); + final multiEdgeEnabled = + mainGetLocalBoolOptionSync(kOptionAllowMultiEdgeToolbarDock); + final shouldResetToTop = + cached != null && cached.multiEdgeEnabled && !multiEdgeEnabled; + if (cached != null && !shouldResetToTop) { + _edge.value = cached.edge; + _fraction.value = cached.fraction; + _multiEdgeEnabled.value = multiEdgeEnabled; + _dockingOptionsInitialized.value = true; + } + WidgetsBinding.instance.addPostFrameCallback((_) async { - await _syncDockingOptions(force: true); + await _syncDockingOptions(force: cached == null || shouldResetToTop); // Initialize toolbar states (collapse, hide) from session options widget.state.init(widget.ffi.sessionId); }); @@ -554,16 +625,17 @@ class _RemoteToolbarState extends State { @override dispose() { - super.dispose(); - + ++_dockingOptionSyncSerial; widget.onEnterOrLeaveImageCleaner(identityHashCode(this)); + super.dispose(); } @override Widget build(BuildContext context) { return Obx(() { // Wait for initialization to complete to prevent flickering - if (!widget.state.initialized.value) { + if (!widget.state.initialized.value || + !_dockingOptionsInitialized.value) { return const SizedBox.shrink(); } // If toolbar is hidden, return empty widget @@ -3023,27 +3095,30 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { if (newEdge == null || frac == null) return; widget.edge.value = newEdge; widget.fraction.value = frac; - // Only persist the edge when multi-edge docking is opted in. In default - // mode the edge is always top, so writing it back would be noise — and - // we want toggling the setting off to feel like a clean revert. + _cacheToolbarDockingOptions( + sessionId: widget.sessionId, + edge: newEdge, + fraction: frac, + multiEdgeEnabled: widget.multiEdgeEnabled, + ); + bind.sessionPeerOption( + sessionId: widget.sessionId, + name: kOptionRemoteMenubarEdge, + value: _toolbarEdgeToString(newEdge), + ); + bind.sessionPeerOption( + sessionId: widget.sessionId, + name: kOptionRemoteMenubarFraction, + value: frac.toString(), + ); if (widget.multiEdgeEnabled) { - bind.sessionPeerOption( - sessionId: widget.sessionId, - name: kOptionRemoteMenubarEdge, - value: _toolbarEdgeToString(newEdge), - ); - bind.sessionPeerOption( - sessionId: widget.sessionId, - name: kOptionRemoteMenubarFraction, - value: frac.toString(), - ); - } else { - bind.sessionPeerOption( - sessionId: widget.sessionId, - name: _legacyRemoteMenubarDragX, - value: frac.toString(), - ); + return; } + bind.sessionPeerOption( + sessionId: widget.sessionId, + name: _legacyRemoteMenubarDragX, + value: frac.toString(), + ); } void _cancelPreview() {