Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 25 additions & 10 deletions lib/src/legacy/window_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
Expand Down
113 changes: 113 additions & 0 deletions test/legacy_set_bounds_test.dart
Original file line number Diff line number Diff line change
@@ -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<nativeapi.Rectangle> 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),
);
});
}