From 246b5b93f88daa6ff950e89e1dcad3cfdeb17e45 Mon Sep 17 00:00:00 2001 From: Jonathan Gilbert Date: Sat, 11 Oct 2025 03:11:56 -0500 Subject: [PATCH] Centralize debounce of save window pos and save window pos on close (#12987) * Added method equals to class LastWindowPosition to compare the contents of instances. Added storage to common.dart for remembering what window position data has previously been written. Factored the actual save code from saveWindowPosition to _saveWindowPositionActual and updated saveWindowPosition to call it through a debouncer, and only if the window position data has actually changed since the last call in the same instance. Added named parameter 'flush' to saveWindowPosition in common.dart, and to _saveFrame in tabbar_widget.dart, and updated the onWindowClosed handler in tabbar_widget.dart to call _saveFrame with flush: true, forcing an immediate save on close. Removed the _saveFrame debouncer from tabbar_widget.dart. * saveWindowPosition: don't reschedule debounce if it's already in flight * Reworked the logic in saveWindowPosition to collapse a rapid series of updates into one save at the end. --- flutter/lib/common.dart | 58 ++++++++++++++++--- .../lib/desktop/widgets/tabbar_widget.dart | 15 ++--- 2 files changed, 57 insertions(+), 16 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 17f51857e..05e53164e 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -18,6 +18,7 @@ import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:flutter_hbb/utils/platform_channel.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; +import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart'; import 'package:provider/provider.dart'; import 'package:uni_links/uni_links.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -1674,6 +1675,16 @@ class LastWindowPosition { LastWindowPosition(this.width, this.height, this.offsetWidth, this.offsetHeight, this.isMaximized, this.isFullscreen); + bool equals(LastWindowPosition other) { + return ( + (width == other.width) && + (height == other.height) && + (offsetWidth == other.offsetWidth) && + (offsetHeight == other.offsetHeight) && + (isMaximized == other.isMaximized) && + (isFullscreen == other.isFullscreen)); + } + Map toJson() { return { "width": width, @@ -1713,9 +1724,14 @@ String get windowFramePrefix => ? "incoming_" : (bind.isOutgoingOnly() ? "outgoing_" : "")); +typedef WindowKey = ({WindowType type, int? windowId}); + +LastWindowPosition? _lastWindowPosition = null; +final Debouncer _saveWindowDebounce = Debouncer(delay: Duration(seconds: 1)); + /// Save window position and size on exit /// Note that windowId must be provided if it's subwindow -Future saveWindowPosition(WindowType type, {int? windowId}) async { +Future saveWindowPosition(WindowType type, {int? windowId, bool? flush}) async { if (type != WindowType.Main && windowId == null) { debugPrint( "Error: windowId cannot be null when saving positions for sub window"); @@ -1784,16 +1800,40 @@ Future saveWindowPosition(WindowType type, {int? windowId}) async { final pos = LastWindowPosition( sz.width, sz.height, position.dx, position.dy, isMaximized, isFullscreen); - debugPrint( - "Saving frame: $windowId: ${pos.width}/${pos.height}, offset:${pos.offsetWidth}/${pos.offsetHeight}, isMaximized:${pos.isMaximized}, isFullscreen:${pos.isFullscreen}"); - await bind.setLocalFlutterOption( - k: windowFramePrefix + type.name, v: pos.toString()); + final WindowKey key = (type: type, windowId: windowId); - if ((type == WindowType.RemoteDesktop || type == WindowType.ViewCamera) && - windowId != null) { - await _saveSessionWindowPosition( - type, windowId, isMaximized, isFullscreen, pos); + final bool haveNewWindowPosition = (_lastWindowPosition == null) || !pos.equals(_lastWindowPosition!); + final bool isPreviousNewWindowPositionPending = _saveWindowDebounce.isRunning; + + if (haveNewWindowPosition || isPreviousNewWindowPositionPending) { + _lastWindowPosition = pos; + + if (flush ?? false) { + // If a previous update is pending, replace it. + _saveWindowDebounce.cancel(); + await _saveWindowPositionActual(key); + } else if (haveNewWindowPosition) { + _saveWindowDebounce.call(() => _saveWindowPositionActual(key)); + } + } +} + +Future _saveWindowPositionActual(WindowKey key) async { + LastWindowPosition? pos = _lastWindowPosition; + + if (pos != null) { + debugPrint( + "Saving frame: ${key.windowId}: ${pos.width}/${pos.height}, offset:${pos.offsetWidth}/${pos.offsetHeight}, isMaximized:${pos.isMaximized}, isFullscreen:${pos.isFullscreen}"); + + await bind.setLocalFlutterOption( + k: windowFramePrefix + key.type.name, v: pos.toString()); + + if ((key.type == WindowType.RemoteDesktop || key.type == WindowType.ViewCamera) && + key.windowId != null) { + await _saveSessionWindowPosition( + key.type, key.windowId!, pos.isMaximized ?? false, pos.isFullscreen ?? false, pos); + } } } diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 4a898c32b..81f264073 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -292,7 +292,6 @@ class DesktopTab extends StatefulWidget { // ignore: must_be_immutable class _DesktopTabState extends State with MultiWindowListener, WindowListener { - final _saveFrameDebounce = Debouncer(delay: Duration(seconds: 1)); Timer? _macOSCheckRestoreTimer; int _macOSCheckRestoreCounter = 0; @@ -370,7 +369,7 @@ class _DesktopTabState extends State void _setMaximized(bool maximize) { stateGlobal.setMaximized(maximize); - _saveFrameDebounce.call(_saveFrame); + _saveFrame(); setState(() {}); } @@ -405,23 +404,23 @@ class _DesktopTabState extends State super.onWindowUnmaximize(); } - _saveFrame() async { + _saveFrame({bool? flush}) async { if (tabType == DesktopTabType.main) { - await saveWindowPosition(WindowType.Main); + await saveWindowPosition(WindowType.Main, flush: flush); } else if (kWindowType != null && kWindowId != null) { - await saveWindowPosition(kWindowType!, windowId: kWindowId); + await saveWindowPosition(kWindowType!, windowId: kWindowId, flush: flush); } } @override void onWindowMoved() { - _saveFrameDebounce.call(_saveFrame); + _saveFrame(); super.onWindowMoved(); } @override void onWindowResized() { - _saveFrameDebounce.call(_saveFrame); + _saveFrame(); super.onWindowResized(); } @@ -460,6 +459,8 @@ class _DesktopTabState extends State }); } + await _saveFrame(flush: true); + // hide window on close if (isMainWindow) { if (rustDeskWinManager.getActiveWindows().contains(kMainWindowId)) {