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
84 changes: 67 additions & 17 deletions lib/bootstrap/platform/desktop_platform_wrapper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ class DesktopAppWrapper extends BaseAppWrapper {
}

class _DesktopAppWrapperState extends BaseAppWrapperState<DesktopAppWrapper> with WindowListener {
bool _windowPlacementInitialized = false;
bool _windowIsMaximized = false;
bool _windowIsFullScreen = false;

@override
Future<void> platformInit() async {
if (defaultTargetPlatform == TargetPlatform.windows) {
Expand All @@ -37,17 +41,33 @@ class _DesktopAppWrapperState extends BaseAppWrapperState<DesktopAppWrapper> wit
ApplicationMenu.setUp(ApplicationMenuImp());

await WindowManager.instance.ensureInitialized();
_windowPlacementInitialized = defaultTargetPlatform != TargetPlatform.windows;
if (_windowPlacementInitialized) {
_windowIsMaximized = await windowManager.isMaximized();
_windowIsFullScreen = await windowManager.isFullScreen();
}
windowManager.addListener(this);

final packageInfo = await PackageInfo.fromPlatform();
final clientSettings = ref.read(clientSettingsProvider);
final startupArguments = ref.read(argumentsStateProvider);
await windowManager.setupFladderWindowChrome(
startupArguments,
clientSettings,
packageInfo,
);
await toggleMacTrafficLights(await windowManager.isFullScreen());
await windowManager.setupFladderWindowChrome(startupArguments, clientSettings, packageInfo);
if (defaultTargetPlatform == TargetPlatform.windows) {
unawaited(_enableWindowPlacementPersistenceAfterStartup());
}
if (defaultTargetPlatform == TargetPlatform.macOS) {
await toggleMacTrafficLights(await windowManager.isFullScreen());
}
}

Future<void> _enableWindowPlacementPersistenceAfterStartup() async {
await Future<void>.delayed(windowsWindowPlacementPersistenceDelay);
if (!mounted) return;

_windowIsMaximized = await windowManager.isMaximized();
_windowIsFullScreen = await windowManager.isFullScreen();
if (!mounted) return;
_windowPlacementInitialized = true;
}

@override
Expand All @@ -63,43 +83,73 @@ class _DesktopAppWrapperState extends BaseAppWrapperState<DesktopAppWrapper> wit
super.onWindowClose();
}

@override
void onWindowResize() async {
bool get _canPersistWindowBounds => shouldPersistWindowBounds(
startupSettled: _windowPlacementInitialized,
isFullScreen: _windowIsFullScreen,
isMaximized: _windowIsMaximized,
);

Future<void> _persistWindowSize() async {
if (!_canPersistWindowBounds) return;
final size = await windowManager.getSize();
if (!_canPersistWindowBounds) return;
ref.read(clientSettingsProvider.notifier).setWindowSize(size);
}

Future<void> _persistWindowPosition() async {
if (!_canPersistWindowBounds) return;
final position = await windowManager.getPosition();
if (!_canPersistWindowBounds) return;
ref.read(clientSettingsProvider.notifier).setWindowPosition(position);
}

@override
void onWindowResize() {
unawaited(_persistWindowSize());
super.onWindowResize();
}

@override
void onWindowResized() async {
final size = await windowManager.getSize();
ref.read(clientSettingsProvider.notifier).setWindowSize(size);
void onWindowResized() {
unawaited(_persistWindowSize());
super.onWindowResized();
}

@override
void onWindowMove() async {
final position = await windowManager.getPosition();
ref.read(clientSettingsProvider.notifier).setWindowPosition(position);
void onWindowMove() {
unawaited(_persistWindowPosition());
super.onWindowMove();
}

@override
void onWindowMoved() async {
final position = await windowManager.getPosition();
ref.read(clientSettingsProvider.notifier).setWindowPosition(position);
void onWindowMoved() {
unawaited(_persistWindowPosition());
super.onWindowMoved();
}

@override
void onWindowMaximize() {
_windowIsMaximized = true;
super.onWindowMaximize();
}

@override
void onWindowUnmaximize() {
_windowIsMaximized = false;
super.onWindowUnmaximize();
}

@override
void onWindowEnterFullScreen() {
_windowIsFullScreen = true;
ref.read(mediaPlaybackProvider.notifier).update((state) => state.copyWith(fullScreen: true));
unawaited(toggleMacTrafficLights(true));
super.onWindowEnterFullScreen();
}

@override
void onWindowLeaveFullScreen() {
_windowIsFullScreen = false;
unawaited(toggleMacTrafficLights(false));
ref.read(mediaPlaybackProvider.notifier).update((state) => state.copyWith(fullScreen: false));
super.onWindowLeaveFullScreen();
Expand Down
70 changes: 70 additions & 0 deletions lib/util/window_helper.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

Expand All @@ -8,12 +10,80 @@ import 'package:fladder/models/settings/arguments_model.dart';
import 'package:fladder/models/settings/client_settings_model.dart';
import 'package:fladder/util/string_extensions.dart';

const windowsNativeStartupBounds = Rect.fromLTWH(10, 10, 1280, 720);
const windowsExternalPlacementSettleDelay = Duration(milliseconds: 750);
const windowsWindowPlacementPersistenceDelay = Duration(milliseconds: 2500);

@visibleForTesting
bool hasExternalWindowsPlacement(Rect bounds, {double tolerance = 2}) =>
(bounds.left - windowsNativeStartupBounds.left).abs() > tolerance ||
(bounds.top - windowsNativeStartupBounds.top).abs() > tolerance ||
(bounds.width - windowsNativeStartupBounds.width).abs() > tolerance ||
(bounds.height - windowsNativeStartupBounds.height).abs() > tolerance;

@visibleForTesting
bool shouldRestoreStoredWindowsBounds({
required Rect currentBounds,
required bool isFullScreen,
required bool isMaximized,
}) =>
!isFullScreen && !isMaximized && !hasExternalWindowsPlacement(currentBounds);

bool shouldPersistWindowBounds({required bool startupSettled, required bool isFullScreen, required bool isMaximized}) =>
startupSettled && !isFullScreen && !isMaximized;

extension WindowHelperSetup on WindowManager {
Future<void> _applyWindowsWindowStateAfterSettle({
required ArgumentsModel startupArguments,
required ClientSettingsModel clientSettings,
required PackageInfo packageInfo,
}) async {
await Future<void>.delayed(windowsExternalPlacementSettleDelay);

final isCurrentlyFullScreen = await windowManager.isFullScreen();
final isCurrentlyMaximized = await windowManager.isMaximized();
final currentBounds = await windowManager.getBounds();
final shouldRestoreBounds = shouldRestoreStoredWindowsBounds(
currentBounds: currentBounds,
isFullScreen: isCurrentlyFullScreen,
isMaximized: isCurrentlyMaximized,
);

// These calls are deliberately deferred until Flutter's first frame and
// external window placement have both had time to complete. A new Windows
// window is taskbar-visible by default, so setSkipTaskbar(false) is not
// needed here.
await windowManager.setBackgroundColor(Colors.transparent);
await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
await windowManager.setTitle(packageInfo.appName.capitalize());

if (startupArguments.htpcMode && !isCurrentlyFullScreen) {
await windowManager.setFullScreen(true);
return;
}

if (shouldRestoreBounds) {
await windowManager.setSize(Size(clientSettings.size.x, clientSettings.size.y));
await windowManager.center();
}
}

Future<void> setupFladderWindowChrome(
ArgumentsModel startupArguments,
ClientSettingsModel clientSettings,
PackageInfo packageInfo,
) async {
if (defaultTargetPlatform == TargetPlatform.windows) {
unawaited(
_applyWindowsWindowStateAfterSettle(
startupArguments: startupArguments,
clientSettings: clientSettings,
packageInfo: packageInfo,
),
);
return;
}

final isFullScreen = await windowManager.isFullScreen();
final isMacDebug = defaultTargetPlatform == TargetPlatform.macOS && kDebugMode;
final shouldResizeAndShow = !isMacDebug || !isFullScreen;
Expand Down
60 changes: 60 additions & 0 deletions test/window_helper_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

import 'package:fladder/util/window_helper.dart';

void main() {
test('native startup bounds are not treated as external placement', () {
expect(hasExternalWindowsPlacement(windowsNativeStartupBounds), isFalse);
});

test('detects placement by an external window manager', () {
expect(hasExternalWindowsPlacement(const Rect.fromLTWH(-7, 0, 2574, 1393)), isTrue);
});

test('minor native rounding differences are tolerated', () {
expect(hasExternalWindowsPlacement(const Rect.fromLTWH(10, 10, 1281, 721)), isFalse);
});

test('stored bounds restore only while the native startup placement remains', () {
expect(
shouldRestoreStoredWindowsBounds(
currentBounds: windowsNativeStartupBounds,
isFullScreen: false,
isMaximized: false,
),
isTrue,
);
expect(
shouldRestoreStoredWindowsBounds(
currentBounds: const Rect.fromLTWH(-7, 0, 2574, 1393),
isFullScreen: false,
isMaximized: false,
),
isFalse,
);
expect(
shouldRestoreStoredWindowsBounds(
currentBounds: windowsNativeStartupBounds,
isFullScreen: false,
isMaximized: true,
),
isFalse,
);
expect(
shouldRestoreStoredWindowsBounds(
currentBounds: windowsNativeStartupBounds,
isFullScreen: true,
isMaximized: false,
),
isFalse,
);
});

test('window bounds persist only after startup and outside maximized modes', () {
expect(shouldPersistWindowBounds(startupSettled: false, isFullScreen: false, isMaximized: false), isFalse);
expect(shouldPersistWindowBounds(startupSettled: true, isFullScreen: false, isMaximized: false), isTrue);
expect(shouldPersistWindowBounds(startupSettled: true, isFullScreen: false, isMaximized: true), isFalse);
expect(shouldPersistWindowBounds(startupSettled: true, isFullScreen: true, isMaximized: false), isFalse);
});
}
24 changes: 22 additions & 2 deletions windows/runner/flutter_window.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,18 @@ bool FlutterWindow::OnCreate() {
});
SetChildContent(flutter_controller_->view()->GetNativeWindow());

flutter_controller_->engine()->SetNextFrameCallback([&]() {
this->Show();
flutter_controller_->engine()->SetNextFrameCallback([this]() {
const HWND window = GetHandle();
const HWND flutter_view =
flutter_controller_->view()->GetNativeWindow();

// An external window manager can reveal or resize the parent while
// Flutter is attaching its child view. Ensure the rendered child remains
// visible without resetting placement on an already-visible parent.
::ShowWindow(flutter_view, SW_SHOW);
if (window != nullptr && !::IsWindowVisible(window)) {
this->Show();
}
});

// Flutter can complete the first frame before the "show window" callback is
Expand Down Expand Up @@ -64,6 +74,16 @@ FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
std::optional<LRESULT> result =
flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
lparam);
if (message == WM_SIZE) {
// Plugins may report WM_SIZE as handled before Win32Window can resize
// the hosted FLUTTERVIEW. Always forward this message to the base host.
const LRESULT resize_result =
Win32Window::MessageHandler(hwnd, message, wparam, lparam);
if (wparam != SIZE_MINIMIZED) {
flutter_controller_->ForceRedraw();
}
return result.value_or(resize_result);
}
if (result) {
return *result;
}
Expand Down
3 changes: 3 additions & 0 deletions windows/runner/win32_window.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,9 @@ void Win32Window::SetChildContent(HWND content) {
MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
frame.bottom - frame.top, true);

// Keep the hosted Flutter surface visible even if an external window
// manager shows or resizes the parent during first-frame attachment.
ShowWindow(content, SW_SHOW);
SetFocus(child_content_);
}

Expand Down
Loading