diff --git a/engine/src/flutter/shell/platform/linux/BUILD.gn b/engine/src/flutter/shell/platform/linux/BUILD.gn index 12ad6f3c436a1..c99403c32aefe 100644 --- a/engine/src/flutter/shell/platform/linux/BUILD.gn +++ b/engine/src/flutter/shell/platform/linux/BUILD.gn @@ -158,10 +158,10 @@ source_set("flutter_linux_sources") { "fl_view.cc", "fl_view_accessible.cc", "fl_window_state_monitor.cc", - "fl_windowing_channel.cc", - "fl_windowing_handler.cc", "fl_windowing.cc", "fl_windowing.h", + "fl_windowing_channel.cc", + "fl_windowing_handler.cc", "key_mapping.g.cc", ] 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 8a79fa349329d..ca58f1e08b645 100644 --- a/examples/multi_window_ref_app/lib/app/main_window.dart +++ b/examples/multi_window_ref_app/lib/app/main_window.dart @@ -149,7 +149,7 @@ class _ActiveWindowsTable extends StatelessWidget { } }, cells: [ - DataCell(Text('$controller.controller.rootView.viewId')), + DataCell(Text('${controller.controller.rootView.viewId}')), DataCell( ListenableBuilder( listenable: controller.controller, diff --git a/examples/multi_window_ref_app/lib/app/regular_window_edit_dialog.dart b/examples/multi_window_ref_app/lib/app/regular_window_edit_dialog.dart index f6a5b4b3dd025..9dc12cda070cf 100644 --- a/examples/multi_window_ref_app/lib/app/regular_window_edit_dialog.dart +++ b/examples/multi_window_ref_app/lib/app/regular_window_edit_dialog.dart @@ -74,7 +74,10 @@ void showRegularWindowEditDialog(BuildContext context, String? title = titleController.text.isEmpty ? null : titleController.text; - onSave?.call(width, height, title, selectedState); + onSave?.call(width, height, title, WindowState.restored); + if (selectedState != WindowState.restored) { + onSave?.call(null, null, title, selectedState); + } Navigator.of(context).pop(); }, child: Text("Save"), diff --git a/packages/flutter/lib/src/widgets/window.dart b/packages/flutter/lib/src/widgets/window.dart index 903195f055e82..a0262ff7ff8ac 100644 --- a/packages/flutter/lib/src/widgets/window.dart +++ b/packages/flutter/lib/src/widgets/window.dart @@ -56,8 +56,7 @@ enum WindowState { abstract class WindowController with ChangeNotifier { @protected /// Sets the view associated with this window. - // ignore: use_setters_to_change_properties - void setView(FlutterView view) { + set view(FlutterView view) { _view = view; } diff --git a/packages/flutter/lib/src/widgets/window_linux.dart b/packages/flutter/lib/src/widgets/window_linux.dart index 16d1a9a8549cf..95331b0118991 100644 --- a/packages/flutter/lib/src/widgets/window_linux.dart +++ b/packages/flutter/lib/src/widgets/window_linux.dart @@ -74,7 +74,7 @@ class PopupWindowControllerLinux extends PopupWindowController { final FlutterView flutterView = WidgetsBinding.instance.platformDispatcher.views.firstWhere( (FlutterView view) => view.viewId == viewId, ); - setView(flutterView); + view = flutterView; } Pointer getWindowHandle() { @@ -142,7 +142,7 @@ class RegularWindowControllerLinux extends RegularWindowController { final FlutterView flutterView = WidgetsBinding.instance.platformDispatcher.views.firstWhere( (FlutterView view) => view.viewId == viewId, ); - setView(flutterView); + view = flutterView; if (title != null) { setTitle(title); } diff --git a/packages/flutter/lib/src/widgets/window_macos.dart b/packages/flutter/lib/src/widgets/window_macos.dart index a5c987a7fe1ee..578336650e6a9 100644 --- a/packages/flutter/lib/src/widgets/window_macos.dart +++ b/packages/flutter/lib/src/widgets/window_macos.dart @@ -73,7 +73,7 @@ class PopupWindowControllerMacOS extends PopupWindowController { final FlutterView flutterView = WidgetsBinding.instance.platformDispatcher.views.firstWhere( (FlutterView view) => view.viewId == viewId, ); - setView(flutterView); + view = flutterView; // This calculates absolute position using the [WindowPositioner]. On Linux it would instead convert // the positioner xdg_positioner and pass it to the window manager. _positionWindow(parentWindow, position, anchorRect); @@ -164,7 +164,7 @@ class RegularWindowControllerMacOS extends RegularWindowController { final FlutterView flutterView = WidgetsBinding.instance.platformDispatcher.views.firstWhere( (FlutterView view) => view.viewId == viewId, ); - setView(flutterView); + view = flutterView; if (title != null) { setTitle(title); } diff --git a/packages/flutter/lib/src/widgets/window_positioning.dart b/packages/flutter/lib/src/widgets/window_positioning.dart index 862a5d9186672..a3fa3ad84ec30 100644 --- a/packages/flutter/lib/src/widgets/window_positioning.dart +++ b/packages/flutter/lib/src/widgets/window_positioning.dart @@ -212,7 +212,7 @@ class WindowPositioner { final Set constraintAdjustment; /// Computes the screen-space rectangle for a child window placed according to - /// the this position. [childSize] is the frame size of the child window. + /// this [WindowPositioner]. [childSize] is the frame size of the child window. /// [anchorRect] is the rectangle relative to which the child window is placed. /// [parentRect] is the parent window's rectangle. [outputRect] is the output /// display area where the child window will be placed. All sizes and rectangles @@ -233,7 +233,110 @@ class WindowPositioner { return defaultResult; } } - // TODO: Finish + + if (constraintAdjustment.contains(WindowPositionerConstraintAdjustment.flipX)) { + final Offset result = + _constraintTo( + parentRect, + parentAnchor.flipX().anchorPositionFor(anchorRect) + _flipX(offset), + ) + + childAnchor.flipX().offsetFor(childSize); + if (_rectContains(outputRect, result & childSize)) { + return result & childSize; + } + } + + if (constraintAdjustment.contains(WindowPositionerConstraintAdjustment.flipY)) { + final Offset result = + _constraintTo( + parentRect, + parentAnchor.flipY().anchorPositionFor(anchorRect) + _flipY(offset), + ) + + childAnchor.flipY().offsetFor(childSize); + if (_rectContains(outputRect, result & childSize)) { + return result & childSize; + } + } + + if (constraintAdjustment.containsAll({ + WindowPositionerConstraintAdjustment.flipX, + WindowPositionerConstraintAdjustment.flipY, + })) { + final Offset result = + _constraintTo( + parentRect, + parentAnchor.flipY().flipX().anchorPositionFor(anchorRect) + _flipX(_flipY(offset)), + ) + + childAnchor.flipY().flipX().offsetFor(childSize); + if (_rectContains(outputRect, result & childSize)) { + return result & childSize; + } + } + + { + Offset result = + _constraintTo(parentRect, parentAnchor.anchorPositionFor(anchorRect) + offset) + + childAnchor.offsetFor(childSize); + + if (constraintAdjustment.contains(WindowPositionerConstraintAdjustment.slideX)) { + final double leftOverhang = result.dx - outputRect.left; + final double rightOverhang = result.dx + childSize.width - outputRect.right; + if (leftOverhang < 0.0) { + result = result.translate(-leftOverhang, 0.0); + } else if (rightOverhang > 0.0) { + result = result.translate(-rightOverhang, 0.0); + } + } + + if (constraintAdjustment.contains(WindowPositionerConstraintAdjustment.slideY)) { + final double topOverhang = result.dy - outputRect.top; + final double bottomOverhang = result.dy + childSize.height - outputRect.bottom; + if (topOverhang < 0.0) { + result = result.translate(0.0, -topOverhang); + } else if (bottomOverhang > 0.0) { + result = result.translate(0.0, -bottomOverhang); + } + } + + if (_rectContains(outputRect, result & childSize)) { + return result & childSize; + } + } + + { + Offset result = + _constraintTo(parentRect, parentAnchor.anchorPositionFor(anchorRect) + offset) + + childAnchor.offsetFor(childSize); + + if (constraintAdjustment.contains(WindowPositionerConstraintAdjustment.resizeX)) { + final double leftOverhang = result.dx - outputRect.left; + final double rightOverhang = result.dx + childSize.width - outputRect.right; + if (leftOverhang < 0.0) { + result = result.translate(-leftOverhang, 0.0); + childSize = Size(childSize.width + leftOverhang, childSize.height); + } + if (rightOverhang > 0.0) { + childSize = Size(childSize.width - rightOverhang, childSize.height); + } + } + + if (constraintAdjustment.contains(WindowPositionerConstraintAdjustment.resizeY)) { + final double topOverhang = result.dy - outputRect.top; + final double bottomOverhang = result.dy + childSize.height - outputRect.bottom; + if (topOverhang < 0.0) { + result = result.translate(0.0, -topOverhang); + childSize = Size(childSize.width, childSize.height + topOverhang); + } + if (bottomOverhang > 0.0) { + childSize = Size(childSize.width, childSize.height - bottomOverhang); + } + } + + if (_rectContains(outputRect, result & childSize)) { + return result & childSize; + } + } + return defaultResult; } } @@ -241,60 +344,68 @@ class WindowPositioner { extension on WindowPositionerAnchor { WindowPositionerAnchor flipX() { switch (this) { - case WindowPositionerAnchor.topLeft: - return WindowPositionerAnchor.topRight; - case WindowPositionerAnchor.topRight: - return WindowPositionerAnchor.topLeft; + case WindowPositionerAnchor.center: + return WindowPositionerAnchor.center; + case WindowPositionerAnchor.top: + return WindowPositionerAnchor.top; + case WindowPositionerAnchor.bottom: + return WindowPositionerAnchor.bottom; case WindowPositionerAnchor.left: return WindowPositionerAnchor.right; case WindowPositionerAnchor.right: return WindowPositionerAnchor.left; + case WindowPositionerAnchor.topLeft: + return WindowPositionerAnchor.topRight; case WindowPositionerAnchor.bottomLeft: return WindowPositionerAnchor.bottomRight; + case WindowPositionerAnchor.topRight: + return WindowPositionerAnchor.topLeft; case WindowPositionerAnchor.bottomRight: return WindowPositionerAnchor.bottomLeft; - default: - return this; } } WindowPositionerAnchor flipY() { switch (this) { - case WindowPositionerAnchor.topLeft: - return WindowPositionerAnchor.bottomLeft; + case WindowPositionerAnchor.center: + return WindowPositionerAnchor.center; case WindowPositionerAnchor.top: return WindowPositionerAnchor.bottom; - case WindowPositionerAnchor.topRight: - return WindowPositionerAnchor.bottomRight; - case WindowPositionerAnchor.bottomLeft: - return WindowPositionerAnchor.topLeft; case WindowPositionerAnchor.bottom: return WindowPositionerAnchor.top; + case WindowPositionerAnchor.left: + return WindowPositionerAnchor.left; + case WindowPositionerAnchor.right: + return WindowPositionerAnchor.right; + case WindowPositionerAnchor.topLeft: + return WindowPositionerAnchor.bottomLeft; + case WindowPositionerAnchor.bottomLeft: + return WindowPositionerAnchor.topLeft; + case WindowPositionerAnchor.topRight: + return WindowPositionerAnchor.bottomRight; case WindowPositionerAnchor.bottomRight: return WindowPositionerAnchor.topRight; - default: - return this; } } Offset offsetFor(Size size) { switch (this) { - case WindowPositionerAnchor.topLeft: - return Offset.zero; + case WindowPositionerAnchor.center: + return Offset(-size.width / 2.0, -size.height / 2.0); case WindowPositionerAnchor.top: return Offset(-size.width / 2.0, 0.0); - case WindowPositionerAnchor.topRight: - return Offset(-size.width, 0.0); + case WindowPositionerAnchor.bottom: + return Offset(-size.width / 2.0, -1.0 * size.height); case WindowPositionerAnchor.left: return Offset(0.0, -size.height / 2.0); - case WindowPositionerAnchor.center: - return Offset(-size.width / 2.0, -size.height / 2.0); case WindowPositionerAnchor.right: return Offset(-1.0 * size.width, -size.height / 2.0); + case WindowPositionerAnchor.topLeft: + return Offset.zero; case WindowPositionerAnchor.bottomLeft: return Offset(0.0, -1.0 * size.height); - case WindowPositionerAnchor.bottom: - return Offset(-size.width / 2.0, -1.0 * size.height); + case WindowPositionerAnchor.topRight: + return Offset(-size.width, 0.0); case WindowPositionerAnchor.bottomRight: return Offset(-1.0 * size.width, -1.0 * size.height); } @@ -302,22 +413,22 @@ extension on WindowPositionerAnchor { Offset anchorPositionFor(Rect rect) { switch (this) { - case WindowPositionerAnchor.topLeft: - return rect.topLeft; + case WindowPositionerAnchor.center: + return rect.center; case WindowPositionerAnchor.top: return rect.topCenter; - case WindowPositionerAnchor.topRight: - return rect.topRight; + case WindowPositionerAnchor.bottom: + return rect.bottomCenter; case WindowPositionerAnchor.left: return rect.centerLeft; - case WindowPositionerAnchor.center: - return rect.center; case WindowPositionerAnchor.right: return rect.centerRight; + case WindowPositionerAnchor.topLeft: + return rect.topLeft; case WindowPositionerAnchor.bottomLeft: return rect.bottomLeft; - case WindowPositionerAnchor.bottom: - return rect.bottomCenter; + case WindowPositionerAnchor.topRight: + return rect.topRight; case WindowPositionerAnchor.bottomRight: return rect.bottomRight; } diff --git a/packages/flutter/lib/src/widgets/window_win32.dart b/packages/flutter/lib/src/widgets/window_win32.dart index 556547d8bf12e..0830358fcdb59 100644 --- a/packages/flutter/lib/src/widgets/window_win32.dart +++ b/packages/flutter/lib/src/widgets/window_win32.dart @@ -114,7 +114,6 @@ class RegularWindowControllerWin32 extends RegularWindowController }) : _owner = owner, _delegate = delegate, super.empty() { - owner.addMessageHandler(this); final Pointer<_WindowCreationRequest> request = ffi.calloc<_WindowCreationRequest>() ..ref.width = size.width @@ -128,7 +127,8 @@ class RegularWindowControllerWin32 extends RegularWindowController final FlutterView flutterView = WidgetsBinding.instance.platformDispatcher.views.firstWhere( (FlutterView view) => view.viewId == viewId, ); - setView(flutterView); + view = flutterView; + owner.addMessageHandler(this); } @override @@ -199,7 +199,6 @@ class RegularWindowControllerWin32 extends RegularWindowController return; } _destroyWindow(getWindowHandle()); - ; _destroyed = true; _delegate.onWindowDestroyed(); _owner.removeMessageHandler(this); diff --git a/packages/flutter/test/widgets/window_positioner_test.dart b/packages/flutter/test/widgets/window_positioner_test.dart new file mode 100644 index 0000000000000..f7142a67a4b35 --- /dev/null +++ b/packages/flutter/test/widgets/window_positioner_test.dart @@ -0,0 +1,554 @@ +// 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. + +import 'dart:ui'; +import 'package:flutter/src/widgets/window_positioning.dart'; +import 'package:flutter_test/flutter_test.dart'; + +extension WindowPositionerAnchorExtension on WindowPositionerAnchor { + Offset anchorPositionFor(Rect rect) { + switch (this) { + case WindowPositionerAnchor.center: + return rect.center; + case WindowPositionerAnchor.top: + return rect.topCenter; + case WindowPositionerAnchor.bottom: + return rect.bottomCenter; + case WindowPositionerAnchor.left: + return rect.centerLeft; + case WindowPositionerAnchor.right: + return rect.centerRight; + case WindowPositionerAnchor.topLeft: + return rect.topLeft; + case WindowPositionerAnchor.bottomLeft: + return rect.bottomLeft; + case WindowPositionerAnchor.topRight: + return rect.topRight; + case WindowPositionerAnchor.bottomRight: + return rect.bottomRight; + } + } +} + +void main() { + group('WindowPlacementTest', () { + const Rect clientDisplayArea = Rect.fromLTWH(0, 0, 800, 600); + const Size clientParentSize = Size(400, 300); + const Size clientChildSize = Size(100, 50); + final Offset clientParentPosition = Offset( + (clientDisplayArea.width - clientParentSize.width) / 2, + (clientDisplayArea.height - clientParentSize.height) / 2, + ); + + final Rect clientParentRect = Rect.fromLTWH( + clientParentPosition.dx, + clientParentPosition.dy, + clientParentSize.width, + clientParentSize.height, + ); + + const Rect displayArea = Rect.fromLTWH(0, 0, 640, 480); + const Size parentSize = Size(600, 400); + const Size childSize = Size(300, 300); + const Rect rectangleNearRhs = Rect.fromLTWH(590, 20, 10, 20); + const Rect rectangleNearLeftSide = Rect.fromLTWH(0, 20, 20, 20); + const Rect rectangleNearAllSides = Rect.fromLTWH(0, 20, 600, 380); + const Rect rectangleNearBottom = Rect.fromLTWH(20, 380, 20, 20); + const Rect rectangleNearBothBottomRight = Rect.fromLTWH(400, 380, 200, 20); + + final Offset parentPosition = Offset( + (displayArea.width - parentSize.width) / 2, + (displayArea.height - parentSize.height) / 2, + ); + + final Rect parentRect = Rect.fromLTWH( + parentPosition.dx, + parentPosition.dy, + parentSize.width, + parentSize.height, + ); + + Rect anchorRectFor(Rect rect) => rect.translate(parentPosition.dx, parentPosition.dy); + Offset onTopEdge(Rect rect, Size childSize) => rect.topLeft - Offset(0, childSize.height); + Offset onLeftEdge(Rect rect, Size childSize) => rect.topLeft - Offset(childSize.width, 0); + + test('Client anchors to parent given anchor rectangle right of parent', () { + const double rectSize = 10.0; + final Rect overlappingRight = Rect.fromCenter( + center: clientParentRect.topRight.translate(-rectSize / 2, clientParentRect.height / 2), + width: rectSize, + height: rectSize, + ); + + const WindowPositioner positioner = WindowPositioner( + parentAnchor: WindowPositionerAnchor.topRight, + childAnchor: WindowPositionerAnchor.topLeft, + constraintAdjustment: { + WindowPositionerConstraintAdjustment.slideY, + WindowPositionerConstraintAdjustment.resizeX, + }, + ); + + final Rect childRect = positioner.placeWindow( + childSize: clientChildSize, + anchorRect: overlappingRight, + parentRect: clientParentRect, + outputRect: clientDisplayArea, + ); + + final Offset expectedPosition = overlappingRight.topRight; + + expect(childRect.topLeft, expectedPosition); + expect(childRect.size, clientChildSize); + }); + + test('Client anchors to parent given anchor rectangle above parent', () { + const double rectSize = 10.0; + final Rect overlappingAbove = Rect.fromCenter( + center: clientParentRect.topCenter.translate(0, -rectSize / 2), + width: rectSize, + height: rectSize, + ); + + const WindowPositioner positioner = WindowPositioner( + parentAnchor: WindowPositionerAnchor.topRight, + childAnchor: WindowPositionerAnchor.bottomRight, + constraintAdjustment: { + WindowPositionerConstraintAdjustment.slideX, + }, + ); + + final Rect childRect = positioner.placeWindow( + childSize: clientChildSize, + anchorRect: overlappingAbove, + parentRect: clientParentRect, + outputRect: clientDisplayArea, + ); + + final Offset expectedPosition = + overlappingAbove.bottomRight - Offset(clientChildSize.width, clientChildSize.height); + + expect(childRect.topLeft, expectedPosition); + expect(childRect.size, clientChildSize); + }); + + test('Client anchors to parent given offset right of parent', () { + const double rectSize = 10.0; + final Rect midRight = Rect.fromLTWH( + clientParentRect.right - rectSize, + clientParentRect.center.dy, + rectSize, + rectSize, + ); + + const WindowPositioner positioner = WindowPositioner( + parentAnchor: WindowPositionerAnchor.topRight, + childAnchor: WindowPositionerAnchor.topLeft, + offset: Offset(rectSize, 0), + constraintAdjustment: { + WindowPositionerConstraintAdjustment.slideY, + WindowPositionerConstraintAdjustment.resizeX, + }, + ); + + final Rect childRect = positioner.placeWindow( + childSize: clientChildSize, + anchorRect: midRight, + parentRect: clientParentRect, + outputRect: clientDisplayArea, + ); + + final Offset expectedPosition = midRight.topRight; + + expect(childRect.topLeft, expectedPosition); + expect(childRect.size, clientChildSize); + }); + + test('Client anchors to parent given offset above parent', () { + const double rectSize = 10.0; + final Rect midTop = Rect.fromLTWH( + clientParentRect.center.dx, + clientParentRect.top, + rectSize, + rectSize, + ); + + const WindowPositioner positioner = WindowPositioner( + parentAnchor: WindowPositionerAnchor.topRight, + childAnchor: WindowPositionerAnchor.bottomRight, + offset: Offset(0, -rectSize), + constraintAdjustment: { + WindowPositionerConstraintAdjustment.slideX, + }, + ); + + final Rect childRect = positioner.placeWindow( + childSize: clientChildSize, + anchorRect: midTop, + parentRect: clientParentRect, + outputRect: clientDisplayArea, + ); + + final Offset expectedPosition = + clientParentPosition + + Offset(clientParentSize.width / 2 + rectSize, 0) - + Offset(clientChildSize.width, clientChildSize.height); + + expect(childRect.topLeft, expectedPosition); + expect(childRect.size, clientChildSize); + }); + + test('Client anchors to parent given anchor rectangle and offset below left parent', () { + const double rectSize = 10.0; + final Rect belowLeft = Rect.fromLTWH( + clientParentRect.left - rectSize, + clientParentRect.bottom, + rectSize, + rectSize, + ); + + const WindowPositioner positioner = WindowPositioner( + parentAnchor: WindowPositionerAnchor.bottomLeft, + childAnchor: WindowPositionerAnchor.topRight, + offset: Offset(-rectSize, rectSize), + constraintAdjustment: { + WindowPositionerConstraintAdjustment.resizeX, + WindowPositionerConstraintAdjustment.resizeY, + }, + ); + + final Rect childRect = positioner.placeWindow( + childSize: clientChildSize, + anchorRect: belowLeft, + parentRect: clientParentRect, + outputRect: clientDisplayArea, + ); + + final Offset expectedPosition = + clientParentRect.bottomLeft - Offset(clientChildSize.width, 0); + + expect(childRect.topLeft, expectedPosition); + expect(childRect.size, clientChildSize); + }); + + group('Can attach by every anchor given no constraint adjustment', () { + for (final WindowPositionerAnchor parentAnchor in WindowPositionerAnchor.values) { + for (final WindowPositionerAnchor childAnchor in WindowPositionerAnchor.values) { + test('parent: $parentAnchor, child: $childAnchor', () { + final Rect anchorRect = anchorRectFor(const Rect.fromLTWH(100, 50, 20, 20)); + final WindowPositioner positioner = WindowPositioner( + parentAnchor: parentAnchor, + childAnchor: childAnchor, + ); + + final Rect childRect = positioner.placeWindow( + childSize: childSize, + anchorRect: anchorRect, + parentRect: parentRect, + outputRect: displayArea, + ); + + expect( + childAnchor.anchorPositionFor(childRect), + parentAnchor.anchorPositionFor(anchorRect), + ); + }); + } + } + }); + + test('Placement is flipped given anchor rectangle near right side and offset', () { + const double xOffset = 42.0; + const double yOffset = 13.0; + const WindowPositioner positioner = WindowPositioner( + parentAnchor: WindowPositionerAnchor.topRight, + childAnchor: WindowPositionerAnchor.topLeft, + offset: Offset(xOffset, yOffset), + constraintAdjustment: { + WindowPositionerConstraintAdjustment.flipX, + }, + ); + + final Rect anchorRect = anchorRectFor(rectangleNearRhs); + final Rect childRect = positioner.placeWindow( + childSize: childSize, + anchorRect: anchorRect, + parentRect: parentRect, + outputRect: displayArea, + ); + + final Offset expectedPosition = + onLeftEdge(anchorRect, childSize) + const Offset(-xOffset, yOffset); + + expect(childRect.topLeft, expectedPosition); + }); + + test('Placement is flipped given anchor rectangle near bottom and offset', () { + const double xOffset = 42.0; + const double yOffset = 13.0; + const WindowPositioner positioner = WindowPositioner( + parentAnchor: WindowPositionerAnchor.bottomLeft, + childAnchor: WindowPositionerAnchor.topLeft, + offset: Offset(xOffset, yOffset), + constraintAdjustment: { + WindowPositionerConstraintAdjustment.flipY, + }, + ); + + final Rect anchorRect = anchorRectFor(rectangleNearBottom); + final Rect childRect = positioner.placeWindow( + childSize: childSize, + anchorRect: anchorRect, + parentRect: parentRect, + outputRect: displayArea, + ); + + final Offset expectedPosition = + onTopEdge(anchorRect, childSize) + const Offset(xOffset, -yOffset); + + expect(childRect.topLeft, expectedPosition); + }); + + test('Placement is flipped both ways given anchor rectangle near bottom right and offset', () { + const double xOffset = 42.0; + const double yOffset = 13.0; + const WindowPositioner positioner = WindowPositioner( + parentAnchor: WindowPositionerAnchor.bottomRight, + childAnchor: WindowPositionerAnchor.topLeft, + offset: Offset(xOffset, yOffset), + constraintAdjustment: { + WindowPositionerConstraintAdjustment.flipX, + WindowPositionerConstraintAdjustment.flipY, + }, + ); + + final Rect anchorRect = anchorRectFor(rectangleNearBothBottomRight); + final Rect childRect = positioner.placeWindow( + childSize: childSize, + anchorRect: anchorRect, + parentRect: parentRect, + outputRect: displayArea, + ); + + final Offset expectedPosition = + anchorRect.topLeft - + Offset(childSize.width, childSize.height) - + const Offset(xOffset, yOffset); + + expect(childRect.topLeft, expectedPosition); + }); + + test('Placement can slide in X given anchor rectangle near right side', () { + const WindowPositioner positioner = WindowPositioner( + parentAnchor: WindowPositionerAnchor.topRight, + childAnchor: WindowPositionerAnchor.topLeft, + constraintAdjustment: { + WindowPositionerConstraintAdjustment.slideX, + }, + ); + + final Rect anchorRect = anchorRectFor(rectangleNearRhs); + final Rect childRect = positioner.placeWindow( + childSize: childSize, + anchorRect: anchorRect, + parentRect: parentRect, + outputRect: displayArea, + ); + + expect(childRect.topLeft.dx, displayArea.right - childSize.width); + }); + + test('Placement can slide in X given anchor rectangle near left side', () { + const WindowPositioner positioner = WindowPositioner( + parentAnchor: WindowPositionerAnchor.topLeft, + childAnchor: WindowPositionerAnchor.topRight, + constraintAdjustment: { + WindowPositionerConstraintAdjustment.slideX, + }, + ); + + final Rect anchorRect = anchorRectFor(rectangleNearLeftSide); + final Rect childRect = positioner.placeWindow( + childSize: childSize, + anchorRect: anchorRect, + parentRect: parentRect, + outputRect: displayArea, + ); + + expect(childRect.topLeft.dx, displayArea.left); + }); + + test('Placement can slide in Y given anchor rectangle near bottom', () { + const WindowPositioner positioner = WindowPositioner( + parentAnchor: WindowPositionerAnchor.bottomLeft, + childAnchor: WindowPositionerAnchor.topLeft, + constraintAdjustment: { + WindowPositionerConstraintAdjustment.slideY, + }, + ); + + final Rect anchorRect = anchorRectFor(rectangleNearBottom); + final Rect childRect = positioner.placeWindow( + childSize: childSize, + anchorRect: anchorRect, + parentRect: parentRect, + outputRect: displayArea, + ); + + expect(childRect.topLeft.dy, displayArea.bottom - childSize.height); + }); + + test('Placement can slide in Y given anchor rectangle near top', () { + const WindowPositioner positioner = WindowPositioner( + parentAnchor: WindowPositionerAnchor.topLeft, + childAnchor: WindowPositionerAnchor.bottomLeft, + constraintAdjustment: { + WindowPositionerConstraintAdjustment.slideY, + }, + ); + + final Rect anchorRect = anchorRectFor(rectangleNearAllSides); + final Rect childRect = positioner.placeWindow( + childSize: childSize, + anchorRect: anchorRect, + parentRect: parentRect, + outputRect: displayArea, + ); + + expect(childRect.topLeft.dy, displayArea.top); + }); + + test('Placement can slide in X and Y given anchor rectangle near bottom right and offset', () { + const WindowPositioner positioner = WindowPositioner( + parentAnchor: WindowPositionerAnchor.bottomLeft, + childAnchor: WindowPositionerAnchor.topLeft, + constraintAdjustment: { + WindowPositionerConstraintAdjustment.slideX, + WindowPositionerConstraintAdjustment.slideY, + }, + ); + + final Rect anchorRect = anchorRectFor(rectangleNearBothBottomRight); + final Rect childRect = positioner.placeWindow( + childSize: childSize, + anchorRect: anchorRect, + parentRect: parentRect, + outputRect: displayArea, + ); + + final Offset expectedPosition = Offset( + displayArea.right - childSize.width, + displayArea.bottom - childSize.height, + ); + + expect(childRect.topLeft, expectedPosition); + }); + + test('Placement can resize in X given anchor rectangle near right side', () { + const WindowPositioner positioner = WindowPositioner( + parentAnchor: WindowPositionerAnchor.topRight, + childAnchor: WindowPositionerAnchor.topLeft, + constraintAdjustment: { + WindowPositionerConstraintAdjustment.resizeX, + }, + ); + + final Rect anchorRect = anchorRectFor(rectangleNearRhs); + final Rect childRect = positioner.placeWindow( + childSize: childSize, + anchorRect: anchorRect, + parentRect: parentRect, + outputRect: displayArea, + ); + + expect(childRect.width, displayArea.right - (anchorRect.left + anchorRect.width)); + }); + + test('Placement can resize in X given anchor rectangle near left side', () { + const WindowPositioner positioner = WindowPositioner( + parentAnchor: WindowPositionerAnchor.topLeft, + childAnchor: WindowPositionerAnchor.topRight, + constraintAdjustment: { + WindowPositionerConstraintAdjustment.resizeX, + }, + ); + + final Rect anchorRect = anchorRectFor(rectangleNearLeftSide); + final Rect childRect = positioner.placeWindow( + childSize: childSize, + anchorRect: anchorRect, + parentRect: parentRect, + outputRect: displayArea, + ); + + expect(childRect.width, anchorRect.left - displayArea.left); + }); + + test('Placement can resize in Y given anchor rectangle near bottom', () { + const WindowPositioner positioner = WindowPositioner( + parentAnchor: WindowPositionerAnchor.bottomLeft, + childAnchor: WindowPositionerAnchor.topLeft, + constraintAdjustment: { + WindowPositionerConstraintAdjustment.resizeY, + }, + ); + + final Rect anchorRect = anchorRectFor(rectangleNearAllSides); + final Rect childRect = positioner.placeWindow( + childSize: childSize, + anchorRect: anchorRect, + parentRect: parentRect, + outputRect: displayArea, + ); + + expect(childRect.height, displayArea.bottom - (anchorRect.top + anchorRect.height)); + }); + + test('Placement can resize in Y given anchor rectangle near top', () { + const WindowPositioner positioner = WindowPositioner( + parentAnchor: WindowPositionerAnchor.topLeft, + childAnchor: WindowPositionerAnchor.bottomLeft, + constraintAdjustment: { + WindowPositionerConstraintAdjustment.resizeY, + }, + ); + + final Rect anchorRect = anchorRectFor(rectangleNearAllSides); + final Rect childRect = positioner.placeWindow( + childSize: childSize, + anchorRect: anchorRect, + parentRect: parentRect, + outputRect: displayArea, + ); + + expect(childRect.height, anchorRect.top - displayArea.top); + }); + + test('Placement can resize in X and Y given anchor rectangle near bottom right and offset', () { + const WindowPositioner positioner = WindowPositioner( + parentAnchor: WindowPositionerAnchor.bottomRight, + childAnchor: WindowPositionerAnchor.topLeft, + constraintAdjustment: { + WindowPositionerConstraintAdjustment.resizeX, + WindowPositionerConstraintAdjustment.resizeY, + }, + ); + + final Rect anchorRect = anchorRectFor(rectangleNearBothBottomRight); + final Rect childRect = positioner.placeWindow( + childSize: childSize, + anchorRect: anchorRect, + parentRect: parentRect, + outputRect: displayArea, + ); + + final Size expectedSize = Size( + displayArea.right - (anchorRect.left + anchorRect.width), + displayArea.bottom - (anchorRect.top + anchorRect.height), + ); + + expect(childRect.size, expectedSize); + }); + }); +}