diff --git a/examples/multiple_windows/lib/app/main_window.dart b/examples/multiple_windows/lib/app/main_window.dart index c33e1d12e48..9d42ab20c13 100644 --- a/examples/multiple_windows/lib/app/main_window.dart +++ b/examples/multiple_windows/lib/app/main_window.dart @@ -96,12 +96,14 @@ class _WindowsTable extends StatelessWidget { context: context, controller: regular, ), + DialogWindowController() => throw UnimplementedError(), }; } static String _getWindowTypeName(BaseWindowController controller) { return switch (controller) { RegularWindowController() => 'Regular', + DialogWindowController() => 'Dialog', }; } diff --git a/examples/multiple_windows/lib/app/window_content.dart b/examples/multiple_windows/lib/app/window_content.dart index 72ea7fd5990..880b5529728 100644 --- a/examples/multiple_windows/lib/app/window_content.dart +++ b/examples/multiple_windows/lib/app/window_content.dart @@ -33,6 +33,7 @@ class WindowContent extends StatelessWidget { controller: regular, child: MaterialApp(home: RegularWindowContent(window: regular)), ), + DialogWindowController() => throw UnimplementedError(), }; } } diff --git a/packages/flutter/lib/src/widgets/_window.dart b/packages/flutter/lib/src/widgets/_window.dart index 5540b8ef6b8..05ff9d36052 100644 --- a/packages/flutter/lib/src/widgets/_window.dart +++ b/packages/flutter/lib/src/widgets/_window.dart @@ -159,7 +159,9 @@ mixin class RegularWindowControllerDelegate { /// {@tool snippet} /// An example usage might look like: /// +/// /// ```dart +/// // TODO(mattkae): remove invalid_use_of_internal_member ignore comment when this API is stable. /// // ignore_for_file: invalid_use_of_internal_member /// import 'package:flutter/widgets.dart'; /// import 'package:flutter/material.dart'; @@ -181,7 +183,7 @@ mixin class RegularWindowControllerDelegate { /// {@end-tool} /// /// Children of a [RegularWindow] widget can access the [RegularWindowController] -/// via the [WindowControllerScope] inherited widget. +/// via the [WindowScope] inherited widget. /// /// {@macro flutter.widgets.windowing.experimental} @internal @@ -190,6 +192,7 @@ abstract class RegularWindowController extends BaseWindowController { /// /// Upon construction, the window is created by the platform. /// + /// {@template flutter.widgets.windowing.constraints} /// The [preferredSize] is the preferred content size of the window. /// This might not be honored by the platform. This is the size that /// the platform will try to apply to the window when it is created. In contrast, @@ -209,6 +212,7 @@ abstract class RegularWindowController extends BaseWindowController { /// /// If both [preferredSize] and [preferredConstraints] are null, /// then the platform will use its own default size for the window. + /// {@endtemplate} /// /// The [title] argument configures the window's initial title. /// If omitted, some platforms might fall back to the app's name. @@ -372,6 +376,250 @@ abstract class RegularWindowController extends BaseWindowController { void setFullscreen(bool fullscreen, {Display? display}); } +/// Delegate class for dialog window controller. +/// +/// {@macro flutter.widgets.windowing.experimental} +/// +/// See also: +/// +/// * [DialogWindowController], the controller that creates and manages dialog windows. +/// * [DialogWindow], the widget for a dialog window. +/// * [RegularWindowControllerDelegate], the delegate for regular window controllers. +@internal +mixin class DialogWindowControllerDelegate { + /// Invoked when the user attempts to close the window. + /// + /// The default implementation destroys the window. Subclasses + /// can override the behavior to delay or prevent the window from closing. + /// + /// {@macro flutter.widgets.windowing.experimental} + /// + /// See also: + /// + /// * [onWindowDestroyed], which is invoked after the window is closed. + @internal + void onWindowCloseRequested(DialogWindowController controller) { + if (!isWindowingEnabled) { + throw UnsupportedError(_kWindowingDisabledErrorMessage); + } + + controller.destroy(); + } + + /// Invoked after the window is closed. + /// + /// {@macro flutter.widgets.windowing.experimental} + /// + /// See also: + /// + /// * [onWindowCloseRequested], which is invoked when the user attempts to close the window. + @internal + void onWindowDestroyed() { + if (!isWindowingEnabled) { + throw UnsupportedError(_kWindowingDisabledErrorMessage); + } + } +} + +/// A controller for a dialog window. +/// +/// Two types of dialogs are supported: +/// * Modal dialogs: created with a non-null parent. These dialogs are modal +/// to the parent, do not have a system menu, and are not selectable from the +/// window switcher. +/// * Modeless dialogs: created with a null parent. These dialogs can be +/// minimized (but not maximized), and have a disabled close button. +/// +/// This class does not interact with the widget tree. Instead, it is typically +/// provided to the [DialogWindow] widget, which renders the content inside the +/// dialog window. +/// +/// The user of this class is responsible for managing the lifecycle of the window. +/// When the window is no longer needed, the user should call [destroy] on this +/// controller to release the resources associated with the window. +/// +/// {@tool snippet} +/// An example usage might look like: +/// +/// ```dart +/// // TODO(mattkae): remove invalid_use_of_internal_member ignore comment when this API is stable. +/// // ignore_for_file: invalid_use_of_internal_member +/// import 'package:flutter/widgets.dart'; +/// import 'package:flutter/material.dart'; +/// import 'package:flutter/src/widgets/_window.dart'; +/// +/// void main() { +/// runWidget( +/// RegularWindow( +/// controller: RegularWindowController( +/// preferredSize: const Size(800, 600), +/// preferredConstraints: const BoxConstraints(minWidth: 640, minHeight: 480), +/// title: 'Example Window', +/// ), +/// child: const MyApp() +/// ) +/// ); +/// } +/// +/// class MyApp extends StatelessWidget { +/// const MyApp({super.key}); +/// +/// @override +/// Widget build(BuildContext context) { +/// return MaterialApp( +/// home: DialogWindow( +/// controller: DialogWindowController( +/// preferredSize: const Size(400, 300), +/// parent: WindowScope.of(context), +/// title: 'Example Dialog' +/// ), +/// child: const Text('Hello, World!') +/// ) +/// ); +/// } +/// } +/// ``` +/// {@end-tool} +/// +/// Children of a [DialogWindow] widget can access the [DialogWindowController] +/// via the [WindowScope] inherited widget. +/// +/// {@macro flutter.widgets.windowing.experimental} +abstract class DialogWindowController extends BaseWindowController { + /// Creates a [DialogWindowController] with the provided properties. + /// + /// Upon construction, the window is created by the platform. + /// + /// {@macro flutter.widgets.windowing.constraints} + /// + /// The [parent] argument specifies the parent window of this dialog. + /// + /// If the [parent] is null, then the dialog is modeless. Such dialogs can + /// be minimized but not maximized. They also have a disabled close button. + /// + /// If the [parent] is non-null, then the dialog is modal to the parent. + /// Such dialogs do not have a system menu. They are also not selectable + /// from the window switcher and they are closed when the parent is closed. + /// + /// The [title] argument configures the window's initial title. + /// If omitted, some platforms might fall back to the app's name. + /// + /// The [delegate] argument can be used to listen to the window's + /// lifecycle. For example, it can be used to save state before + /// a window is closed. + /// + /// {@macro flutter.widgets.windowing.experimental} + factory DialogWindowController({ + Size? preferredSize, + BoxConstraints? preferredConstraints, + BaseWindowController? parent, + String? title, + DialogWindowControllerDelegate? delegate, + }) { + WidgetsFlutterBinding.ensureInitialized(); + final WindowingOwner owner = WidgetsBinding.instance.windowingOwner; + return owner.createDialogWindowController( + delegate: delegate ?? DialogWindowControllerDelegate(), + preferredSize: preferredSize, + preferredConstraints: preferredConstraints, + title: title, + parent: parent, + ); + } + + /// Creates an empty [DialogWindowController]. + /// + /// This method is only intended to be used by subclasses of the + /// [DialogWindowController]. + /// + /// Users who want to instantiate a new [DialogWindowController] should + /// always use the factory method to create a controller that is valid + /// for their particular platform. + /// + /// {@macro flutter.widgets.windowing.experimental} + @internal + @protected + DialogWindowController.empty(); + + /// The parent controller of this dialog, if any. + /// + /// If null, this dialog is modeless. + /// If non-null, this dialog is modal to the parent. + BaseWindowController? get parent; + + /// The current title of the window. + /// + /// This might differ from the requested title. + /// + /// {@macro flutter.widgets.windowing.experimental} + @internal + String get title; + + /// Whether the window is currently activated. + /// + /// If `true` this means that the window is currently focused and + /// can receive user input. + /// + /// {@macro flutter.widgets.windowing.experimental} + @internal + bool get isActivated; + + /// Whether or not window is currently minimized. + /// + /// {@macro flutter.widgets.windowing.experimental} + @internal + bool get isMinimized; + + /// Request change to the content size of the window. + /// + /// The [size] describes the new requested window size. If the size disagrees + /// with the current constraints placed upon the window, the platform might + /// clamp the size within the constraints. + /// + /// The platform is free to ignore this request. + /// + /// {@macro flutter.widgets.windowing.experimental} + @internal + void setSize(Size size); + + /// Request change to the constraints of the window. + /// + /// The [constraints] describes the new constraints that the window should + /// satisfy. If the constraints disagree with the current size of the window, + /// the platform might resize the window to satisfy the new constraints. + /// + /// The platform is free to ignore this request. + /// + /// {@macro flutter.widgets.windowing.experimental} + @internal + void setConstraints(BoxConstraints constraints); + + /// Request change for the window title. + /// + /// {@macro flutter.widgets.windowing.experimental} + @internal + void setTitle(String title); + + /// Requests that the window be displayed in its current size and position. + /// + /// The platform may also give the window input focus and bring it to the + /// top of the window stack. However, this behavior is platform-dependent. + /// + /// If the window is minimized, the window returns to the size and position + /// that it had before that state was applied. The window will also be + /// brought to the top of the window stack. + /// + /// {@macro flutter.widgets.windowing.experimental} + @internal + void activate(); + + /// Requests window to be minimized. + /// + /// {@macro flutter.widgets.windowing.experimental} + @internal + void setMinimized(bool minimized); +} + /// [WindowingOwner] is responsible for creating and managing window controllers. /// /// A custom implementation can be provided by setting [WidgetsBinding.windowingOwner]. @@ -394,6 +642,22 @@ abstract class WindowingOwner { String? title, }); + /// Creates a [DialogWindowController] with the provided properties. + /// + /// Most app developers should use [DialogWindowController]'s constructor + /// instead of calling this method directly. This method allows platforms + /// to inject platform-specific logic. + /// + /// {@macro flutter.widgets.windowing.experimental} + @internal + DialogWindowController createDialogWindowController({ + required DialogWindowControllerDelegate delegate, + Size? preferredSize, + BoxConstraints? preferredConstraints, + BaseWindowController? parent, + String? title, + }); + /// Returns whether the application has any top level windows created by this /// windowing owner. /// @@ -435,6 +699,17 @@ class _WindowingOwnerUnsupported extends WindowingOwner { throw UnsupportedError(errorMessage); } + @override + DialogWindowController createDialogWindowController({ + required DialogWindowControllerDelegate delegate, + Size? preferredSize, + BoxConstraints? preferredConstraints, + BaseWindowController? parent, + String? title, + }) { + throw UnsupportedError(errorMessage); + } + @override bool hasTopLevelWindows() { throw UnsupportedError(errorMessage); @@ -458,6 +733,7 @@ class _WindowingOwnerUnsupported extends WindowingOwner { /// An example usage might look like: /// /// ```dart +/// // TODO(mattkae): remove invalid_use_of_internal_member ignore comment when this API is stable. /// // ignore_for_file: invalid_use_of_internal_member /// import 'package:flutter/widgets.dart'; /// import 'package:flutter/material.dart'; @@ -523,6 +799,107 @@ class RegularWindow extends StatelessWidget { } } +/// The [DialogWindow] widget provides a way to render a dialog window in the +/// widget tree. +/// +/// The provided [controller] creates the native window that backs +/// the widget. The [child] widget is rendered into this newly created window. +/// +/// When a [DialogWindow] widget is removed from the tree, the window that was created +/// by the [controller] remains valid until the caller destroys it by calling +/// [DialogWindowController.destroy]. +/// +/// Widgets in the same tree as the [child] widget will have access to the +/// [DialogWindowController] via the [WindowScope] widget. +/// +/// {@tool snippet} +/// An example usage might look like: +/// +/// ```dart +/// // TODO(mattkae): remove invalid_use_of_internal_member ignore comment when this API is stable. +/// // ignore_for_file: invalid_use_of_internal_member +/// import 'package:flutter/widgets.dart'; +/// import 'package:flutter/material.dart'; +/// import 'package:flutter/src/widgets/_window.dart'; +/// +/// void main() { +/// runWidget( +/// RegularWindow( +/// controller: RegularWindowController( +/// preferredSize: const Size(800, 600), +/// preferredConstraints: const BoxConstraints(minWidth: 640, minHeight: 480), +/// title: 'Example Window', +/// ), +/// child: const MyApp() +/// ) +/// ); +/// } +/// +/// class MyApp extends StatelessWidget { +/// const MyApp({super.key}); +/// +/// @override +/// Widget build(BuildContext context) { +/// return MaterialApp( +/// home: DialogWindow( +/// controller: DialogWindowController( +/// preferredSize: const Size(400, 300), +/// parent: WindowScope.of(context), +/// title: 'Example Dialog' +/// ), +/// child: const Text('Hello, World!') +/// ) +/// ); +/// } +/// } +/// ``` +/// {@end-tool} +/// +/// {@macro flutter.widgets.windowing.experimental} +@internal +class DialogWindow extends StatelessWidget { + /// Creates a dialog window widget. + /// + /// The [controller] creates the native backing window into which the + /// [child] widget is rendered. + /// + /// It is up to the caller to destroy the window by calling + /// [DialogWindowController.destroy] when the window is no longer needed. + /// + /// {@macro flutter.widgets.windowing.experimental} + @internal + DialogWindow({super.key, required this.controller, required this.child}) { + if (!isWindowingEnabled) { + throw UnsupportedError(_kWindowingDisabledErrorMessage); + } + } + + /// Controller for this widget. + /// + /// {@macro flutter.widgets.windowing.experimental} + @internal + final DialogWindowController controller; + + /// The content rendered into this window. + /// + /// {@macro flutter.widgets.windowing.experimental} + @internal + final Widget child; + + /// {@macro flutter.widgets.windowing.experimental} + @internal + @override + Widget build(BuildContext context) { + return ListenableBuilder( + listenable: controller, + builder: (BuildContext context, Widget? widget) => WindowScope( + controller: controller, + child: View(view: controller.rootView, child: child), + ), + ); + } +} + enum _WindowControllerAspect { contentSize, title, activated, maximized, minimized, fullscreen } /// Provides descendants with access to the [BaseWindowController] associated with @@ -537,6 +914,7 @@ enum _WindowControllerAspect { contentSize, title, activated, maximized, minimiz /// See also: /// /// * [RegularWindow], the widget to create a regular window. +/// * [DialogWindow], the widget to create a dialog window. @internal class WindowScope extends InheritedModel<_WindowControllerAspect> { /// Creates a new [WindowScope]. @@ -565,8 +943,8 @@ class WindowScope extends InheritedModel<_WindowControllerAspect> { /// Returns the [BaseWindowController] for the window that hosts the given context. /// - /// {@template flutter.widgets.windowing.windowControllerScope.of} - /// If there is no [WindowControllerScope] in scope, this method + /// {@template flutter.widgets.windowing.WindowScope.of} + /// If there is no [WindowScope] in scope, this method /// will throw a [TypeError] exception in release builds, and throws /// a descriptive [FlutterError] in debug builds. /// @@ -580,7 +958,9 @@ class WindowScope extends InheritedModel<_WindowControllerAspect> { /// See also: /// /// * [RegularWindowController], the controller for regular top-level windows. + /// * [DialogWindowController], the controller for dialog windows. /// * [RegularWindow], the widget for a regular window. + /// * [DialogWindow], the widget for a dialog window. /// * [maybeOf], which doesn't throw or assert if it doesn't find a /// [WindowScope] ancestor. It returns null instead. @internal @@ -595,7 +975,9 @@ class WindowScope extends InheritedModel<_WindowControllerAspect> { /// See also: /// /// * [RegularWindowController], the controller for regular top-level windows. + /// * [DialogWindowController], the controller for dialog windows. /// * [RegularWindow], the widget for a regular window. + /// * [DialogWindow], the widget for a dialog window. /// * [of], which will throw if it doesn't find a [WindowScope] ancestor, /// instead of returning null. @internal @@ -648,6 +1030,7 @@ class WindowScope extends InheritedModel<_WindowControllerAspect> { final BaseWindowController controller = _of(context, _WindowControllerAspect.title); return switch (controller) { RegularWindowController() => controller.title, + DialogWindowController() => controller.title, }; } @@ -668,6 +1051,7 @@ class WindowScope extends InheritedModel<_WindowControllerAspect> { return switch (controller) { RegularWindowController() => controller.title, + DialogWindowController() => controller.title, }; } @@ -689,6 +1073,7 @@ class WindowScope extends InheritedModel<_WindowControllerAspect> { final BaseWindowController controller = _of(context, _WindowControllerAspect.activated); return switch (controller) { RegularWindowController() => controller.isActivated, + DialogWindowController() => controller.isActivated, }; } @@ -710,6 +1095,7 @@ class WindowScope extends InheritedModel<_WindowControllerAspect> { return switch (controller) { RegularWindowController() => controller.isActivated, + DialogWindowController() => controller.isActivated, }; } @@ -731,6 +1117,7 @@ class WindowScope extends InheritedModel<_WindowControllerAspect> { final BaseWindowController controller = _of(context, _WindowControllerAspect.minimized); return switch (controller) { RegularWindowController() => controller.isMinimized, + DialogWindowController() => controller.isMinimized, }; } @@ -752,6 +1139,7 @@ class WindowScope extends InheritedModel<_WindowControllerAspect> { return switch (controller) { RegularWindowController() => controller.isMinimized, + DialogWindowController() => controller.isMinimized, }; } @@ -773,6 +1161,7 @@ class WindowScope extends InheritedModel<_WindowControllerAspect> { final BaseWindowController controller = _of(context, _WindowControllerAspect.maximized); return switch (controller) { RegularWindowController() => controller.isMaximized, + DialogWindowController() => false, }; } @@ -794,6 +1183,7 @@ class WindowScope extends InheritedModel<_WindowControllerAspect> { return switch (controller) { RegularWindowController() => controller.isMaximized, + DialogWindowController() => false, }; } @@ -816,6 +1206,7 @@ class WindowScope extends InheritedModel<_WindowControllerAspect> { return switch (controller) { RegularWindowController() => controller.isFullscreen, + DialogWindowController() => false, }; } @@ -837,6 +1228,7 @@ class WindowScope extends InheritedModel<_WindowControllerAspect> { return switch (controller) { RegularWindowController() => controller.isFullscreen, + DialogWindowController() => false, }; } @@ -898,26 +1290,34 @@ class WindowScope extends InheritedModel<_WindowControllerAspect> { _WindowControllerAspect.title => switch (controller) { final RegularWindowController regular => regular.title != (oldWidget.controller as RegularWindowController).title, + final DialogWindowController dialog => + dialog.title != (oldWidget.controller as DialogWindowController).title, }, _WindowControllerAspect.activated => switch (controller) { final RegularWindowController regular => regular.isActivated != (oldWidget.controller as RegularWindowController).isActivated, + final DialogWindowController dialog => + dialog.isActivated != (oldWidget.controller as DialogWindowController).isActivated, }, _WindowControllerAspect.maximized => switch (controller) { final RegularWindowController regular => regular.isMaximized != (oldWidget.controller as RegularWindowController).isMaximized, + DialogWindowController() => false, }, _WindowControllerAspect.minimized => switch (controller) { final RegularWindowController regular => regular.isMinimized != (oldWidget.controller as RegularWindowController).isMinimized, + final DialogWindowController dialog => + dialog.isMinimized != (oldWidget.controller as DialogWindowController).isMinimized, }, _WindowControllerAspect.fullscreen => switch (controller) { final RegularWindowController regular => regular.isFullscreen != (oldWidget.controller as RegularWindowController).isFullscreen, + DialogWindowController() => false, }, }, ); diff --git a/packages/flutter/lib/src/widgets/_window_win32.dart b/packages/flutter/lib/src/widgets/_window_win32.dart index 1d6a81c3ccf..46647a630a6 100644 --- a/packages/flutter/lib/src/widgets/_window_win32.dart +++ b/packages/flutter/lib/src/widgets/_window_win32.dart @@ -148,6 +148,18 @@ class WindowingOwnerWin32 extends WindowingOwner { ); } + @internal + @override + DialogWindowController createDialogWindowController({ + required DialogWindowControllerDelegate delegate, + Size? preferredSize, + BoxConstraints? preferredConstraints, + BaseWindowController? parent, + String? title, + }) { + throw UnimplementedError('Dialog windows are not yet implemented on Windows.'); + } + /// Register a new [WindowsMessageHandler]. /// /// The handler will be triggered for unhandled messages for all top level diff --git a/packages/flutter/test/widgets/windowing_test.dart b/packages/flutter/test/widgets/windowing_test.dart index ca8995bed2d..c14aa3b833a 100644 --- a/packages/flutter/test/widgets/windowing_test.dart +++ b/packages/flutter/test/widgets/windowing_test.dart @@ -7,6 +7,9 @@ import 'package:flutter/src/foundation/_features.dart' show isWindowingEnabled; import 'package:flutter/src/widgets/_window.dart' show BaseWindowController, + DialogWindow, + DialogWindowController, + DialogWindowControllerDelegate, RegularWindow, RegularWindowController, RegularWindowControllerDelegate, @@ -18,8 +21,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'multi_view_testing.dart'; -class _StubWindowController extends RegularWindowController { - _StubWindowController(WidgetTester tester) : super.empty() { +class _StubRegularWindowController extends RegularWindowController { + _StubRegularWindowController(WidgetTester tester) : super.empty() { rootView = FakeView(tester.view); } @@ -66,6 +69,45 @@ class _StubWindowController extends RegularWindowController { void destroy() {} } +class _StubDialogWindowController extends DialogWindowController { + _StubDialogWindowController(WidgetTester tester) : super.empty() { + rootView = FakeView(tester.view); + } + + @override + BaseWindowController? get parent => null; + + @override + Size get contentSize => Size.zero; + + @override + String get title => 'Stub Window'; + + @override + bool get isActivated => true; + + @override + bool get isMinimized => false; + + @override + void setSize(Size size) {} + + @override + void setConstraints(BoxConstraints constraints) {} + + @override + void setTitle(String title) {} + + @override + void activate() {} + + @override + void setMinimized(bool minimized) {} + + @override + void destroy() {} +} + void main() { group('Windowing', () { group('isWindowingEnabled is false', () { @@ -86,6 +128,14 @@ void main() { ); }); + test('default WindowingOwner throws when accessing createDialogWindowController', () { + final WindowingOwner owner = createDefaultWindowingOwner(); + expect( + () => owner.createDialogWindowController(delegate: DialogWindowControllerDelegate()), + throwsUnsupportedError, + ); + }); + test('default WindowingOwner throws when accessing hasTopLevelWindows', () { final WindowingOwner owner = createDefaultWindowingOwner(); expect(() => owner.hasTopLevelWindows(), throwsUnsupportedError); @@ -93,7 +143,20 @@ void main() { testWidgets('RegularWindow throws UnsupportedError', (WidgetTester tester) async { expect( - () => RegularWindow(controller: _StubWindowController(tester), child: const Text('Test')), + () => RegularWindow( + controller: _StubRegularWindowController(tester), + child: const Text('Test'), + ), + throwsUnsupportedError, + ); + }); + + testWidgets('DialogWindow throws UnsupportedError', (WidgetTester tester) async { + expect( + () => DialogWindow( + controller: _StubDialogWindowController(tester), + child: const Text('Test'), + ), throwsUnsupportedError, ); }); @@ -112,7 +175,7 @@ void main() { }); testWidgets('RegularWindow does not throw', (WidgetTester tester) async { - final _StubWindowController controller = _StubWindowController(tester); + final _StubRegularWindowController controller = _StubRegularWindowController(tester); addTearDown(controller.dispose); await tester.pumpWidget( wrapWithView: false, @@ -120,8 +183,17 @@ void main() { ); }); - testWidgets('Can access WindowScope.of', (WidgetTester tester) async { - final _StubWindowController controller = _StubWindowController(tester); + testWidgets('Dialog does not throw', (WidgetTester tester) async { + final _StubDialogWindowController controller = _StubDialogWindowController(tester); + addTearDown(controller.dispose); + await tester.pumpWidget( + wrapWithView: false, + DialogWindow(controller: controller, child: Container()), + ); + }); + + testWidgets('Can access WindowScope.of for regular windows', (WidgetTester tester) async { + final _StubRegularWindowController controller = _StubRegularWindowController(tester); addTearDown(controller.dispose); await tester.pumpWidget( wrapWithView: false, @@ -138,8 +210,28 @@ void main() { ); }); - testWidgets('Can access WindowScope.maybeOf', (WidgetTester tester) async { - final _StubWindowController controller = _StubWindowController(tester); + testWidgets('Can access WindowScope.of for dialog windows', (WidgetTester tester) async { + final _StubDialogWindowController controller = _StubDialogWindowController(tester); + addTearDown(controller.dispose); + await tester.pumpWidget( + wrapWithView: false, + DialogWindow( + controller: controller, + child: Builder( + builder: (BuildContext context) { + final BaseWindowController scope = WindowScope.of(context); + expect(scope, isA()); + return const SizedBox.shrink(); + }, + ), + ), + ); + }); + + testWidgets('Can access WindowScope.maybeOf for regular windows', ( + WidgetTester tester, + ) async { + final _StubRegularWindowController controller = _StubRegularWindowController(tester); addTearDown(controller.dispose); await tester.pumpWidget( wrapWithView: false, @@ -156,8 +248,28 @@ void main() { ); }); - testWidgets('Can access WindowScope.contentSizeOf', (WidgetTester tester) async { - final _StubWindowController controller = _StubWindowController(tester); + testWidgets('Can access WindowScope.maybeOf for dialog windows', (WidgetTester tester) async { + final _StubDialogWindowController controller = _StubDialogWindowController(tester); + addTearDown(controller.dispose); + await tester.pumpWidget( + wrapWithView: false, + DialogWindow( + controller: controller, + child: Builder( + builder: (BuildContext context) { + final BaseWindowController? scope = WindowScope.maybeOf(context); + expect(scope, isA()); + return const SizedBox.shrink(); + }, + ), + ), + ); + }); + + testWidgets('Can access WindowScope.contentSizeOf for regular windows', ( + WidgetTester tester, + ) async { + final _StubRegularWindowController controller = _StubRegularWindowController(tester); addTearDown(controller.dispose); await tester.pumpWidget( wrapWithView: false, @@ -174,8 +286,30 @@ void main() { ); }); - testWidgets('Can access WindowScope.maybeContentSizeOf', (WidgetTester tester) async { - final _StubWindowController controller = _StubWindowController(tester); + testWidgets('Can access WindowScope.contentSizeOf for dialog windows', ( + WidgetTester tester, + ) async { + final _StubDialogWindowController controller = _StubDialogWindowController(tester); + addTearDown(controller.dispose); + await tester.pumpWidget( + wrapWithView: false, + DialogWindow( + controller: controller, + child: Builder( + builder: (BuildContext context) { + final Size size = WindowScope.contentSizeOf(context); + expect(size, equals(Size.zero)); + return const SizedBox.shrink(); + }, + ), + ), + ); + }); + + testWidgets('Can access WindowScope.maybeContentSizeOf for regular windows', ( + WidgetTester tester, + ) async { + final _StubRegularWindowController controller = _StubRegularWindowController(tester); addTearDown(controller.dispose); await tester.pumpWidget( wrapWithView: false, @@ -192,8 +326,30 @@ void main() { ); }); - testWidgets('Can access WindowScope.titleOf', (WidgetTester tester) async { - final _StubWindowController controller = _StubWindowController(tester); + testWidgets('Can access WindowScope.maybeContentSizeOf for dialog windows', ( + WidgetTester tester, + ) async { + final _StubDialogWindowController controller = _StubDialogWindowController(tester); + addTearDown(controller.dispose); + await tester.pumpWidget( + wrapWithView: false, + DialogWindow( + controller: controller, + child: Builder( + builder: (BuildContext context) { + final Size? size = WindowScope.maybeContentSizeOf(context); + expect(size, equals(Size.zero)); + return const SizedBox.shrink(); + }, + ), + ), + ); + }); + + testWidgets('Can access WindowScope.titleOf for regular windows', ( + WidgetTester tester, + ) async { + final _StubRegularWindowController controller = _StubRegularWindowController(tester); addTearDown(controller.dispose); await tester.pumpWidget( wrapWithView: false, @@ -210,8 +366,28 @@ void main() { ); }); - testWidgets('Can access WindowScope.maybeTitleOf', (WidgetTester tester) async { - final _StubWindowController controller = _StubWindowController(tester); + testWidgets('Can access WindowScope.titleOf for dialog windows', (WidgetTester tester) async { + final _StubDialogWindowController controller = _StubDialogWindowController(tester); + addTearDown(controller.dispose); + await tester.pumpWidget( + wrapWithView: false, + DialogWindow( + controller: controller, + child: Builder( + builder: (BuildContext context) { + final String title = WindowScope.titleOf(context); + expect(title, equals('Stub Window')); + return const SizedBox.shrink(); + }, + ), + ), + ); + }); + + testWidgets('Can access WindowScope.maybeTitleOf for regular windows', ( + WidgetTester tester, + ) async { + final _StubRegularWindowController controller = _StubRegularWindowController(tester); addTearDown(controller.dispose); await tester.pumpWidget( wrapWithView: false, @@ -228,8 +404,30 @@ void main() { ); }); - testWidgets('Can access WindowScope.isActivatedOf', (WidgetTester tester) async { - final _StubWindowController controller = _StubWindowController(tester); + testWidgets('Can access WindowScope.maybeTitleOf for dialog windows', ( + WidgetTester tester, + ) async { + final _StubDialogWindowController controller = _StubDialogWindowController(tester); + addTearDown(controller.dispose); + await tester.pumpWidget( + wrapWithView: false, + DialogWindow( + controller: controller, + child: Builder( + builder: (BuildContext context) { + final String? title = WindowScope.maybeTitleOf(context); + expect(title, equals('Stub Window')); + return const SizedBox.shrink(); + }, + ), + ), + ); + }); + + testWidgets('Can access WindowScope.isActivatedOf for regular windows', ( + WidgetTester tester, + ) async { + final _StubRegularWindowController controller = _StubRegularWindowController(tester); addTearDown(controller.dispose); await tester.pumpWidget( wrapWithView: false, @@ -246,8 +444,30 @@ void main() { ); }); - testWidgets('Can access WindowScope.maybeIsActivatedOf', (WidgetTester tester) async { - final _StubWindowController controller = _StubWindowController(tester); + testWidgets('Can access WindowScope.isActivatedOf for dialog windows', ( + WidgetTester tester, + ) async { + final _StubDialogWindowController controller = _StubDialogWindowController(tester); + addTearDown(controller.dispose); + await tester.pumpWidget( + wrapWithView: false, + DialogWindow( + controller: controller, + child: Builder( + builder: (BuildContext context) { + final bool isActivated = WindowScope.isActivatedOf(context); + expect(isActivated, equals(true)); + return const SizedBox.shrink(); + }, + ), + ), + ); + }); + + testWidgets('Can access WindowScope.maybeIsActivatedOf for regular windows', ( + WidgetTester tester, + ) async { + final _StubRegularWindowController controller = _StubRegularWindowController(tester); addTearDown(controller.dispose); await tester.pumpWidget( wrapWithView: false, @@ -264,8 +484,30 @@ void main() { ); }); - testWidgets('Can access WindowScope.isMinimizedOf', (WidgetTester tester) async { - final _StubWindowController controller = _StubWindowController(tester); + testWidgets('Can access WindowScope.maybeIsActivatedOf for dialog windows', ( + WidgetTester tester, + ) async { + final _StubDialogWindowController controller = _StubDialogWindowController(tester); + addTearDown(controller.dispose); + await tester.pumpWidget( + wrapWithView: false, + DialogWindow( + controller: controller, + child: Builder( + builder: (BuildContext context) { + final bool? isActivated = WindowScope.maybeIsActivatedOf(context); + expect(isActivated, equals(true)); + return const SizedBox.shrink(); + }, + ), + ), + ); + }); + + testWidgets('Can access WindowScope.isMinimizedOf for regular windows', ( + WidgetTester tester, + ) async { + final _StubRegularWindowController controller = _StubRegularWindowController(tester); addTearDown(controller.dispose); await tester.pumpWidget( wrapWithView: false, @@ -282,8 +524,30 @@ void main() { ); }); - testWidgets('Can access WindowScope.maybeIsMinimizedOf', (WidgetTester tester) async { - final _StubWindowController controller = _StubWindowController(tester); + testWidgets('Can access WindowScope.isMinimizedOf for dialog windows', ( + WidgetTester tester, + ) async { + final _StubDialogWindowController controller = _StubDialogWindowController(tester); + addTearDown(controller.dispose); + await tester.pumpWidget( + wrapWithView: false, + DialogWindow( + controller: controller, + child: Builder( + builder: (BuildContext context) { + final bool isMinimized = WindowScope.isMinimizedOf(context); + expect(isMinimized, equals(false)); + return const SizedBox.shrink(); + }, + ), + ), + ); + }); + + testWidgets('Can access WindowScope.maybeIsMinimizedOf for regular windows', ( + WidgetTester tester, + ) async { + final _StubRegularWindowController controller = _StubRegularWindowController(tester); addTearDown(controller.dispose); await tester.pumpWidget( wrapWithView: false, @@ -300,8 +564,30 @@ void main() { ); }); - testWidgets('Can access WindowScope.isMaximizedOf', (WidgetTester tester) async { - final _StubWindowController controller = _StubWindowController(tester); + testWidgets('Can access WindowScope.maybeIsMinimizedOf for dialog windows', ( + WidgetTester tester, + ) async { + final _StubDialogWindowController controller = _StubDialogWindowController(tester); + addTearDown(controller.dispose); + await tester.pumpWidget( + wrapWithView: false, + DialogWindow( + controller: controller, + child: Builder( + builder: (BuildContext context) { + final bool? isMinimized = WindowScope.maybeIsMinimizedOf(context); + expect(isMinimized, equals(false)); + return const SizedBox.shrink(); + }, + ), + ), + ); + }); + + testWidgets('Can access WindowScope.isMaximizedOf for regular windows', ( + WidgetTester tester, + ) async { + final _StubRegularWindowController controller = _StubRegularWindowController(tester); addTearDown(controller.dispose); await tester.pumpWidget( wrapWithView: false, @@ -318,8 +604,30 @@ void main() { ); }); - testWidgets('Can access WindowScope.maybeIsMaximizedOf', (WidgetTester tester) async { - final _StubWindowController controller = _StubWindowController(tester); + testWidgets('Can access WindowScope.isMaximizedOf for dialog windows', ( + WidgetTester tester, + ) async { + final _StubDialogWindowController controller = _StubDialogWindowController(tester); + addTearDown(controller.dispose); + await tester.pumpWidget( + wrapWithView: false, + DialogWindow( + controller: controller, + child: Builder( + builder: (BuildContext context) { + final bool isMaximized = WindowScope.isMaximizedOf(context); + expect(isMaximized, equals(false)); + return const SizedBox.shrink(); + }, + ), + ), + ); + }); + + testWidgets('Can access WindowScope.maybeIsMaximizedOf for regular windows', ( + WidgetTester tester, + ) async { + final _StubRegularWindowController controller = _StubRegularWindowController(tester); addTearDown(controller.dispose); await tester.pumpWidget( wrapWithView: false, @@ -336,8 +644,30 @@ void main() { ); }); - testWidgets('Can access WindowScope.isFullscreenOf', (WidgetTester tester) async { - final _StubWindowController controller = _StubWindowController(tester); + testWidgets('Can access WindowScope.maybeIsMaximizedOf for dialog windows', ( + WidgetTester tester, + ) async { + final _StubDialogWindowController controller = _StubDialogWindowController(tester); + addTearDown(controller.dispose); + await tester.pumpWidget( + wrapWithView: false, + DialogWindow( + controller: controller, + child: Builder( + builder: (BuildContext context) { + final bool? isMaximized = WindowScope.maybeIsMaximizedOf(context); + expect(isMaximized, equals(false)); + return const SizedBox.shrink(); + }, + ), + ), + ); + }); + + testWidgets('Can access WindowScope.isFullscreenOf for regular windows', ( + WidgetTester tester, + ) async { + final _StubRegularWindowController controller = _StubRegularWindowController(tester); addTearDown(controller.dispose); await tester.pumpWidget( wrapWithView: false, @@ -354,8 +684,30 @@ void main() { ); }); - testWidgets('Can access WindowScope.maybeIsFullscreenOf', (WidgetTester tester) async { - final _StubWindowController controller = _StubWindowController(tester); + testWidgets('Can access WindowScope.isFullscreenOf for dialog windows', ( + WidgetTester tester, + ) async { + final _StubDialogWindowController controller = _StubDialogWindowController(tester); + addTearDown(controller.dispose); + await tester.pumpWidget( + wrapWithView: false, + DialogWindow( + controller: controller, + child: Builder( + builder: (BuildContext context) { + final bool isFullscreen = WindowScope.isFullscreenOf(context); + expect(isFullscreen, equals(false)); + return const SizedBox.shrink(); + }, + ), + ), + ); + }); + + testWidgets('Can access WindowScope.maybeIsFullscreenOf for regular windows', ( + WidgetTester tester, + ) async { + final _StubRegularWindowController controller = _StubRegularWindowController(tester); addTearDown(controller.dispose); await tester.pumpWidget( wrapWithView: false, @@ -371,6 +723,26 @@ void main() { ), ); }); + + testWidgets('Can access WindowScope.maybeIsFullscreenOf for dialog windows', ( + WidgetTester tester, + ) async { + final _StubDialogWindowController controller = _StubDialogWindowController(tester); + addTearDown(controller.dispose); + await tester.pumpWidget( + wrapWithView: false, + DialogWindow( + controller: controller, + child: Builder( + builder: (BuildContext context) { + final bool? isFullscreen = WindowScope.maybeIsFullscreenOf(context); + expect(isFullscreen, equals(false)); + return const SizedBox.shrink(); + }, + ), + ), + ); + }); }); }); }