diff --git a/examples/multi_window_ref_app/lib/app/main_window.dart b/examples/multi_window_ref_app/lib/app/main_window.dart index 19c4f1d63ed44..cef69dbe13d0b 100644 --- a/examples/multi_window_ref_app/lib/app/main_window.dart +++ b/examples/multi_window_ref_app/lib/app/main_window.dart @@ -1,32 +1,27 @@ import 'package:flutter/material.dart'; +import 'package:multi_window_ref_app/app/window_controller_render.dart'; -import 'regular_window.dart'; import 'window_settings.dart'; import 'window_settings_dialog.dart'; +import 'window_manager_model.dart'; class MainWindow extends StatefulWidget { - const MainWindow({super.key}); + MainWindow({super.key, required WindowController mainController}) { + _windowManagerModel.add( + KeyedWindowController(isMainWindow: true, controller: mainController)); + } + + final WindowManagerModel _windowManagerModel = WindowManagerModel(); + final WindowSettings _settings = WindowSettings(); @override State createState() => _MainWindowState(); } class _MainWindowState extends State { - int selectedRowIndex = -1; - final List _managedWindows = []; - @override Widget build(BuildContext context) { - List getWindowsInTree(List windows) { - return windows - .expand((window) => [window, ...getWindowsInTree(window.children)]) - .toList(); - } - - final List windows = - getWindowsInTree(MultiWindowAppContext.of(context)!.windows); - - final widget = Scaffold( + final child = Scaffold( appBar: AppBar( title: const Text('Multi Window Reference App'), ), @@ -38,11 +33,7 @@ class _MainWindowState extends State { child: SingleChildScrollView( scrollDirection: Axis.vertical, child: _ActiveWindowsTable( - windows: windows, - selectedRowIndex: selectedRowIndex, - onSelectedRowIndexChanged: (int index) => - setState(() => selectedRowIndex = index), - ), + windowManagerModel: widget._windowManagerModel), ), ), Expanded( @@ -50,13 +41,14 @@ class _MainWindowState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _WindowCreatorCard( - selectedWindow: selectedRowIndex < 0 || - selectedRowIndex >= windows.length - ? null - : windows[selectedRowIndex], - onDialogOpened: (window) => _managedWindows.add(window), - onDialogClosed: (window) => _managedWindows.remove(window)), + ListenableBuilder( + listenable: widget._windowManagerModel, + builder: (BuildContext context, Widget? child) { + return _WindowCreatorCard( + selectedWindow: widget._windowManagerModel.selected, + windowManagerModel: widget._windowManagerModel, + windowSettings: widget._settings); + }) ], ), ), @@ -64,136 +56,143 @@ class _MainWindowState extends State { ), ); - final window = WindowContext.of(context)!.window; - final List childViews = []; - for (final Window childWindow in window.children) { - if (!_shouldRenderWindow(childWindow)) { - continue; - } - - childViews.add(View( - view: childWindow.view, - child: WindowContext( - window: childWindow, - child: childWindow.builder(context), - ), - )); - } - - return ViewAnchor(view: ViewCollection(views: childViews), child: widget); - } - - bool _shouldRenderWindow(Window window) { - return !_managedWindows.contains(window); + return ViewAnchor( + view: ListenableBuilder( + listenable: widget._windowManagerModel, + builder: (BuildContext context, Widget? _) { + final List childViews = []; + for (final KeyedWindowController controller + in widget._windowManagerModel.windows) { + if (controller.parent == null && !controller.isMainWindow) { + childViews.add(WindowControllerRender( + controller: controller.controller, + key: controller.key, + windowSettings: widget._settings, + windowManagerModel: widget._windowManagerModel, + onDestroyed: () => + widget._windowManagerModel.remove(controller), + onError: () => + widget._windowManagerModel.remove(controller), + )); + } + } + + return ViewCollection(views: childViews); + }), + child: child); } } class _ActiveWindowsTable extends StatelessWidget { - const _ActiveWindowsTable( - {required this.windows, - required this.selectedRowIndex, - required this.onSelectedRowIndexChanged}); + const _ActiveWindowsTable({required this.windowManagerModel}); - final List windows; - final int selectedRowIndex; - final void Function(int) onSelectedRowIndexChanged; + final WindowManagerModel windowManagerModel; @override Widget build(BuildContext context) { - return DataTable( - showBottomBorder: true, - onSelectAll: (selected) { - onSelectedRowIndexChanged(-1); - }, - columns: const [ - DataColumn( - label: SizedBox( - width: 20, - child: Text( - 'ID', - style: TextStyle( - fontSize: 16, + return ListenableBuilder( + listenable: windowManagerModel, + builder: (BuildContext context, Widget? widget) { + return DataTable( + showBottomBorder: true, + onSelectAll: (selected) { + windowManagerModel.select(null); + }, + columns: const [ + DataColumn( + label: SizedBox( + width: 20, + child: Text( + 'ID', + style: TextStyle( + fontSize: 16, + ), + ), + ), ), - ), - ), - ), - DataColumn( - label: SizedBox( - width: 120, - child: Text( - 'Type', - style: TextStyle( - fontSize: 16, + DataColumn( + label: SizedBox( + width: 120, + child: Text( + 'Type', + style: TextStyle( + fontSize: 16, + ), + ), + ), ), - ), - ), - ), - DataColumn( - label: SizedBox( - width: 20, - child: Text(''), - ), - numeric: true), - ], - rows: windows.asMap().entries.map((indexedEntry) { - final index = indexedEntry.key; - final Window entry = indexedEntry.value; - final window = entry; - final viewId = window.view.viewId; - final archetype = window.archetype; - final isSelected = selectedRowIndex == index; - - return DataRow( - color: WidgetStateColor.resolveWith((states) { - if (states.contains(WidgetState.selected)) { - return Theme.of(context).colorScheme.primary.withOpacity(0.08); - } - return Colors.transparent; - }), - selected: isSelected, - onSelectChanged: (selected) { - if (selected != null) { - onSelectedRowIndexChanged(selected ? index : -1); - } - }, - cells: [ - DataCell( - Text('$viewId'), - ), - DataCell( - Text(archetype.toString().replaceFirst('WindowArchetype.', '')), - ), - DataCell( - IconButton( - icon: const Icon(Icons.delete_outlined), - onPressed: () { - destroyWindow(context, window); + DataColumn( + label: SizedBox( + width: 20, + child: Text(''), + ), + numeric: true), + ], + rows: (windowManagerModel.windows) + .map((KeyedWindowController controller) { + return DataRow( + key: controller.key, + color: WidgetStateColor.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return Theme.of(context) + .colorScheme + .primary + .withAlpha(20); + } + return Colors.transparent; + }), + selected: controller.controller == windowManagerModel.selected, + onSelectChanged: (selected) { + if (selected != null) { + windowManagerModel.select( + selected ? controller.controller.view?.viewId : null); + } }, - ), - ), - ], - ); - }).toList(), - ); + cells: [ + DataCell( + ListenableBuilder( + listenable: controller.controller, + builder: (BuildContext context, Widget? _) => Text( + controller.controller.view != null + ? '${controller.controller.view?.viewId}' + : 'Loading...')), + ), + DataCell( + ListenableBuilder( + listenable: controller.controller, + builder: (BuildContext context, Widget? _) => Text( + controller.controller.type + .toString() + .replaceFirst('WindowArchetype.', ''))), + ), + DataCell( + ListenableBuilder( + listenable: controller.controller, + builder: (BuildContext context, Widget? _) => + IconButton( + icon: const Icon(Icons.delete_outlined), + onPressed: () async { + await controller.controller.destroy(); + }, + )), + ), + ], + ); + }).toList(), + ); + }); } } -class _WindowCreatorCard extends StatefulWidget { +class _WindowCreatorCard extends StatelessWidget { const _WindowCreatorCard( {required this.selectedWindow, - required this.onDialogOpened, - required this.onDialogClosed}); - - final Window? selectedWindow; - final void Function(Window) onDialogOpened; - final void Function(Window) onDialogClosed; - - @override - State createState() => _WindowCreatorCardState(); -} + required this.windowManagerModel, + required this.windowSettings}); -class _WindowCreatorCardState extends State<_WindowCreatorCard> { - WindowSettings _settings = WindowSettings(); + final WindowController? selectedWindow; + final WindowManagerModel windowManagerModel; + final WindowSettings windowSettings; @override Widget build(BuildContext context) { @@ -219,12 +218,8 @@ class _WindowCreatorCardState extends State<_WindowCreatorCard> { children: [ OutlinedButton( onPressed: () async { - await createRegular( - context: context, - size: _settings.regularSize, - builder: (BuildContext context) { - return const MaterialApp(home: RegularWindowContent()); - }); + windowManagerModel.add(KeyedWindowController( + controller: RegularWindowController())); }, child: const Text('Regular'), ), @@ -234,15 +229,7 @@ class _WindowCreatorCardState extends State<_WindowCreatorCard> { child: TextButton( child: const Text('SETTINGS'), onPressed: () { - windowSettingsDialog(context, _settings, - widget.onDialogOpened, widget.onDialogClosed) - .then( - (WindowSettings? settings) { - if (settings != null) { - _settings = settings; - } - }, - ); + windowSettingsDialog(context, windowSettings); }, ), ), diff --git a/examples/multi_window_ref_app/lib/app/regular_window.dart b/examples/multi_window_ref_app/lib/app/regular_window.dart deleted file mode 100644 index e564b8fe9f351..0000000000000 --- a/examples/multi_window_ref_app/lib/app/regular_window.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:flutter/material.dart'; - -class RegularWindowContent extends StatelessWidget { - const RegularWindowContent({super.key}); - - @override - Widget build(BuildContext context) { - final Window window = WindowContext.of(context)!.window; - final widget = Scaffold( - appBar: AppBar(title: Text('${window.archetype}')), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - ElevatedButton( - onPressed: () async { - await createRegular( - context: context, - size: const Size(400, 300), - builder: (BuildContext context) => - const MaterialApp(home: RegularWindowContent())); - }, - child: const Text('Create Regular Window'), - ), - const SizedBox(height: 20), - Text( - 'View #${window.view.viewId}\n' - 'Parent View: ${window.parent?.view.viewId}\n' - 'Logical Size: ${window.size.width}\u00D7${window.size.height}\n' - 'DPR: ${MediaQuery.of(context).devicePixelRatio}', - textAlign: TextAlign.center, - ), - ], - ), - ), - ); - - final List childViews = window.children.map((childWindow) { - return View( - view: childWindow.view, - child: WindowContext( - window: childWindow, - child: childWindow.builder(context), - ), - ); - }).toList(); - - return ViewAnchor(view: ViewCollection(views: childViews), child: widget); - } -} diff --git a/examples/multi_window_ref_app/lib/app/regular_window_content.dart b/examples/multi_window_ref_app/lib/app/regular_window_content.dart new file mode 100644 index 0000000000000..45089e878f0a8 --- /dev/null +++ b/examples/multi_window_ref_app/lib/app/regular_window_content.dart @@ -0,0 +1,189 @@ +import 'package:flutter/material.dart'; +import 'package:multi_window_ref_app/app/window_controller_render.dart'; +import 'package:multi_window_ref_app/app/window_manager_model.dart'; +import 'package:multi_window_ref_app/app/window_settings.dart'; +import 'dart:math'; +import 'package:vector_math/vector_math_64.dart'; + +class RegularWindowContent extends StatefulWidget { + const RegularWindowContent( + {super.key, + required this.window, + required this.windowSettings, + required this.windowManagerModel}); + + final RegularWindowController window; + final WindowSettings windowSettings; + final WindowManagerModel windowManagerModel; + + @override + State createState() => _RegularWindowContentState(); +} + +class _RegularWindowContentState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _animation; + late final Color cubeColor; + + @override + void initState() { + super.initState(); + _animation = AnimationController( + vsync: this, + lowerBound: 0, + upperBound: 2 * pi, + duration: const Duration(seconds: 15), + )..repeat(); + cubeColor = _generateRandomDarkColor(); + } + + @override + void dispose() { + _animation.dispose(); + super.dispose(); + } + + Color _generateRandomDarkColor() { + final random = Random(); + const int lowerBound = 32; + const int span = 160; + int red = lowerBound + random.nextInt(span); + int green = lowerBound + random.nextInt(span); + int blue = lowerBound + random.nextInt(span); + return Color.fromARGB(255, red, green, blue); + } + + @override + Widget build(BuildContext context) { + final dpr = MediaQuery.of(context).devicePixelRatio; + + final child = Scaffold( + appBar: AppBar(title: Text('${widget.window.type}')), + body: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return CustomPaint( + size: const Size(200, 200), + painter: _RotatedWireCube( + angle: _animation.value, color: cubeColor), + ); + }, + ), + ], + ), + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () { + widget.windowManagerModel.add(KeyedWindowController( + controller: RegularWindowController())); + }, + child: const Text('Create Regular Window'), + ), + const SizedBox(height: 20), + ListenableBuilder( + listenable: widget.window, + builder: (BuildContext context, Widget? _) { + return Text( + 'View #${widget.window.view?.viewId ?? "Unknown"}\n' + 'Parent View: ${widget.window.parentViewId}\n' + 'View Size: ${(widget.window.view!.physicalSize.width / dpr).toStringAsFixed(1)}\u00D7${(widget.window.view!.physicalSize.height / dpr).toStringAsFixed(1)}\n' + 'Window Size: ${widget.window.size?.width}\u00D7${widget.window.size?.height}\n' + 'Device Pixel Ratio: $dpr', + textAlign: TextAlign.center, + ); + }) + ], + ), + ], + )), + ); + + return ViewAnchor( + view: ListenableBuilder( + listenable: widget.windowManagerModel, + builder: (BuildContext context, Widget? _) { + final List childViews = []; + for (final KeyedWindowController controller + in widget.windowManagerModel.windows) { + if (controller.parent == widget.window) { + childViews.add(WindowControllerRender( + controller: controller.controller, + key: controller.key, + windowSettings: widget.windowSettings, + windowManagerModel: widget.windowManagerModel, + onDestroyed: () => + widget.windowManagerModel.remove(controller), + onError: () => widget.windowManagerModel.remove(controller), + )); + } + } + + return ViewCollection(views: childViews); + }), + child: child); + } +} + +class _RotatedWireCube extends CustomPainter { + static List vertices = [ + Vector3(-0.5, -0.5, -0.5), + Vector3(0.5, -0.5, -0.5), + Vector3(0.5, 0.5, -0.5), + Vector3(-0.5, 0.5, -0.5), + Vector3(-0.5, -0.5, 0.5), + Vector3(0.5, -0.5, 0.5), + Vector3(0.5, 0.5, 0.5), + Vector3(-0.5, 0.5, 0.5), + ]; + + static const List> edges = [ + [0, 1], [1, 2], [2, 3], [3, 0], // Front face + [4, 5], [5, 6], [6, 7], [7, 4], // Back face + [0, 4], [1, 5], [2, 6], [3, 7], // Connecting front and back + ]; + + final double angle; + final Color color; + + _RotatedWireCube({required this.angle, required this.color}); + + Offset scaleAndCenter(Vector3 point, double size, Offset center) { + final scale = size / 2; + return Offset(center.dx + point.x * scale, center.dy - point.y * scale); + } + + @override + void paint(Canvas canvas, Size size) { + final rotatedVertices = vertices + .map((vertex) => Matrix4.rotationX(angle).transformed3(vertex)) + .map((vertex) => Matrix4.rotationY(angle).transformed3(vertex)) + .map((vertex) => Matrix4.rotationZ(angle).transformed3(vertex)) + .toList(); + + final center = Offset(size.width / 2, size.height / 2); + + final paint = Paint() + ..color = color + ..style = PaintingStyle.stroke + ..strokeWidth = 2; + + for (var edge in edges) { + final p1 = scaleAndCenter(rotatedVertices[edge[0]], size.width, center); + final p2 = scaleAndCenter(rotatedVertices[edge[1]], size.width, center); + canvas.drawLine(p1, p2, paint); + } + } + + @override + bool shouldRepaint(_RotatedWireCube oldDelegate) => true; +} diff --git a/examples/multi_window_ref_app/lib/app/window_controller_render.dart b/examples/multi_window_ref_app/lib/app/window_controller_render.dart new file mode 100644 index 0000000000000..fb9eb6d5e825d --- /dev/null +++ b/examples/multi_window_ref_app/lib/app/window_controller_render.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'regular_window_content.dart'; +import 'window_manager_model.dart'; +import 'window_settings.dart'; + +class WindowControllerRender extends StatelessWidget { + const WindowControllerRender( + {required this.controller, + required this.onDestroyed, + required this.onError, + required this.windowSettings, + required this.windowManagerModel, + required super.key}); + + final WindowController controller; + final VoidCallback onDestroyed; + final VoidCallback onError; + final WindowSettings windowSettings; + final WindowManagerModel windowManagerModel; + + @override + Widget build(BuildContext context) { + switch (controller.type) { + case WindowArchetype.regular: + return RegularWindow( + key: key, + onDestroyed: onDestroyed, + onError: (String? reason) => onError(), + preferredSize: windowSettings.regularSize, + controller: controller as RegularWindowController, + child: RegularWindowContent( + window: controller as RegularWindowController, + windowSettings: windowSettings, + windowManagerModel: windowManagerModel)); + default: + throw UnimplementedError( + "The provided window type does not have an implementation"); + } + } +} diff --git a/examples/multi_window_ref_app/lib/app/window_manager_model.dart b/examples/multi_window_ref_app/lib/app/window_manager_model.dart new file mode 100644 index 0000000000000..b21f5097c4218 --- /dev/null +++ b/examples/multi_window_ref_app/lib/app/window_manager_model.dart @@ -0,0 +1,48 @@ +import 'package:flutter/widgets.dart'; + +class KeyedWindowController { + KeyedWindowController( + {this.parent, this.isMainWindow = false, required this.controller}); + + final WindowController? parent; + final WindowController controller; + final bool isMainWindow; + final UniqueKey key = UniqueKey(); +} + +/// Manages a flat list of all of the [WindowController]s that have been +/// created by the application as well as which controller is currently +/// selected by the UI. +class WindowManagerModel extends ChangeNotifier { + final List _windows = []; + List get windows => _windows; + int? _selectedViewId; + WindowController? get selected { + if (_selectedViewId == null) { + return null; + } + + for (final KeyedWindowController controller in _windows) { + if (controller.controller.view?.viewId == _selectedViewId) { + return controller.controller; + } + } + + return null; + } + + void add(KeyedWindowController window) { + _windows.add(window); + notifyListeners(); + } + + void remove(KeyedWindowController window) { + _windows.remove(window); + notifyListeners(); + } + + void select(int? viewId) { + _selectedViewId = viewId; + notifyListeners(); + } +} diff --git a/examples/multi_window_ref_app/lib/app/window_settings.dart b/examples/multi_window_ref_app/lib/app/window_settings.dart index f93ab4e624db8..8cb0d5ba13498 100644 --- a/examples/multi_window_ref_app/lib/app/window_settings.dart +++ b/examples/multi_window_ref_app/lib/app/window_settings.dart @@ -1,21 +1,13 @@ import 'package:flutter/material.dart'; -class WindowSettings { - WindowSettings( - {this.regularSize = const Size(400, 300), - this.floatingRegularSize = const Size(300, 300), - this.dialogSize = const Size(300, 250), - this.satelliteSize = const Size(150, 300), - this.popupSize = const Size(200, 200), - this.tipSize = const Size(140, 140), - this.anchorToWindow = false, - this.anchorRect = const Rect.fromLTWH(0, 0, 1000, 1000)}); - final Size regularSize; - final Size floatingRegularSize; - final Size dialogSize; - final Size satelliteSize; - final Size popupSize; - final Size tipSize; - final Rect anchorRect; - final bool anchorToWindow; +class WindowSettings extends ChangeNotifier { + WindowSettings({Size regularSize = const Size(400, 300)}) + : _regularSize = regularSize; + + Size _regularSize; + Size get regularSize => _regularSize; + set regularSize(Size value) { + _regularSize = value; + notifyListeners(); + } } diff --git a/examples/multi_window_ref_app/lib/app/window_settings_dialog.dart b/examples/multi_window_ref_app/lib/app/window_settings_dialog.dart index a2ee837be705c..2d8fe433b133f 100644 --- a/examples/multi_window_ref_app/lib/app/window_settings_dialog.dart +++ b/examples/multi_window_ref_app/lib/app/window_settings_dialog.dart @@ -1,356 +1,45 @@ import 'package:flutter/material.dart'; import 'package:multi_window_ref_app/app/window_settings.dart'; -Future windowSettingsDialog( - BuildContext context, - WindowSettings settings, - void Function(Window) onWindowOpened, - void Function(Window) onWindowClosed -) async { +Future windowSettingsDialog( + BuildContext context, WindowSettings settings) async { return await showDialog( barrierDismissible: true, context: context, builder: (BuildContext ctx) { - Size regularSize = settings.regularSize; - Size floatingRegularSize = settings.floatingRegularSize; - Size dialogSize = settings.dialogSize; - Size satelliteSize = settings.satelliteSize; - Size popupSize = settings.popupSize; - Size tipSize = settings.tipSize; - Rect anchorRect = settings.anchorRect; - bool anchorToWindow = settings.anchorToWindow; - - return StatefulBuilder( - builder: (BuildContext ctx, StateSetter setState) { - return SimpleDialog( - contentPadding: const EdgeInsets.all(4), - titlePadding: const EdgeInsets.fromLTRB(24, 10, 24, 0), - title: const Center( - child: Text('Window Settings'), - ), - children: [ - SizedBox( - width: 600, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - children: [ - Expanded( - child: ListTile( - title: const Text('Regular'), - subtitle: Row( - children: [ - Expanded( - child: TextFormField( - initialValue: regularSize.width.toString(), - decoration: const InputDecoration( - labelText: 'Initial width', - ), - onChanged: (String value) => setState( - () => regularSize = Size( - double.tryParse(value) ?? 0, - regularSize.height), - ), - ), - ), - const SizedBox( - width: 20, - ), - Expanded( - child: TextFormField( - initialValue: regularSize.height.toString(), - decoration: const InputDecoration( - labelText: 'Initial height', - ), - onChanged: (String value) => setState( - () => regularSize = Size( - regularSize.width, - double.tryParse(value) ?? 0), - ), - ), - ), - ], - ), - ), - ), - const SizedBox( - width: 10, - ), - Expanded( - child: ListTile( - title: const Text('Floating Regular'), - subtitle: Row( - children: [ - Expanded( - child: TextFormField( - initialValue: - floatingRegularSize.width.toString(), - decoration: const InputDecoration( - labelText: 'Initial width', - ), - onChanged: (String value) => setState( - () => floatingRegularSize = Size( - double.tryParse(value) ?? 0, - floatingRegularSize.height), - ), - ), - ), - const SizedBox( - width: 20, - ), - Expanded( - child: TextFormField( - initialValue: - floatingRegularSize.height.toString(), - decoration: const InputDecoration( - labelText: 'Initial height', - ), - onChanged: (String value) => setState( - () => floatingRegularSize = Size( - regularSize.width, - double.tryParse(value) ?? 0), - ), - ), - ), - ], - ), - ), - ), - ], - ), - const SizedBox( - height: 2, - ), - Row( - children: [ - Expanded( - child: ListTile( - title: const Text('Dialog'), - subtitle: Row( - children: [ - Expanded( - child: TextFormField( - initialValue: dialogSize.width.toString(), - decoration: const InputDecoration( - labelText: 'Initial width', - ), - onChanged: (String value) => setState( - () => dialogSize = Size( - double.tryParse(value) ?? 0, - dialogSize.height), - ), - ), - ), - const SizedBox( - width: 20, - ), - Expanded( - child: TextFormField( - initialValue: dialogSize.height.toString(), - decoration: const InputDecoration( - labelText: 'Initial height', - ), - onChanged: (String value) => setState( - () => dialogSize = Size(dialogSize.width, - double.tryParse(value) ?? 0), - ), - ), - ), - ], - ), - ), - ), - const SizedBox( - width: 10, - ), - Expanded( - child: ListTile( - title: const Text('Satellite'), - subtitle: Row( - children: [ - Expanded( - child: TextFormField( - initialValue: - satelliteSize.width.toString(), - decoration: const InputDecoration( - labelText: 'Initial width', - ), - onChanged: (String value) => setState( - () => satelliteSize = Size( - double.tryParse(value) ?? 0, - satelliteSize.height), - ), - ), - ), - const SizedBox( - width: 20, - ), - Expanded( - child: TextFormField( - initialValue: - satelliteSize.height.toString(), - decoration: const InputDecoration( - labelText: 'Initial height', - ), - onChanged: (String value) => setState( - () => satelliteSize = Size( - satelliteSize.width, - double.tryParse(value) ?? 0), - ), - ), - ), - ], - ), - ), - ), - ], - ), - const SizedBox( - height: 2, - ), - Row( - children: [ - Expanded( - child: ListTile( - title: const Text('Popup'), - subtitle: Row( - children: [ - Expanded( - child: TextFormField( - initialValue: popupSize.width.toString(), - decoration: const InputDecoration( - labelText: 'Initial width', - ), - onChanged: (String value) => setState( - () => popupSize = Size( - double.tryParse(value) ?? 0, - popupSize.height), - ), - ), - ), - const SizedBox( - width: 20, - ), - Expanded( - child: TextFormField( - initialValue: popupSize.height.toString(), - decoration: const InputDecoration( - labelText: 'Initial height', - ), - onChanged: (String value) => setState( - () => popupSize = Size(popupSize.width, - double.tryParse(value) ?? 0), - ), - ), - ), - ], - ), - ), - ), - const SizedBox( - width: 10, - ), - Expanded( - child: ListTile( - title: const Text('Tip'), - subtitle: Row( - children: [ - Expanded( - child: TextFormField( - initialValue: tipSize.width.toString(), - decoration: const InputDecoration( - labelText: 'Initial width', - ), - onChanged: (String value) => setState( - () => tipSize = Size( - double.tryParse(value) ?? 0, - tipSize.height), - ), - ), - ), - const SizedBox( - width: 20, - ), - Expanded( - child: TextFormField( - initialValue: tipSize.height.toString(), - decoration: const InputDecoration( - labelText: 'Initial height', - ), - onChanged: (String value) => setState( - () => tipSize = Size(tipSize.width, - double.tryParse(value) ?? 0), - ), - ), - ), - ], - ), - ), - ), - ], - ), - const SizedBox( - height: 2, - ), - const Divider(), - Row( - children: [ - Expanded( - child: ListTile( - title: const Text('Anchoring'), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Align( - alignment: Alignment.centerLeft, - child: CheckboxListTile( - title: const Text('Anchor to Window'), - subtitle: const Text("Use the parent's window frame as the anchor rectangle"), - contentPadding: EdgeInsets.zero, - value: anchorToWindow, - onChanged: (bool? value) { - setState(() { - anchorToWindow = value ?? false; - }); - }, - controlAffinity: - ListTileControlAffinity.leading, - ), - ), - Align( - alignment: Alignment.centerLeft, - child: Text( - "View Anchor Rectangle (values will be clamped to the size of the parent view)", - style: TextStyle( - color: anchorToWindow - ? Theme.of(context).disabledColor - : Theme.of(context).textTheme.bodyMedium?.color, - ), - ), - ), - Row( + return SimpleDialog( + contentPadding: const EdgeInsets.all(4), + titlePadding: const EdgeInsets.fromLTRB(24, 10, 24, 0), + title: const Center( + child: Text('Window Settings'), + ), + children: [ + SizedBox( + width: 600, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Expanded( + child: ListTile( + title: const Text('Regular'), + subtitle: ListenableBuilder( + listenable: settings, + builder: (BuildContext ctx, Widget? _) { + return Row( children: [ Expanded( child: TextFormField( - enabled: !anchorToWindow, - initialValue: - anchorRect.left.toString(), + initialValue: settings.regularSize.width + .toString(), decoration: const InputDecoration( - labelText: 'Left', + labelText: 'Initial width', ), - onChanged: anchorToWindow - ? null - : (String value) => setState( - () => anchorRect = - Rect.fromLTWH( - double.tryParse( - value) ?? - 0, - anchorRect.top, - anchorRect.width, - anchorRect.height), - ), + onChanged: (String value) => + settings.regularSize = Size( + double.tryParse(value) ?? 0, + settings.regularSize.height), ), ), const SizedBox( @@ -358,113 +47,44 @@ Future windowSettingsDialog( ), Expanded( child: TextFormField( - enabled: !anchorToWindow, - initialValue: anchorRect.top.toString(), + initialValue: settings + .regularSize.height + .toString(), decoration: const InputDecoration( - labelText: 'Top', + labelText: 'Initial height', ), - onChanged: anchorToWindow - ? null - : (String value) => setState( - () => anchorRect = - Rect.fromLTWH( - anchorRect.left, - double.tryParse( - value) ?? - 0, - anchorRect.width, - anchorRect.height), - ), - ), - ), - const SizedBox( - width: 20, - ), - Expanded( - child: TextFormField( - enabled: !anchorToWindow, - initialValue: - anchorRect.width.toString(), - decoration: const InputDecoration( - labelText: 'Width', - ), - onChanged: anchorToWindow - ? null - : (String value) => setState( - () => anchorRect = - Rect.fromLTWH( - anchorRect.left, - anchorRect.top, - double.tryParse( - value) ?? - 0, - anchorRect.height), - ), - ), - ), - const SizedBox( - width: 20, - ), - Expanded( - child: TextFormField( - enabled: !anchorToWindow, - initialValue: - anchorRect.height.toString(), - decoration: const InputDecoration( - labelText: 'Height', - ), - onChanged: anchorToWindow - ? null - : (String value) => setState( - () => anchorRect = - Rect.fromLTWH( - anchorRect.left, - anchorRect.top, - anchorRect.width, - double.tryParse( - value) ?? - 0), - ), + onChanged: (String value) => + settings.regularSize = Size( + settings.regularSize.width, + double.tryParse(value) ?? 0), ), ), ], - ), - ], - ), - ), + ); + }), ), - ], - ), - const SizedBox( - height: 2, - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: TextButton( - onPressed: () { - Navigator.of(context, rootNavigator: true) - .pop(WindowSettings( - regularSize: regularSize, - floatingRegularSize: floatingRegularSize, - dialogSize: dialogSize, - satelliteSize: satelliteSize, - popupSize: popupSize, - tipSize: tipSize, - anchorRect: anchorRect, - anchorToWindow: anchorToWindow, - )); - }, - child: const Text('Apply'), - ), + ), + const SizedBox( + width: 10, + ), + ], + ), + ], ), - const SizedBox( - height: 2, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: TextButton( + onPressed: () { + Navigator.of(context, rootNavigator: true).pop(); + }, + child: const Text('Apply'), ), - ], - ); - }); + ), + const SizedBox( + height: 2, + ), + ], + ); }); } diff --git a/examples/multi_window_ref_app/lib/main.dart b/examples/multi_window_ref_app/lib/main.dart index bff2d5b87e619..48148c7b6b6dc 100644 --- a/examples/multi_window_ref_app/lib/main.dart +++ b/examples/multi_window_ref_app/lib/main.dart @@ -1,14 +1,12 @@ import 'package:flutter/material.dart'; - import 'app/main_window.dart'; void main() { - runWidget(MultiWindowApp(initialWindows: [ - (BuildContext context) => createRegular( - context: context, - size: const Size(800, 600), - builder: (context) { - return const MaterialApp(home: MainWindow()); - }) + final RegularWindowController controller = RegularWindowController(); + runWidget(WindowingApp(children: [ + RegularWindow( + controller: controller, + preferredSize: const Size(800, 600), + child: MaterialApp(home: MainWindow(mainController: controller))) ])); } diff --git a/examples/multi_window_ref_app/windows/flutter/CMakeLists.txt b/examples/multi_window_ref_app/windows/flutter/CMakeLists.txt index 4bff75b03b7aa..2eea2a3218180 100644 --- a/examples/multi_window_ref_app/windows/flutter/CMakeLists.txt +++ b/examples/multi_window_ref_app/windows/flutter/CMakeLists.txt @@ -46,10 +46,6 @@ list(APPEND CPP_WRAPPER_SOURCES_CORE "standard_codec.cc" ) list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") -list(APPEND CPP_WRAPPER_SOURCES_WINDOWING - "windowing.cc" -) -list(TRANSFORM CPP_WRAPPER_SOURCES_WINDOWING PREPEND "${WRAPPER_ROOT}/") list(APPEND CPP_WRAPPER_SOURCES_PLUGIN "plugin_registrar.cc" ) @@ -114,6 +110,7 @@ add_custom_target(flutter_assemble DEPENDS "${FLUTTER_LIBRARY}" ${FLUTTER_LIBRARY_HEADERS} ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_WINDOWING} ${CPP_WRAPPER_SOURCES_PLUGIN} ${CPP_WRAPPER_SOURCES_APP} ) diff --git a/examples/multi_window_ref_app/windows/runner/Runner.rc b/examples/multi_window_ref_app/windows/runner/Runner.rc index ebdce3fdd6e39..34a087abcbc73 100644 --- a/examples/multi_window_ref_app/windows/runner/Runner.rc +++ b/examples/multi_window_ref_app/windows/runner/Runner.rc @@ -52,7 +52,7 @@ END // Icon with lowest ID value placed first to ensure application icon // remains consistent on all systems. -IDI_APP_ICON ICON "resources\\app_icon.ico" +// IDI_APP_ICON ICON "resources\\app_icon.ico" ///////////////////////////////////////////////////////////////////////////// @@ -89,13 +89,13 @@ BEGIN BEGIN BLOCK "040904e4" BEGIN - VALUE "CompanyName", "com.example" "\0" - VALUE "FileDescription", "multi_window_ref_app" "\0" + VALUE "CompanyName", "The Flutter Authors" "\0" + VALUE "FileDescription", "A sample application demonstrating Flutter APIs." "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" - VALUE "InternalName", "multi_window_ref_app" "\0" - VALUE "LegalCopyright", "Copyright (C) 2024 com.example. All rights reserved." "\0" - VALUE "OriginalFilename", "multi_window_ref_app.exe" "\0" - VALUE "ProductName", "multi_window_ref_app" "\0" + VALUE "InternalName", "Flutter API Sample" "\0" + VALUE "LegalCopyright", "Copyright 2014 The Flutter Authors. All rights reserved." "\0" + VALUE "OriginalFilename", "flutter_api_samples.exe" "\0" + VALUE "ProductName", "Flutter API Sample" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" END END diff --git a/examples/multi_window_ref_app/windows/runner/flutter_window.cpp b/examples/multi_window_ref_app/windows/runner/flutter_window.cpp new file mode 100644 index 0000000000000..9cbd3109c3fee --- /dev/null +++ b/examples/multi_window_ref_app/windows/runner/flutter_window.cpp @@ -0,0 +1,65 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/examples/multi_window_ref_app/windows/runner/flutter_window.h b/examples/multi_window_ref_app/windows/runner/flutter_window.h new file mode 100644 index 0000000000000..bbc5836c018a2 --- /dev/null +++ b/examples/multi_window_ref_app/windows/runner/flutter_window.h @@ -0,0 +1,37 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/examples/multi_window_ref_app/windows/runner/resource.h b/examples/multi_window_ref_app/windows/runner/resource.h index 66a65d1e4a79f..c245ff19cb580 100644 --- a/examples/multi_window_ref_app/windows/runner/resource.h +++ b/examples/multi_window_ref_app/windows/runner/resource.h @@ -1,3 +1,7 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + //{{NO_DEPENDENCIES}} // Microsoft Visual C++ generated include file. // Used by Runner.rc diff --git a/examples/multi_window_ref_app/windows/runner/resources/app_icon.ico b/examples/multi_window_ref_app/windows/runner/resources/app_icon.ico deleted file mode 100644 index c04e20caf6370..0000000000000 Binary files a/examples/multi_window_ref_app/windows/runner/resources/app_icon.ico and /dev/null differ diff --git a/examples/multi_window_ref_app/windows/runner/utils.cpp b/examples/multi_window_ref_app/windows/runner/utils.cpp index 3a0b46511a71b..6abcd65042070 100644 --- a/examples/multi_window_ref_app/windows/runner/utils.cpp +++ b/examples/multi_window_ref_app/windows/runner/utils.cpp @@ -1,3 +1,7 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + #include "utils.h" #include @@ -49,7 +53,6 @@ std::string Utf8FromUtf16(const wchar_t* utf16_string) { CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, -1, nullptr, 0, nullptr, nullptr) -1; // remove the trailing null character - int input_length = (int)wcslen(utf16_string); std::string utf8_string; if (target_length == 0 || target_length > utf8_string.max_size()) { return utf8_string; @@ -57,7 +60,7 @@ std::string Utf8FromUtf16(const wchar_t* utf16_string) { utf8_string.resize(target_length); int converted_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - input_length, utf8_string.data(), target_length, nullptr, nullptr); + -1, utf8_string.data(), target_length, nullptr, nullptr); if (converted_length == 0) { return std::string(); } diff --git a/examples/multi_window_ref_app/windows/runner/utils.h b/examples/multi_window_ref_app/windows/runner/utils.h index 3879d54755798..54414c989ba71 100644 --- a/examples/multi_window_ref_app/windows/runner/utils.h +++ b/examples/multi_window_ref_app/windows/runner/utils.h @@ -1,3 +1,7 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + #ifndef RUNNER_UTILS_H_ #define RUNNER_UTILS_H_ diff --git a/examples/multi_window_ref_app/windows/runner/win32_window.cpp b/examples/multi_window_ref_app/windows/runner/win32_window.cpp new file mode 100644 index 0000000000000..8fef19b0fbfd4 --- /dev/null +++ b/examples/multi_window_ref_app/windows/runner/win32_window.cpp @@ -0,0 +1,249 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "win32_window.h" + +#include + +#include "resource.h" + +namespace { + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + return OnCreate(); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} diff --git a/examples/multi_window_ref_app/windows/runner/win32_window.h b/examples/multi_window_ref_app/windows/runner/win32_window.h new file mode 100644 index 0000000000000..33786ecd6f0b9 --- /dev/null +++ b/examples/multi_window_ref_app/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates and shows a win32 window with |title| and position and size using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size to will treat the width height passed in to this function + // as logical pixels and scale to appropriate for the default monitor. Returns + // true if the window was created successfully. + bool CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/packages/flutter/lib/src/widgets/window.dart b/packages/flutter/lib/src/widgets/window.dart index 5488615f5d078..d368f484ce7b1 100644 --- a/packages/flutter/lib/src/widgets/window.dart +++ b/packages/flutter/lib/src/widgets/window.dart @@ -3,169 +3,331 @@ // found in the LICENSE file. import 'dart:ui' show FlutterView; -import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; -/// Defines the type of a [Window] +/// Defines the type of the Window enum WindowArchetype { - /// Defines a standard [Window] - regular, + /// Defines a traditional window + regular +} - /// Defines a [Window] that is on a layer above [regular] [Window]s and is not dockable - floatingRegular, +/// Controller used with the [RegularWindow] widget. This controller +/// provides access to modify and destroy the window, in addition to +/// listening to changes on the window. +abstract class WindowController with ChangeNotifier { + FlutterView? _view; + + /// The ID of the view used for this window, which is unique to each window. + FlutterView? get view => _view; + set view(FlutterView? value) { + _view = value; + notifyListeners(); + } - /// Defines a dialog [Window] - dialog, + Size? _size; - /// Defines a satellite [Window] - satellite, + /// The current size of the window. This may differ from the requested size. + Size? get size => _size; + set size(Size? value) { + _size = value; + notifyListeners(); + } - /// Defines a popup [Window] - popup, + int? _parentViewId; - /// Defines a tooltip - tip, -} + /// The ID of the parent in which this rendered, if any. + int? get parentViewId => _parentViewId; + set parentViewId(int? value) { + _parentViewId = value; + notifyListeners(); + } -class WindowChangedEvent { - WindowChangedEvent({this.size}); - Size? size; + /// The archetype of the window. + WindowArchetype get type; + + /// Destroys this window. + Future destroy() async { + if (view == null) { + return; + } + + return destroyWindow(view!.viewId); + } } -class _WindowMetadata { - _WindowMetadata( - {required this.flView, - required this.archetype, - required this.size, - this.parent}); +/// Provided to [RegularWindow]. Allows the user to listen on changes +/// to a regular window and modify the window. +class RegularWindowController extends WindowController { + @override + WindowArchetype get type => WindowArchetype.regular; - final FlutterView flView; - final WindowArchetype archetype; - final Size size; - final Window? parent; + /// Modify the properties of the window. + Future modify({Size? size}) { + throw UnimplementedError(); + } } -/// Defines a [Window] created by the application. To use [Window]s, you must wrap -/// your application in the [MultiWindowApp] widget. New [Window]s are created via -/// global functions like [createRegular] and [createPopup]. -abstract class Window { - /// [view] the underlying [FlutterView] - /// [builder] render function containing the content of this [Window] - /// [size] initial [Size] of the [Window] - /// [parent] the parent of this window, if any - Window( - {required this.view, - required this.builder, - required this.size, - this.parent}); +/// A widget that creates a regular window. This content of this window is +/// rendered into a [View], meaning that this widget must be rendered into +/// either a [ViewAnchor] or a [ViewCollection]. +class RegularWindow extends StatefulWidget { + /// Creates a regular window widget + const RegularWindow( + {this.controller, + this.onDestroyed, + this.onError, + super.key, + required this.preferredSize, + required this.child}); - /// The underlying [FlutterView] associated with this [Window] - final FlutterView view; + /// Controller for this widget. + final RegularWindowController? controller; + + /// Called when the window backing this widget is destroyed. + final void Function()? onDestroyed; - /// The render function containing the content of this [Window] - final Widget Function(BuildContext context) builder; + /// Called when an error is encountered during the creation of this widget. + final void Function(String?)? onError; - /// The current [Size] of the [Window] - Size size; + /// Preferred size of the window. + final Size preferredSize; - /// The parent of this window, which may or may not exist. - final Window? parent; + /// The content rendered into this window. + final Widget child; - /// A list of child [Window]s associated with this window - final List children = []; + @override + State createState() => _RegularWindowState(); +} - UniqueKey _key = UniqueKey(); +class _RegularWindowState extends State { + _WindowListener? _listener; + Future? _future; + _WindowingAppState? _app; + int? _viewId; + bool _hasBeenDestroyed = false; - final StreamController _onDestroyedController = - StreamController.broadcast(); - final StreamController _onWindowChangedController = - StreamController.broadcast(); + @override + void initState() { + super.initState(); + final Future createRegularFuture = + createRegular(size: widget.preferredSize); + setState(() { + _future = createRegularFuture; + }); - Stream get destroyedStream { - return _onDestroyedController.stream; + createRegularFuture.then((WindowCreationResult metadata) async { + _viewId = metadata.view.viewId; + if (widget.controller != null) { + widget.controller!.view = metadata.view; + widget.controller!.parentViewId = metadata.parent; + widget.controller!.size = metadata.size; + } + + SchedulerBinding.instance.addPostFrameCallback((_) async { + final _WindowingAppContext? windowingAppContext = + _WindowingAppContext.of(context); + assert(windowingAppContext != null); + _listener = _WindowListener( + viewId: metadata.view.viewId, + onChanged: (_WindowChangeProperties properties) { + if (widget.controller == null) { + return; + } + + if (properties.size != null) { + widget.controller!.size = properties.size; + } + + if (properties.parentViewId != null) { + widget.controller!.parentViewId = properties.parentViewId; + } + }, + onDestroyed: () { + widget.onDestroyed?.call(); + _hasBeenDestroyed = true; + }); + _app = windowingAppContext!.windowingApp; + _app!._registerListener(_listener!); + }); + }).catchError((Object? error) { + widget.onError?.call(error.toString()); + }); } - Stream get changedStream { - return _onWindowChangedController.stream; + @override + Future dispose() async { + if (_listener != null) { + assert(_app != null); + _app!._unregisterListener(_listener!); + } + + // In the event that we're being disposed before we've been destroyed + // we need to destroy the window on our way out. + if (!_hasBeenDestroyed && _viewId != null) { + // In the event of an argument error, we do nothing. We assume that + // the window has been successfully destroyed somehow else. + try { + await destroyWindow(_viewId!); + } on ArgumentError {} + } + + super.dispose(); } - WindowArchetype get archetype; + @override + Widget build(BuildContext context) { + return FutureBuilder( + key: widget.key, + future: _future, + builder: (BuildContext context, + AsyncSnapshot metadata) { + if (!metadata.hasData) { + return const ViewCollection(views: []); + } + + return View( + view: metadata.data!.view, + child: WindowContext( + viewId: metadata.data!.view.viewId, child: widget.child)); + }); + } } -/// Describes a top level window that is created with [createRegular]. -class RegularWindow extends Window { - /// [view] the underlying [FlutterView] - /// [builder] render function containing the content of this [Window] - /// [size] initial [Size] of the [Window] - RegularWindow( - {required super.view, required super.builder, required super.size}); +/// Provides descendents with access to the [Window] in which they are rendered +class WindowContext extends InheritedWidget { + /// [window] the [Window] + const WindowContext({super.key, required this.viewId, required super.child}); + + /// The view ID in this context + final int viewId; + + /// Returns the [WindowContext] if any + static WindowContext? of(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType(); + } @override - WindowArchetype get archetype { - return WindowArchetype.regular; + bool updateShouldNotify(WindowContext oldWidget) { + return viewId != oldWidget.viewId; } } -/// Creates a new regular [Window]. +/// The raw data returned as a result of creating a window. +class WindowCreationResult { + /// Creates a new window. + WindowCreationResult( + {required this.view, + required this.archetype, + required this.size, + this.parent}); + + /// The view associated with the window. + final FlutterView view; + + /// The archetype of the window. + final WindowArchetype archetype; + + /// The initial size of the window. + final Size size; + + /// The id of the window's parent, if any. + final int? parent; +} + +/// Creates a regular window for the platform and returns the metadata associated +/// with the new window. Users should prefer using the [RegularWindow] +/// widget instead of this method. /// -/// [context] the current [BuildContext], which must include a [MultiWindowAppContext] /// [size] the size of the new [Window] in pixels -/// [builder] a builder function that returns the contents of the new [Window] -Future createRegular( - {required BuildContext context, - required Size size, - required WidgetBuilder builder}) async { - final MultiWindowAppContext? multiViewAppContext = - MultiWindowAppContext.of(context); - if (multiViewAppContext == null) { - throw Exception( - 'Cannot create a window: your application does not use MultiViewApp. Try wrapping your toplevel application in a MultiViewApp widget'); - } +Future createRegular({required Size size}) { + return _createWindow(viewBuilder: (MethodChannel channel) async { + return await channel.invokeMethod('createWindow', { + 'size': [size.width.toInt(), size.height.toInt()], + }) as Map; + }); +} - return multiViewAppContext.windowController - .createRegular(size: size, builder: builder); +Future _createWindow( + {required Future> Function(MethodChannel channel) + viewBuilder}) async { + WidgetsFlutterBinding.ensureInitialized(); + final Map creationData = + await viewBuilder(SystemChannels.windowing); + final int viewId = creationData['viewId']! as int; + final WindowArchetype archetype = + WindowArchetype.values[creationData['archetype']! as int]; + final List size = creationData['size']! as List; + final int? parentViewId = creationData['parentViewId'] as int?; + + final FlutterView flView = + WidgetsBinding.instance.platformDispatcher.views.firstWhere( + (FlutterView view) => view.viewId == viewId, + orElse: () { + throw Exception('No matching view found for viewId: $viewId'); + }, + ); + + return WindowCreationResult( + view: flView, + archetype: archetype, + size: Size((size[0]! as int).toDouble(), (size[1]! as int).toDouble()), + parent: parentViewId); } -/// Destroys the provided [Window] +/// Destroys the window associated with the provided view ID. /// -/// [context] the current [BuildContext], which must include a [MultiWindowAppContext] -/// [window] the [Window] to be destroyed -Future destroyWindow(BuildContext context, Window window) async { - final MultiWindowAppContext? multiViewAppContext = - MultiWindowAppContext.of(context); - if (multiViewAppContext == null) { - throw Exception( - 'Cannot create a window: your application does not use MultiViewApp. Try wrapping your toplevel application in a MultiViewApp widget'); +/// [viewId] the view id of the window that should be destroyed +Future destroyWindow(int viewId) async { + try { + await SystemChannels.windowing + .invokeMethod('destroyWindow', {'viewId': viewId}); + } on PlatformException catch (e) { + throw ArgumentError( + 'Unable to delete window with view_id=$viewId. Does the window exist? Error: $e'); } +} + +class _WindowChangeProperties { + _WindowChangeProperties({this.size, this.parentViewId}); - return multiViewAppContext.windowController.destroyWindow(window); + Size? size; + int? parentViewId; } -/// Declares that an application will create multiple [Window]s. -/// The current [Window] can be looked up with [WindowContext.of]. -class MultiWindowApp extends StatefulWidget { - /// [initialWindows] A list of [Function]s to create [Window]s that will be run as soon as the app starts. - const MultiWindowApp({super.key, this.initialWindows}); +class _WindowListener { + _WindowListener( + {required this.viewId, + required this.onChanged, + required this.onDestroyed}); + + int viewId; + void Function(_WindowChangeProperties) onChanged; + void Function()? onDestroyed; +} - /// A list of [Function]s to create [Window]s that will be run as soon as the app starts. - final List Function(BuildContext)>? initialWindows; +/// Declares that an application will create multiple windows. +class WindowingApp extends StatefulWidget { + /// Creates a new windowing app with the provided child windows. + const WindowingApp({super.key, required this.children}); + + /// A list of initial windows to render. These windows will be placed inside + /// of a [ViewCollection]. + final List children; @override - State createState() => WindowController(); + State createState() => _WindowingAppState(); } -/// Provides methods to create, update, and delete [Window]s. It is preferred that -/// you use the global functions like [createRegular] and [destroyWindow] over -/// accessing the [WindowController] directly. -class WindowController extends State { - List _windows = []; +class _WindowingAppState extends State { + final List<_WindowListener> _listeners = <_WindowListener>[]; @override void initState() { super.initState(); + WidgetsFlutterBinding.ensureInitialized(); SystemChannels.windowing.setMethodCallHandler(_methodCallHandler); } @@ -174,263 +336,74 @@ class WindowController extends State { call.arguments as Map; switch (call.method) { + case 'onWindowCreated': + final int viewId = arguments['viewId']! as int; + int? parentViewId; + if (arguments['parentViewId'] != null) { + parentViewId = arguments['parentViewId']! as int; + } + + final _WindowChangeProperties properties = + _WindowChangeProperties(parentViewId: parentViewId); + for (final _WindowListener listener in _listeners) { + if (listener.viewId == viewId) { + listener.onChanged(properties); + } + } case 'onWindowChanged': final int viewId = arguments['viewId']! as int; - final Window? window = _findWindow(viewId); - assert(window != null); Size? size; if (arguments['size'] != null) { final List sizeRaw = arguments['size']! as List; size = Size( (sizeRaw[0]! as int).toDouble(), (sizeRaw[1]! as int).toDouble()); } - _changed(window!, size); + + final _WindowChangeProperties properties = + _WindowChangeProperties(size: size); + for (final _WindowListener listener in _listeners) { + if (listener.viewId == viewId) { + listener.onChanged(properties); + } + } case 'onWindowDestroyed': final int viewId = arguments['viewId']! as int; - _remove(viewId); - } - } - - Future<_WindowMetadata> _createWindow( - {required Future> Function(MethodChannel channel) - viewBuilder, - required WidgetBuilder builder}) async { - final Map creationData = - await viewBuilder(SystemChannels.windowing); - final int viewId = creationData['viewId']! as int; - final WindowArchetype archetype = - WindowArchetype.values[creationData['archetype']! as int]; - final List size = creationData['size']! as List; - final int? parentViewId = creationData['parentViewId'] as int?; - - final FlutterView flView = - WidgetsBinding.instance.platformDispatcher.views.firstWhere( - (FlutterView view) => view.viewId == viewId, - orElse: () { - throw Exception('No matching view found for viewId: $viewId'); - }, - ); - - Window? parent; - if (parentViewId != null) { - parent = _findWindow(parentViewId); - assert(parent != null, - 'No matching window found for parentViewId: $parentViewId'); - } - - return _WindowMetadata( - flView: flView, - archetype: archetype, - size: Size((size[0]! as int).toDouble(), (size[1]! as int).toDouble()), - parent: parent); - } - - /// Creates a new regular [Window] - /// - /// [size] the size of the new [Window] in pixels - /// [builder] a builder function that returns the contents of the new [Window] - Future createRegular( - {required Size size, required WidgetBuilder builder}) async { - final _WindowMetadata metadata = await _createWindow( - viewBuilder: (MethodChannel channel) async { - return await channel.invokeMethod('createWindow', { - 'size': [size.width.toInt(), size.height.toInt()], - }) as Map; - }, - builder: builder); - final RegularWindow window = RegularWindow( - view: metadata.flView, builder: builder, size: metadata.size); - _add(window); - return window; - } - - /// Destroys the provided [Window] - /// - /// [window] the [Window] to be destroyed - Future destroyWindow(Window window) async { - try { - await SystemChannels.windowing.invokeMethod( - 'destroyWindow', {'viewId': window.view.viewId}); - _remove(window.view.viewId); - } on PlatformException catch (e) { - throw ArgumentError( - 'Unable to delete window with view_id=${window.view.viewId}. Does the window exist? Error: $e'); - } - } - - void _add(Window window) { - final List copy = List.from(_windows); - if (window.parent != null) { - window.parent!.children.add(window); - Window rootWindow = window; - while (rootWindow.parent != null) { - rootWindow = rootWindow.parent!; - } - rootWindow._key = UniqueKey(); - } else { - copy.add(window); - } - - setState(() { - _windows = copy; - }); - } - - Window? _findWindow(int viewId) { - Window? find(int viewId, Window window) { - if (window.view.viewId == viewId) { - return window; - } - - for (final Window other in window.children) { - final Window? result = find(viewId, other); - if (result != null) { - return result; + for (final _WindowListener listener in _listeners) { + if (listener.viewId == viewId) { + listener.onDestroyed?.call(); + } } - } - - return null; - } - - for (final Window other in _windows) { - final Window? result = find(viewId, other); - if (result != null) { - return result; - } - } - - return null; - } - - void _remove(int viewId) { - final List copy = List.from(_windows); - - final Window? toDelete = _findWindow(viewId); - if (toDelete == null) { - return; - } - - if (toDelete.parent == null) { - copy.remove(toDelete); - } else { - toDelete.parent!.children.remove(toDelete); - } - - toDelete._onDestroyedController.add(null); - - setState(() { - _windows = copy; - }); - } - - void _changed(Window window, Size? size) { - if (size != null) { - window.size = size; - window._onWindowChangedController.add(WindowChangedEvent(size: size)); } } - @override - Widget build(BuildContext context) { - return MultiWindowAppContext( - windows: _windows, - windowController: this, - child: _MultiWindowAppView( - initialWindows: widget.initialWindows, windows: _windows)); - } -} - -/// Provides access to the list of [Window]s. -/// Users may provide the identifier of a [View] to look up a particular -/// [Window] if any exists. -/// -/// This class also provides access to the [WindowController] which is -/// used internally to provide access to create, update, and delete methods -/// on the windowing system. -class MultiWindowAppContext extends InheritedWidget { - /// [windows] a list of [Window]s - /// [windowController] the [WindowController] active in this context - const MultiWindowAppContext( - {super.key, - required super.child, - required this.windows, - required this.windowController}); - - /// The list of Windows - final List windows; - - /// The [WindowController] active in this context - final WindowController windowController; - - /// Returns the [MultiWindowAppContext] if any - static MultiWindowAppContext? of(BuildContext context) { - return context.dependOnInheritedWidgetOfExactType(); - } - - @override - bool updateShouldNotify(MultiWindowAppContext oldWidget) { - return windows != oldWidget.windows || - windowController != oldWidget.windowController; - } -} - -class _MultiWindowAppView extends StatefulWidget { - const _MultiWindowAppView( - {required this.initialWindows, required this.windows}); - - final List Function(BuildContext)>? initialWindows; - final List windows; - - @override - State createState() => _MultiWindowAppViewState(); -} - -class _MultiWindowAppViewState extends State<_MultiWindowAppView> { - @override - void initState() { - super.initState(); - SchedulerBinding.instance.addPostFrameCallback((_) async { - if (widget.initialWindows != null) { - for (final Future Function(BuildContext) window - in widget.initialWindows!) { - await window(context); - } - } - }); + void _registerListener(_WindowListener listener) { + _listeners.add(listener); } - Widget buildView(BuildContext context, Window window) { - return View( - key: window._key, - view: window.view, - child: WindowContext(window: window, child: window.builder(context))); + void _unregisterListener(_WindowListener listener) { + _listeners.remove(listener); } @override Widget build(BuildContext context) { - final List views = []; - for (final Window window in widget.windows) { - views.add(buildView(context, window)); - } - return ViewCollection(views: views); + return _WindowingAppContext( + windowingApp: this, child: ViewCollection(views: widget.children)); } } -/// Provides descendents with access to the [Window] in which they are rendered -class WindowContext extends InheritedWidget { - /// [window] the [Window] - const WindowContext({super.key, required this.window, required super.child}); +class _WindowingAppContext extends InheritedWidget { + const _WindowingAppContext( + {super.key, required super.child, required this.windowingApp}); - /// The [Window] in this context - final Window window; + final _WindowingAppState windowingApp; - /// Returns the [WindowContext] if any - static WindowContext? of(BuildContext context) { - return context.dependOnInheritedWidgetOfExactType(); + /// Returns the [MultiWindowAppContext] if any + static _WindowingAppContext? of(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType<_WindowingAppContext>(); } @override - bool updateShouldNotify(WindowContext oldWidget) { - return window != oldWidget.window; + bool updateShouldNotify(_WindowingAppContext oldWidget) { + return windowingApp != oldWidget.windowingApp; } }