From afc2815c24948228de10010f9771af9190577de6 Mon Sep 17 00:00:00 2001 From: palmoni5 Date: Tue, 10 Nov 2026 21:06:37 +0200 Subject: [PATCH] perf(legacy): skip the native bounds read when setBounds gets a full Rect setBounds read the window's current bounds through nativeapi before every call, even when the caller passed a full Rect and none of the current values were used. Only read them when position or size is partial. Adds debugBindWindow (same hook as the sibling legacy PRs) so the bridge can be tested against a Dart fake window. --- lib/src/legacy/window_manager.dart | 35 ++++++--- test/legacy_set_bounds_test.dart | 113 +++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+), 10 deletions(-) create mode 100644 test/legacy_set_bounds_test.dart diff --git a/lib/src/legacy/window_manager.dart b/lib/src/legacy/window_manager.dart index 73396ab9..a620c1fe 100644 --- a/lib/src/legacy/window_manager.dart +++ b/lib/src/legacy/window_manager.dart @@ -4,7 +4,8 @@ import 'dart:async'; import 'dart:io'; import 'dart:ui' show Brightness, Color, Offset, PlatformDispatcher, Rect, Size; -import 'package:flutter/foundation.dart' show ObserverList, VoidCallback; +import 'package:flutter/foundation.dart' + show ObserverList, VoidCallback, visibleForTesting; import 'package:flutter/painting.dart' show Alignment; import 'package:nativeapi/nativeapi.dart' as nativeapi; import 'package:nativeapi_flutter/nativeapi_flutter.dart' @@ -66,6 +67,15 @@ class WindowManager { nativeapi.Window? _window; nativeapi.ListenerId? _nativeListenerId; + /// Addresses [window] instead of the platform's current window, or goes + /// back to the current window with null. For tests of this bridge only. + @visibleForTesting + void debugBindWindow(nativeapi.Window? window) { + _unwireNativeEvents(); + _window = window; + if (_listeners.isNotEmpty || _isPreventClose) _wireNativeEvents(); + } + bool _isPreventClose = false; bool _isDestroying = false; bool _isMaximized = false; @@ -397,15 +407,20 @@ class WindowManager { final window = _target; if (window == null) return; if (bounds == null && position == null && size == null) return; - final current = window.bounds.toRect(); - final target = - bounds ?? - Rect.fromLTWH( - position?.dx ?? current.left, - position?.dy ?? current.top, - size?.width ?? current.width, - size?.height ?? current.height, - ); + // Full bounds are already known: reading them again through nativeapi + // performs an unnecessary synchronous FFI call on every restore/move. + Rect target; + if (bounds != null) { + target = bounds; + } else { + final current = window.bounds.toRect(); + target = Rect.fromLTWH( + position?.dx ?? current.left, + position?.dy ?? current.top, + size?.width ?? current.width, + size?.height ?? current.height, + ); + } if (!animate || !window.animateBounds(target.toNative())) { window.bounds = target.toNative(); } diff --git a/test/legacy_set_bounds_test.dart b/test/legacy_set_bounds_test.dart new file mode 100644 index 00000000..063574c8 --- /dev/null +++ b/test/legacy_set_bounds_test.dart @@ -0,0 +1,113 @@ +// ignore_for_file: deprecated_member_use_from_same_package + +import 'dart:ui' show Offset, Rect, Size; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:nativeapi/nativeapi.dart' as nativeapi; +import 'package:window_manager/legacy.dart'; + +// A nativeapi window simulated in Dart; no native window is created. +class _FakeNativeWindow extends nativeapi.Window { + _FakeNativeWindow() : super.borrowed(0); + + int boundsReads = 0; + nativeapi.Rectangle currentBounds = const nativeapi.Rectangle( + x: 1, + y: 2, + width: 300, + height: 200, + ); + + @override + nativeapi.Rectangle get bounds { + boundsReads++; + return currentBounds; + } + + @override + set bounds(nativeapi.Rectangle value) { + currentBounds = value; + } + + // Simulates a platform without native bounds animation (Windows/Linux). + final List animateRequests = []; + + @override + bool animateBounds(nativeapi.Rectangle bounds) { + animateRequests.add(bounds); + return false; + } +} + +void main() { + late _FakeNativeWindow native; + + setUp(() { + native = _FakeNativeWindow(); + windowManager.debugBindWindow(native); + }); + + tearDown(() => windowManager.debugBindWindow(null)); + + test('full bounds are applied without reading the window bounds', () async { + await windowManager.setBounds(const Rect.fromLTWH(40, 70, 800, 600)); + expect(native.boundsReads, 0); + expect( + native.currentBounds, + const nativeapi.Rectangle(x: 40, y: 70, width: 800, height: 600), + ); + }); + + test('partial updates still read the omitted dimensions', () async { + await windowManager.setBounds(null, position: const Offset(25, 35)); + expect(native.boundsReads, 1); + expect( + native.currentBounds, + const nativeapi.Rectangle(x: 25, y: 35, width: 300, height: 200), + ); + + await windowManager.setBounds(null, size: const Size(640, 480)); + expect(native.boundsReads, 2); + expect( + native.currentBounds, + const nativeapi.Rectangle(x: 25, y: 35, width: 640, height: 480), + ); + }); + + test('full bounds win over position and size', () async { + await windowManager.setBounds( + const Rect.fromLTWH(5, 6, 700, 500), + position: const Offset(99, 99), + size: const Size(1, 1), + ); + expect(native.boundsReads, 0); + expect( + native.currentBounds, + const nativeapi.Rectangle(x: 5, y: 6, width: 700, height: 500), + ); + }); + + test('animate falls back to a plain set when refused', () async { + await windowManager.setBounds( + const Rect.fromLTWH(10, 20, 900, 650), + animate: true, + ); + expect(native.boundsReads, 0); + expect(native.animateRequests, [ + const nativeapi.Rectangle(x: 10, y: 20, width: 900, height: 650), + ]); + expect( + native.currentBounds, + const nativeapi.Rectangle(x: 10, y: 20, width: 900, height: 650), + ); + }); + + test('an empty request is a no-op', () async { + await windowManager.setBounds(null); + expect(native.boundsReads, 0); + expect( + native.currentBounds, + const nativeapi.Rectangle(x: 1, y: 2, width: 300, height: 200), + ); + }); +}