mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Add tooltip windows to the windowing API alongside the window positioning logic (#177404)
## What's new?
- Added tooltips to the windowing API, but they have no implementation
for the time being
- Added the `WindowPostiioner` logic to the windowing API
- Wrote a lot of tests 🧪
## Pre-launch Checklist
- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.
This commit is contained in:
parent
2778dfd2cb
commit
084deda0ba
109
examples/api/lib/widgets/windows/tooltip.0.dart
Normal file
109
examples/api/lib/widgets/windows/tooltip.0.dart
Normal file
@ -0,0 +1,109 @@
|
||||
// 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.
|
||||
|
||||
// TODO(mattkae): remove invalid_use_of_internal_member ignore comment when this API is stable.
|
||||
// See: https://github.com/flutter/flutter/issues/177586
|
||||
// TODO(mattkae): refactor this example for better widget position tracking
|
||||
// This positioning logic is simpler than you might want in production. See https://github.com/flutter/flutter/issues/178829.
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
// ignore_for_file: implementation_imports
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/src/widgets/_window.dart';
|
||||
import 'package:flutter/src/widgets/_window_positioner.dart';
|
||||
|
||||
void main() {
|
||||
try {
|
||||
runWidget(
|
||||
RegularWindow(
|
||||
controller: RegularWindowController(
|
||||
preferredSize: const Size(800, 600),
|
||||
preferredConstraints: const BoxConstraints(minWidth: 640, minHeight: 480),
|
||||
title: 'Example Window',
|
||||
),
|
||||
child: const MaterialApp(home: MyApp()),
|
||||
),
|
||||
);
|
||||
} on UnsupportedError catch (e) {
|
||||
// TODO(mattkae): Remove this catch block when Windows tooltips are supported in tests.
|
||||
// For now, we need to catch the error so that the API smoke tests pass.
|
||||
runApp(
|
||||
MaterialApp(
|
||||
home: Scaffold(body: Center(child: Text(e.message ?? 'Unsupported'))),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MyApp extends StatefulWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
State<MyApp> createState() {
|
||||
return _MyAppState();
|
||||
}
|
||||
}
|
||||
|
||||
class _MyAppState extends State<MyApp> {
|
||||
final GlobalKey _key = GlobalKey();
|
||||
TooltipWindowController? _tooltipController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<Widget> children = <Widget>[
|
||||
Text(
|
||||
key: _key,
|
||||
'Hover Me',
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
];
|
||||
|
||||
if (_tooltipController != null) {
|
||||
children.add(
|
||||
TooltipWindow(
|
||||
controller: _tooltipController!,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
color: Colors.black,
|
||||
child: const Text('This is a tooltip', style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return MouseRegion(
|
||||
onEnter: (_) => setState(
|
||||
() => _tooltipController = TooltipWindowController(
|
||||
parent: WindowScope.of(context),
|
||||
anchorRect: _getAnchorRect()!,
|
||||
positioner: const WindowPositioner(
|
||||
parentAnchor: WindowPositionerAnchor.right,
|
||||
childAnchor: WindowPositionerAnchor.left,
|
||||
),
|
||||
),
|
||||
),
|
||||
onExit: (_) => setState(() {
|
||||
_tooltipController?.destroy();
|
||||
_tooltipController = null;
|
||||
}),
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
color: _tooltipController != null ? Colors.blueAccent : Colors.blue,
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(children: children),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Rect? _getAnchorRect() {
|
||||
final RenderBox? renderBox = _key.currentContext?.findRenderObject() as RenderBox?;
|
||||
if (renderBox != null) {
|
||||
final Offset position = renderBox.localToGlobal(Offset.zero);
|
||||
final Size size = renderBox.size;
|
||||
return position & size; // creates a Rect
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
12
examples/api/test/widgets/windows/tooltip.0_test.dart
Normal file
12
examples/api/test/widgets/windows/tooltip.0_test.dart
Normal file
@ -0,0 +1,12 @@
|
||||
// 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 'package:flutter_api_samples/widgets/windows/tooltip.0.dart' as example;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Calling tooltip main returns normally', (WidgetTester tester) async {
|
||||
expect(() => example.main(), returnsNormally);
|
||||
});
|
||||
}
|
||||
@ -101,6 +101,7 @@ class _WindowsTable extends StatelessWidget {
|
||||
context: context,
|
||||
controller: dialog,
|
||||
),
|
||||
TooltipWindowController() => null,
|
||||
};
|
||||
}
|
||||
|
||||
@ -108,6 +109,7 @@ class _WindowsTable extends StatelessWidget {
|
||||
return switch (controller) {
|
||||
RegularWindowController() => 'Regular',
|
||||
DialogWindowController() => 'Dialog',
|
||||
TooltipWindowController() => 'Tooltip',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -39,6 +39,7 @@ class WindowContent extends StatelessWidget {
|
||||
controller: dialog,
|
||||
child: MaterialApp(home: DialogWindowContent(window: dialog)),
|
||||
),
|
||||
TooltipWindowController() => throw UnimplementedError(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,6 +20,7 @@ import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../foundation/_features.dart';
|
||||
import '_window_io.dart' if (dart.library.js_interop) '_window_web.dart' as window_impl;
|
||||
import '_window_positioner.dart';
|
||||
import 'basic.dart';
|
||||
import 'binding.dart';
|
||||
import 'framework.dart';
|
||||
@ -620,6 +621,148 @@ abstract class DialogWindowController extends BaseWindowController {
|
||||
void setMinimized(bool minimized);
|
||||
}
|
||||
|
||||
/// Delegate class for tooltip window controller.
|
||||
///
|
||||
/// {@macro flutter.widgets.windowing.experimental}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [TooltipWindowController], the controller that creates and manages tooltip windows.
|
||||
/// * [TooltipWindow], the widget for a tooltip window.
|
||||
/// * [RegularWindowControllerDelegate], the delegate for regular window controllers.
|
||||
mixin class TooltipWindowControllerDelegate {
|
||||
/// Invoked when the user attempts to close the window.
|
||||
///
|
||||
/// The default implementation destroys the window. Subclasses
|
||||
/// can override the behavior to delay or prevent the window from closing.
|
||||
///
|
||||
/// {@macro flutter.widgets.windowing.experimental}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [onWindowDestroyed], which is invoked after the window is closed.
|
||||
@internal
|
||||
void onWindowCloseRequested(TooltipWindowController controller) {
|
||||
if (!isWindowingEnabled) {
|
||||
throw UnsupportedError(_kWindowingDisabledErrorMessage);
|
||||
}
|
||||
|
||||
controller.destroy();
|
||||
}
|
||||
|
||||
/// Invoked after the window is closed.
|
||||
///
|
||||
/// {@macro flutter.widgets.windowing.experimental}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [onWindowCloseRequested], which is invoked when the user attempts to close the window.
|
||||
@internal
|
||||
void onWindowDestroyed() {
|
||||
if (!isWindowingEnabled) {
|
||||
throw UnsupportedError(_kWindowingDisabledErrorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A controller for a tooltip window.
|
||||
///
|
||||
/// A tooltip window is a small window that displays brief, informative text
|
||||
/// when a user hovers over or focuses on a UI element. Tooltip windows are
|
||||
/// typically used to provide additional context or explanations for UI elements
|
||||
/// without cluttering the main interface. As such, it may not receive input
|
||||
/// focus from the user. It will however stay open when another window receives
|
||||
/// input focus.
|
||||
///
|
||||
/// This class does not interact with the widget tree. Instead, it is typically
|
||||
/// provided to the [TooltipWindow] widget, which renders the content inside the
|
||||
/// tooltip window.
|
||||
///
|
||||
/// The user of this class is responsible for managing the lifecycle of the window.
|
||||
/// When the window is no longer needed, the user should call [destroy] on this
|
||||
/// controller to release the resources associated with the window.
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// An example usage of [TooltipWindowController] looks like:
|
||||
///
|
||||
/// ** See code in examples/api/lib/widgets/windows/tooltip.0.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// Children of a [TooltipWindow] widget can access the [TooltipWindowController]
|
||||
/// via the [WindowScope] inherited widget.
|
||||
///
|
||||
/// {@macro flutter.widgets.windowing.experimental}
|
||||
abstract class TooltipWindowController extends BaseWindowController {
|
||||
/// Creates a [TooltipWindowController] with the provided properties.
|
||||
///
|
||||
/// Upon construction, the window is created by the platform.
|
||||
///
|
||||
/// The [parent] argument specifies the parent window of this tooltip.
|
||||
///
|
||||
/// The [anchorRect] argument specifies the rectangle in the parent's coordinate
|
||||
/// space to which the tooltip is anchored.
|
||||
///
|
||||
/// The [positioner] argument specifies how the tooltip should be positioned
|
||||
/// relative to the [anchorRect].
|
||||
///
|
||||
/// {@macro flutter.widgets.windowing.constraints}
|
||||
///
|
||||
/// The [delegate] argument can be used to listen to the window's
|
||||
/// lifecycle. For example, it can be used to save state before
|
||||
/// a window is closed.
|
||||
///
|
||||
/// {@macro flutter.widgets.windowing.experimental}
|
||||
factory TooltipWindowController({
|
||||
required BaseWindowController parent,
|
||||
required Rect anchorRect,
|
||||
required WindowPositioner positioner,
|
||||
BoxConstraints? preferredConstraints,
|
||||
TooltipWindowControllerDelegate? delegate,
|
||||
}) {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
final WindowingOwner owner = WidgetsBinding.instance.windowingOwner;
|
||||
final TooltipWindowController controller = owner.createTooltipWindowController(
|
||||
parent: parent,
|
||||
preferredConstraints: preferredConstraints ?? const BoxConstraints(),
|
||||
delegate: delegate ?? TooltipWindowControllerDelegate(),
|
||||
anchorRect: anchorRect,
|
||||
positioner: positioner,
|
||||
);
|
||||
return controller;
|
||||
}
|
||||
|
||||
/// Creates an empty [TooltipWindowController].
|
||||
///
|
||||
/// This method is only intended to be used by subclasses of the
|
||||
/// [TooltipWindowController].
|
||||
///
|
||||
/// Users who want to instantiate a new [TooltipWindowController] should
|
||||
/// always use the factory method to create a controller that is valid
|
||||
/// for their particular platform.
|
||||
///
|
||||
/// {@macro flutter.widgets.windowing.experimental}
|
||||
@internal
|
||||
@protected
|
||||
TooltipWindowController.empty();
|
||||
|
||||
/// The parent controller of this tooltip.
|
||||
///
|
||||
/// The tooltip will be destroyed if its parent is destroyed.
|
||||
BaseWindowController get parent;
|
||||
|
||||
/// Request change to the constraints of the window.
|
||||
///
|
||||
/// The [constraints] describes the new constraints that the window should
|
||||
/// satisfy. If the constraints disagree with the current size of the window,
|
||||
/// the platform might resize the window to satisfy the new constraints.
|
||||
///
|
||||
/// The platform is free to ignore this request.
|
||||
///
|
||||
/// {@macro flutter.widgets.windowing.experimental}
|
||||
@internal
|
||||
void setConstraints(BoxConstraints constraints);
|
||||
}
|
||||
|
||||
/// [WindowingOwner] is responsible for creating and managing window controllers.
|
||||
///
|
||||
/// A custom implementation can be provided by setting [WidgetsBinding.windowingOwner].
|
||||
@ -657,6 +800,22 @@ abstract class WindowingOwner {
|
||||
BaseWindowController? parent,
|
||||
String? title,
|
||||
});
|
||||
|
||||
/// Creates a [TooltipWindowController] with the provided properties.
|
||||
///
|
||||
/// Most app developers should use [TooltipWindowController]'s constructor
|
||||
/// instead of calling this method directly. This method allows platforms
|
||||
/// to inject platform-specific logic.
|
||||
///
|
||||
/// {@macro flutter.widgets.windowing.experimental}
|
||||
@internal
|
||||
TooltipWindowController createTooltipWindowController({
|
||||
required TooltipWindowControllerDelegate delegate,
|
||||
required BoxConstraints preferredConstraints,
|
||||
required Rect anchorRect,
|
||||
required WindowPositioner positioner,
|
||||
required BaseWindowController parent,
|
||||
});
|
||||
}
|
||||
|
||||
/// Creates default windowing owner for standard desktop embedders.
|
||||
@ -702,6 +861,17 @@ class _WindowingOwnerUnsupported extends WindowingOwner {
|
||||
}) {
|
||||
throw UnsupportedError(errorMessage);
|
||||
}
|
||||
|
||||
@override
|
||||
TooltipWindowController createTooltipWindowController({
|
||||
required TooltipWindowControllerDelegate delegate,
|
||||
required BoxConstraints preferredConstraints,
|
||||
required Rect anchorRect,
|
||||
required WindowPositioner positioner,
|
||||
required BaseWindowController parent,
|
||||
}) {
|
||||
throw UnimplementedError(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/// The [RegularWindow] widget provides a way to render a regular window in the
|
||||
@ -888,6 +1058,50 @@ class DialogWindow extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
@internal
|
||||
class TooltipWindow extends StatelessWidget {
|
||||
/// Creates a tooltip window widget.
|
||||
///
|
||||
/// The [controller] creates the native backing window into which the
|
||||
/// [child] widget is rendered.
|
||||
///
|
||||
/// It is up to the caller to destroy the window by calling
|
||||
/// [TooltipWindowController.destroy] when the window is no longer needed.
|
||||
///
|
||||
/// {@macro flutter.widgets.windowing.experimental}
|
||||
@internal
|
||||
TooltipWindow({super.key, required this.controller, required this.child}) {
|
||||
if (!isWindowingEnabled) {
|
||||
throw UnsupportedError(_kWindowingDisabledErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/// Controller for this widget.
|
||||
///
|
||||
/// {@macro flutter.widgets.windowing.experimental}
|
||||
@internal
|
||||
final TooltipWindowController controller;
|
||||
|
||||
/// The content rendered into this window.
|
||||
///
|
||||
/// {@macro flutter.widgets.windowing.experimental}
|
||||
@internal
|
||||
final Widget child;
|
||||
|
||||
/// {@macro flutter.widgets.windowing.experimental}
|
||||
@internal
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListenableBuilder(
|
||||
listenable: controller,
|
||||
builder: (BuildContext context, Widget? widget) => WindowScope(
|
||||
controller: controller,
|
||||
child: View(view: controller.rootView, child: child),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum _WindowControllerAspect { contentSize, title, activated, maximized, minimized, fullscreen }
|
||||
|
||||
/// Provides descendants with access to the [BaseWindowController] associated with
|
||||
@ -1019,6 +1233,7 @@ class WindowScope extends InheritedModel<_WindowControllerAspect> {
|
||||
return switch (controller) {
|
||||
RegularWindowController() => controller.title,
|
||||
DialogWindowController() => controller.title,
|
||||
TooltipWindowController() => '',
|
||||
};
|
||||
}
|
||||
|
||||
@ -1040,6 +1255,7 @@ class WindowScope extends InheritedModel<_WindowControllerAspect> {
|
||||
return switch (controller) {
|
||||
RegularWindowController() => controller.title,
|
||||
DialogWindowController() => controller.title,
|
||||
TooltipWindowController() => '',
|
||||
};
|
||||
}
|
||||
|
||||
@ -1062,6 +1278,7 @@ class WindowScope extends InheritedModel<_WindowControllerAspect> {
|
||||
return switch (controller) {
|
||||
RegularWindowController() => controller.isActivated,
|
||||
DialogWindowController() => controller.isActivated,
|
||||
TooltipWindowController() => false,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1084,6 +1301,7 @@ class WindowScope extends InheritedModel<_WindowControllerAspect> {
|
||||
return switch (controller) {
|
||||
RegularWindowController() => controller.isActivated,
|
||||
DialogWindowController() => controller.isActivated,
|
||||
TooltipWindowController() => false,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1106,6 +1324,7 @@ class WindowScope extends InheritedModel<_WindowControllerAspect> {
|
||||
return switch (controller) {
|
||||
RegularWindowController() => controller.isMinimized,
|
||||
DialogWindowController() => controller.isMinimized,
|
||||
TooltipWindowController() => false,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1128,6 +1347,7 @@ class WindowScope extends InheritedModel<_WindowControllerAspect> {
|
||||
return switch (controller) {
|
||||
RegularWindowController() => controller.isMinimized,
|
||||
DialogWindowController() => controller.isMinimized,
|
||||
TooltipWindowController() => false,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1150,6 +1370,7 @@ class WindowScope extends InheritedModel<_WindowControllerAspect> {
|
||||
return switch (controller) {
|
||||
RegularWindowController() => controller.isMaximized,
|
||||
DialogWindowController() => false,
|
||||
TooltipWindowController() => false,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1172,6 +1393,7 @@ class WindowScope extends InheritedModel<_WindowControllerAspect> {
|
||||
return switch (controller) {
|
||||
RegularWindowController() => controller.isMaximized,
|
||||
DialogWindowController() => false,
|
||||
TooltipWindowController() => false,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1195,6 +1417,7 @@ class WindowScope extends InheritedModel<_WindowControllerAspect> {
|
||||
return switch (controller) {
|
||||
RegularWindowController() => controller.isFullscreen,
|
||||
DialogWindowController() => false,
|
||||
TooltipWindowController() => false,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1217,6 +1440,7 @@ class WindowScope extends InheritedModel<_WindowControllerAspect> {
|
||||
return switch (controller) {
|
||||
RegularWindowController() => controller.isFullscreen,
|
||||
DialogWindowController() => false,
|
||||
TooltipWindowController() => false,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1280,6 +1504,7 @@ class WindowScope extends InheritedModel<_WindowControllerAspect> {
|
||||
regular.title != (oldWidget.controller as RegularWindowController).title,
|
||||
final DialogWindowController dialog =>
|
||||
dialog.title != (oldWidget.controller as DialogWindowController).title,
|
||||
TooltipWindowController() => false,
|
||||
},
|
||||
_WindowControllerAspect.activated => switch (controller) {
|
||||
final RegularWindowController regular =>
|
||||
@ -1287,12 +1512,14 @@ class WindowScope extends InheritedModel<_WindowControllerAspect> {
|
||||
(oldWidget.controller as RegularWindowController).isActivated,
|
||||
final DialogWindowController dialog =>
|
||||
dialog.isActivated != (oldWidget.controller as DialogWindowController).isActivated,
|
||||
TooltipWindowController() => false,
|
||||
},
|
||||
_WindowControllerAspect.maximized => switch (controller) {
|
||||
final RegularWindowController regular =>
|
||||
regular.isMaximized !=
|
||||
(oldWidget.controller as RegularWindowController).isMaximized,
|
||||
DialogWindowController() => false,
|
||||
TooltipWindowController() => false,
|
||||
},
|
||||
_WindowControllerAspect.minimized => switch (controller) {
|
||||
final RegularWindowController regular =>
|
||||
@ -1300,12 +1527,14 @@ class WindowScope extends InheritedModel<_WindowControllerAspect> {
|
||||
(oldWidget.controller as RegularWindowController).isMinimized,
|
||||
final DialogWindowController dialog =>
|
||||
dialog.isMinimized != (oldWidget.controller as DialogWindowController).isMinimized,
|
||||
TooltipWindowController() => false,
|
||||
},
|
||||
_WindowControllerAspect.fullscreen => switch (controller) {
|
||||
final RegularWindowController regular =>
|
||||
regular.isFullscreen !=
|
||||
(oldWidget.controller as RegularWindowController).isFullscreen,
|
||||
DialogWindowController() => false,
|
||||
TooltipWindowController() => false,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@ -23,6 +23,7 @@ import 'package:flutter/rendering.dart';
|
||||
|
||||
import '../foundation/_features.dart';
|
||||
import '_window.dart';
|
||||
import '_window_positioner.dart';
|
||||
import 'binding.dart';
|
||||
|
||||
// Maximum width and height a window can be.
|
||||
@ -591,6 +592,18 @@ class WindowingOwnerLinux extends WindowingOwner {
|
||||
_windows[controller.rootView.viewId] = controller._window;
|
||||
return controller;
|
||||
}
|
||||
|
||||
@internal
|
||||
@override
|
||||
TooltipWindowController createTooltipWindowController({
|
||||
required TooltipWindowControllerDelegate delegate,
|
||||
required BoxConstraints preferredConstraints,
|
||||
required Rect anchorRect,
|
||||
required WindowPositioner positioner,
|
||||
required BaseWindowController parent,
|
||||
}) {
|
||||
throw UnimplementedError('Tooltip windows are not yet implemented on Linux.');
|
||||
}
|
||||
}
|
||||
|
||||
/// Implementation of [RegularWindowController] for the Linux platform.
|
||||
|
||||
@ -12,6 +12,7 @@ import 'package:flutter/rendering.dart';
|
||||
|
||||
import '../foundation/_features.dart';
|
||||
import '_window.dart';
|
||||
import '_window_positioner.dart';
|
||||
import 'binding.dart';
|
||||
|
||||
// Do not import this file in production applications or packages published
|
||||
@ -109,6 +110,18 @@ class WindowingOwnerMacOS extends WindowingOwner {
|
||||
return res;
|
||||
}
|
||||
|
||||
@internal
|
||||
@override
|
||||
TooltipWindowController createTooltipWindowController({
|
||||
required TooltipWindowControllerDelegate delegate,
|
||||
required BoxConstraints preferredConstraints,
|
||||
required Rect anchorRect,
|
||||
required WindowPositioner positioner,
|
||||
required BaseWindowController parent,
|
||||
}) {
|
||||
throw UnimplementedError('Tooltip windows are not yet implemented on MacOS.');
|
||||
}
|
||||
|
||||
final List<BaseWindowController> _activeControllers = <BaseWindowController>[];
|
||||
|
||||
/// Returns the window handle for the given [view], or null is the window
|
||||
|
||||
520
packages/flutter/lib/src/widgets/_window_positioner.dart
Normal file
520
packages/flutter/lib/src/widgets/_window_positioner.dart
Normal file
@ -0,0 +1,520 @@
|
||||
// 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/foundation.dart';
|
||||
import '_window.dart';
|
||||
|
||||
/// Defines how a child window will be placed relative to the anchor rectangle
|
||||
/// of its parent.
|
||||
///
|
||||
/// The specified anchor is used to derive an anchor point on the anchor rectangle that
|
||||
/// the child [BaseWindowController] will be positioned relative to.
|
||||
/// If a corner anchor is set (e.g. [topLeft] or [bottomRight]),
|
||||
/// the anchor point will be at the specified corner; otherwise, the derived anchor point
|
||||
/// will be centered on the specified edge, or in the center of the anchor rectangle
|
||||
/// if no edge is specified.
|
||||
///
|
||||
/// {@macro flutter.widgets.windowing.experimental}
|
||||
@internal
|
||||
enum WindowPositionerAnchor {
|
||||
/// If the [WindowPositioner.parentAnchor] is set to [center], then the
|
||||
/// child window will be positioned relative to the center
|
||||
/// of the parent window.
|
||||
///
|
||||
/// If [WindowPositioner.childAnchor] is set to [center], then the middle
|
||||
/// of the child window will be positioned relative to
|
||||
/// [WindowPositioner.parentAnchor].
|
||||
///
|
||||
/// {@macro flutter.widgets.windowing.experimental}
|
||||
@internal
|
||||
center,
|
||||
|
||||
/// If the [WindowPositioner.parentAnchor] is set to [top], then the
|
||||
/// child window will be positioned relative to the top
|
||||
/// of the parent window.
|
||||
///
|
||||
/// If [WindowPositioner.childAnchor] is set to [top], then the top
|
||||
/// of the child window will be positioned relative to
|
||||
/// [WindowPositioner.parentAnchor].
|
||||
///
|
||||
/// {@macro flutter.widgets.windowing.experimental}
|
||||
@internal
|
||||
top,
|
||||
|
||||
/// If the [WindowPositioner.parentAnchor] is set to [bottom], then the
|
||||
/// child window will be positioned relative to the bottom
|
||||
/// of the parent window.
|
||||
///
|
||||
/// If [WindowPositioner.childAnchor] is set to [bottom], then the bottom
|
||||
/// of the child window will be positioned relative to
|
||||
/// [WindowPositioner.parentAnchor].
|
||||
///
|
||||
/// {@macro flutter.widgets.windowing.experimental}
|
||||
@internal
|
||||
bottom,
|
||||
|
||||
/// If the [WindowPositioner.parentAnchor] is set to [left], then the
|
||||
/// child window will be positioned relative to the left
|
||||
/// of the parent window.
|
||||
///
|
||||
/// If [WindowPositioner.childAnchor] is set to [left], then the left
|
||||
/// of the child window will be positioned relative to
|
||||
/// [WindowPositioner.parentAnchor].
|
||||
///
|
||||
/// {@macro flutter.widgets.windowing.experimental}
|
||||
@internal
|
||||
left,
|
||||
|
||||
/// If the [WindowPositioner.parentAnchor] is set to [right], then the
|
||||
/// child window will be positioned relative to the right
|
||||
/// of the parent window.
|
||||
///
|
||||
/// If [WindowPositioner.childAnchor] is set to [right], then the right
|
||||
/// of the child window will be positioned relative to
|
||||
/// [WindowPositioner.parentAnchor].
|
||||
///
|
||||
/// {@macro flutter.widgets.windowing.experimental}
|
||||
@internal
|
||||
right,
|
||||
|
||||
/// If the [WindowPositioner.parentAnchor] is set to [topLeft], then the
|
||||
/// child window will be positioned relative to the top left
|
||||
/// of the parent window.
|
||||
///
|
||||
/// If [WindowPositioner.childAnchor] is set to [topLeft], then the top left
|
||||
/// of the child window will be positioned relative to
|
||||
/// [WindowPositioner.parentAnchor].
|
||||
///
|
||||
/// {@macro flutter.widgets.windowing.experimental}
|
||||
@internal
|
||||
topLeft,
|
||||
|
||||
/// If the [WindowPositioner.parentAnchor] is set to [bottomLeft], then the
|
||||
/// child window will be positioned relative to the bottom left
|
||||
/// of the parent window.
|
||||
///
|
||||
/// If [WindowPositioner.childAnchor] is set to [bottomLeft], then the bottom left
|
||||
/// of the child window will be positioned relative to
|
||||
/// [WindowPositioner.parentAnchor].
|
||||
///
|
||||
/// {@macro flutter.widgets.windowing.experimental}
|
||||
@internal
|
||||
bottomLeft,
|
||||
|
||||
/// If the [WindowPositioner.parentAnchor] is set to [topRight], then the
|
||||
/// child window will be positioned relative to the top right
|
||||
/// of the parent window.
|
||||
///
|
||||
/// If [WindowPositioner.childAnchor] is set to [topRight], then the top right
|
||||
/// of the child window will be positioned relative to
|
||||
/// [WindowPositioner.parentAnchor].
|
||||
///
|
||||
/// {@macro flutter.widgets.windowing.experimental}
|
||||
@internal
|
||||
topRight,
|
||||
|
||||
/// If the [WindowPositioner.parentAnchor] is set to [bottomRight], then the
|
||||
/// child window will be positioned relative to the bottom right
|
||||
/// of the parent window.
|
||||
///
|
||||
/// If [WindowPositioner.childAnchor] is set to [bottomRight], then the bottom right
|
||||
/// of the child window will be positioned relative to
|
||||
/// [WindowPositioner.parentAnchor].
|
||||
///
|
||||
/// {@macro flutter.widgets.windowing.experimental}
|
||||
@internal
|
||||
bottomRight;
|
||||
|
||||
WindowPositionerAnchor _flipX() {
|
||||
return switch (this) {
|
||||
WindowPositionerAnchor.center => WindowPositionerAnchor.center,
|
||||
WindowPositionerAnchor.top => WindowPositionerAnchor.top,
|
||||
WindowPositionerAnchor.bottom => WindowPositionerAnchor.bottom,
|
||||
WindowPositionerAnchor.left => WindowPositionerAnchor.right,
|
||||
WindowPositionerAnchor.right => WindowPositionerAnchor.left,
|
||||
WindowPositionerAnchor.topLeft => WindowPositionerAnchor.topRight,
|
||||
WindowPositionerAnchor.bottomLeft => WindowPositionerAnchor.bottomRight,
|
||||
WindowPositionerAnchor.topRight => WindowPositionerAnchor.topLeft,
|
||||
WindowPositionerAnchor.bottomRight => WindowPositionerAnchor.bottomLeft,
|
||||
};
|
||||
}
|
||||
|
||||
WindowPositionerAnchor _flipY() {
|
||||
return switch (this) {
|
||||
WindowPositionerAnchor.center => WindowPositionerAnchor.center,
|
||||
WindowPositionerAnchor.top => WindowPositionerAnchor.bottom,
|
||||
WindowPositionerAnchor.bottom => WindowPositionerAnchor.top,
|
||||
WindowPositionerAnchor.left => WindowPositionerAnchor.left,
|
||||
WindowPositionerAnchor.right => WindowPositionerAnchor.right,
|
||||
WindowPositionerAnchor.topLeft => WindowPositionerAnchor.bottomLeft,
|
||||
WindowPositionerAnchor.bottomLeft => WindowPositionerAnchor.topLeft,
|
||||
WindowPositionerAnchor.topRight => WindowPositionerAnchor.bottomRight,
|
||||
WindowPositionerAnchor.bottomRight => WindowPositionerAnchor.topRight,
|
||||
};
|
||||
}
|
||||
|
||||
Offset _offsetFor(Size size) {
|
||||
return switch (this) {
|
||||
WindowPositionerAnchor.center => Offset(-size.width / 2.0, -size.height / 2.0),
|
||||
WindowPositionerAnchor.top => Offset(-size.width / 2.0, 0.0),
|
||||
WindowPositionerAnchor.bottom => Offset(-size.width / 2.0, -size.height),
|
||||
WindowPositionerAnchor.left => Offset(0.0, -size.height / 2.0),
|
||||
WindowPositionerAnchor.right => Offset(-size.width, -size.height / 2.0),
|
||||
WindowPositionerAnchor.topLeft => Offset.zero,
|
||||
WindowPositionerAnchor.bottomLeft => Offset(0.0, -size.height),
|
||||
WindowPositionerAnchor.topRight => Offset(-size.width, 0.0),
|
||||
WindowPositionerAnchor.bottomRight => Offset(-size.width, -size.height),
|
||||
};
|
||||
}
|
||||
|
||||
Offset _anchorPositionFor(Rect rect) {
|
||||
return switch (this) {
|
||||
WindowPositionerAnchor.center => rect.center,
|
||||
WindowPositionerAnchor.top => rect.topCenter,
|
||||
WindowPositionerAnchor.bottom => rect.bottomCenter,
|
||||
WindowPositionerAnchor.left => rect.centerLeft,
|
||||
WindowPositionerAnchor.right => rect.centerRight,
|
||||
WindowPositionerAnchor.topLeft => rect.topLeft,
|
||||
WindowPositionerAnchor.bottomLeft => rect.bottomLeft,
|
||||
WindowPositionerAnchor.topRight => rect.topRight,
|
||||
WindowPositionerAnchor.bottomRight => rect.bottomRight,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// The [WindowPositionerConstraintAdjustment] describes how a window will adjust
|
||||
/// its position when it would be partly constrained by the platform.
|
||||
///
|
||||
/// {@template flutter.widgets.window_positioner.constraint_adjustment}
|
||||
/// Whether a window is considered "constrained" is left to the platform
|
||||
/// to determine. For example, the window may be partly outside the
|
||||
/// output's 'work area', thus necessitating the child window's
|
||||
/// position be adjusted until it is entirely inside the work area.
|
||||
///
|
||||
/// The adjustments can be combined, according to a defined precedence:
|
||||
///
|
||||
/// 1. [WindowPositionerConstraintAdjustment.flipX] and [WindowPositionerConstraintAdjustment.flipY]
|
||||
/// 2. [WindowPositionerConstraintAdjustment.slideX] and [WindowPositionerConstraintAdjustment.slideY]
|
||||
/// 3. [WindowPositionerConstraintAdjustment.resizeX] and [WindowPositionerConstraintAdjustment.resizeY]
|
||||
///
|
||||
/// The first adjustment that results in the child window being entirely inside the work area will be picked.
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// {@macro flutter.widgets.windowing.experimental}
|
||||
@internal
|
||||
class WindowPositionerConstraintAdjustment {
|
||||
const WindowPositionerConstraintAdjustment({
|
||||
this.flipX = false,
|
||||
this.flipY = false,
|
||||
this.slideX = false,
|
||||
this.slideY = false,
|
||||
this.resizeX = false,
|
||||
this.resizeY = false,
|
||||
});
|
||||
|
||||
/// If [slideX] is `true` and the window would be displayed off the screen in the X-axis,then it will be
|
||||
/// translated in the X-direction (either negative or positive) in order
|
||||
/// to best display the window on screen.
|
||||
///
|
||||
/// {@macro flutter.widgets.windowing.experimental}
|
||||
@internal
|
||||
final bool slideX;
|
||||
|
||||
/// If [slideY] is `true` and the window would be displayed off the screen in the Y-axis, then it will be
|
||||
/// translated in the Y-direction (either negative or positive) in order
|
||||
/// to best display the window on screen.
|
||||
///
|
||||
/// {@macro flutter.widgets.windowing.experimental}
|
||||
@internal
|
||||
final bool slideY;
|
||||
|
||||
/// If [flipX] is `true` and the window would be displayed off the screen in the X-axis in one direction, then
|
||||
/// it will be flipped to the opposite side of its parent in order
|
||||
/// to best display the window on screen.
|
||||
///
|
||||
/// {@macro flutter.widgets.windowing.experimental}
|
||||
@internal
|
||||
final bool flipX;
|
||||
|
||||
/// If [flipY] is `true` and the window would be displayed off the screen in the Y-axis in one direction, then
|
||||
/// it will be flipped to the opposite side of its parent in order
|
||||
/// to best display the window on screen.
|
||||
///
|
||||
/// {@macro flutter.widgets.windowing.experimental}
|
||||
@internal
|
||||
final bool flipY;
|
||||
|
||||
/// If [resizeX] is `true` and the window would be displayed off the screen in the X-axis, then
|
||||
/// its width will be reduced such that it fits on screen.
|
||||
///
|
||||
/// {@macro flutter.widgets.windowing.experimental}
|
||||
@internal
|
||||
final bool resizeX;
|
||||
|
||||
/// If `true` and the window would be displayed off the screen in the Y-axis, then
|
||||
/// its height will be reduced such that it fits on screen.
|
||||
///
|
||||
/// {@macro flutter.widgets.windowing.experimental}
|
||||
@internal
|
||||
final bool resizeY;
|
||||
}
|
||||
|
||||
/// The [WindowPositioner] defines how child windows are placed relative to
|
||||
/// their parent window.
|
||||
///
|
||||
/// For example, the rules may be defined such that the child window remains
|
||||
/// within the visible area's borders, and to specify how the child window
|
||||
/// changes its position, such as sliding along an axis, or flipping around a
|
||||
/// rectangle.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [TooltipWindowController], a subclass of [BaseWindowController] that
|
||||
/// uses [WindowPositioner] to position tooltip windows.
|
||||
/// * [WindowPositionerAnchor], which defines anchor points for positioning
|
||||
/// windows.
|
||||
/// * [WindowPositionerConstraintAdjustment], which defines how windows adjust
|
||||
/// their position when they would be partly constrained by the platform.
|
||||
///
|
||||
/// {@macro flutter.widgets.windowing.experimental}
|
||||
@internal
|
||||
class WindowPositioner {
|
||||
/// Const constructor for [WindowPositioner].
|
||||
const WindowPositioner({
|
||||
this.parentAnchor = WindowPositionerAnchor.center,
|
||||
this.childAnchor = WindowPositionerAnchor.center,
|
||||
this.offset = Offset.zero,
|
||||
this.constraintAdjustment = const WindowPositionerConstraintAdjustment(),
|
||||
});
|
||||
|
||||
/// Copy a [WindowPositioner] with some fields replaced.
|
||||
WindowPositioner copyWith({
|
||||
WindowPositionerAnchor? parentAnchor,
|
||||
WindowPositionerAnchor? childAnchor,
|
||||
Offset? offset,
|
||||
WindowPositionerConstraintAdjustment? constraintAdjustment,
|
||||
}) {
|
||||
return WindowPositioner(
|
||||
parentAnchor: parentAnchor ?? this.parentAnchor,
|
||||
childAnchor: childAnchor ?? this.childAnchor,
|
||||
offset: offset ?? this.offset,
|
||||
constraintAdjustment: constraintAdjustment ?? this.constraintAdjustment,
|
||||
);
|
||||
}
|
||||
|
||||
/// Defines the point on the parent from which to position the child.
|
||||
///
|
||||
/// The specified anchor is used to derive an anchor point that the child
|
||||
/// window will be positioned relative to. If a corner anchor is set
|
||||
/// (e.g. [WindowPositionerAnchor.topLeft] or [WindowPositionerAnchor.bottomRight]),
|
||||
/// the anchor point will be at the specified corner;
|
||||
/// otherwise, the derived anchor point will be centered on the specified
|
||||
/// edge, or in the center of the anchor rectangle if no edge is specified.
|
||||
///
|
||||
/// The child is positioned by placing [childAnchor] on top of [parentAnchor] and then translating by [offset].
|
||||
///
|
||||
/// Defaults to [WindowPositionerAnchor.center].
|
||||
///
|
||||
/// {@macro flutter.widgets.windowing.experimental}
|
||||
final WindowPositionerAnchor parentAnchor;
|
||||
|
||||
/// Defines the point on the child that is positioned relative to the parent.
|
||||
///
|
||||
/// The specified anchor is used to derive an anchor point that will be positioned
|
||||
/// relative to the [parentAnchor]. If a corner anchor is set (e.g. [WindowPositionerAnchor.topLeft] or
|
||||
/// [WindowPositionerAnchor.bottomRight]), the anchor point will be at the specified corner;
|
||||
/// otherwise, the derived anchor point will be centered on the specified
|
||||
/// edge, or in the center of the anchor rectangle if no edge is specified.
|
||||
///
|
||||
/// The child is positioned by placing [childAnchor] on top of [parentAnchor] and then translating by [offset].
|
||||
///
|
||||
/// Defaults to [WindowPositionerAnchor.center].
|
||||
///
|
||||
/// {@macro flutter.widgets.windowing.experimental}
|
||||
final WindowPositionerAnchor childAnchor;
|
||||
|
||||
/// The offset with which to place the child relative to the parent.
|
||||
///
|
||||
/// The child is positioned by placing [childAnchor] on top of [parentAnchor] and then translating by [offset].
|
||||
///
|
||||
/// For example if the anchor of the anchor rectangle is at (x, y), the window
|
||||
/// has a [childAnchor] of [WindowPositionerAnchor.topLeft], and the [offset]
|
||||
/// is (ox, oy), the calculated window position will be (x + ox, y + oy).
|
||||
/// The offset position of the window is the one used for constraint testing.
|
||||
/// See [constraintAdjustment].
|
||||
///
|
||||
/// An example use case is placing a popup menu on top of a user interface
|
||||
/// element, while aligning the user interface element of the parent window
|
||||
/// with some user interface element placed somewhere in the popup window.
|
||||
///
|
||||
/// Defaults to [Offset.zero].
|
||||
///
|
||||
/// {@macro flutter.widgets.windowing.experimental}
|
||||
final Offset offset;
|
||||
|
||||
/// Defines how Flutter will adjust the position of the window if the unadjusted
|
||||
/// position would result in the window being partly constrained by the platform.
|
||||
///
|
||||
/// {@macro flutter.widgets.window_positioner.constraint_adjustment}
|
||||
///
|
||||
/// The first adjustment that results in the child window being entirely inside the work area will be picked.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [WindowPositionerConstraintAdjustment] for details on each adjustment type.
|
||||
///
|
||||
/// {@macro flutter.widgets.windowing.experimental}
|
||||
final WindowPositionerConstraintAdjustment constraintAdjustment;
|
||||
|
||||
/// Computes the screen-space rectangle for a child window placed according to
|
||||
/// 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.
|
||||
///
|
||||
/// [displayRect] is the output display area where the child window will be placed.
|
||||
///
|
||||
/// All sizes and rectangles are in physical coordinates.
|
||||
///
|
||||
/// {@macro flutter.widgets.windowing.experimental}
|
||||
Rect placeWindow({
|
||||
required Size childSize,
|
||||
required Rect anchorRect,
|
||||
required Rect parentRect,
|
||||
required Rect displayRect,
|
||||
}) {
|
||||
Rect defaultResult;
|
||||
{
|
||||
final Offset result =
|
||||
_constrainTo(parentRect, parentAnchor._anchorPositionFor(anchorRect) + offset) +
|
||||
childAnchor._offsetFor(childSize);
|
||||
defaultResult = result & childSize;
|
||||
if (_rectContains(displayRect, defaultResult)) {
|
||||
return defaultResult;
|
||||
}
|
||||
}
|
||||
|
||||
if (constraintAdjustment.flipX) {
|
||||
final Offset result =
|
||||
_constrainTo(
|
||||
parentRect,
|
||||
parentAnchor._flipX()._anchorPositionFor(anchorRect) + _flipX(offset),
|
||||
) +
|
||||
childAnchor._flipX()._offsetFor(childSize);
|
||||
if (_rectContains(displayRect, result & childSize)) {
|
||||
return result & childSize;
|
||||
}
|
||||
}
|
||||
|
||||
if (constraintAdjustment.flipY) {
|
||||
final Offset result =
|
||||
_constrainTo(
|
||||
parentRect,
|
||||
parentAnchor._flipY()._anchorPositionFor(anchorRect) + _flipY(offset),
|
||||
) +
|
||||
childAnchor._flipY()._offsetFor(childSize);
|
||||
if (_rectContains(displayRect, result & childSize)) {
|
||||
return result & childSize;
|
||||
}
|
||||
}
|
||||
|
||||
if (constraintAdjustment.flipX && constraintAdjustment.flipY) {
|
||||
final Offset result =
|
||||
_constrainTo(
|
||||
parentRect,
|
||||
parentAnchor._flipY()._flipX()._anchorPositionFor(anchorRect) + _flipX(_flipY(offset)),
|
||||
) +
|
||||
childAnchor._flipY()._flipX()._offsetFor(childSize);
|
||||
if (_rectContains(displayRect, result & childSize)) {
|
||||
return result & childSize;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
Offset result =
|
||||
_constrainTo(parentRect, parentAnchor._anchorPositionFor(anchorRect) + offset) +
|
||||
childAnchor._offsetFor(childSize);
|
||||
|
||||
if (constraintAdjustment.slideX) {
|
||||
final double leftOverhang = result.dx - displayRect.left;
|
||||
final double rightOverhang = result.dx + childSize.width - displayRect.right;
|
||||
if (leftOverhang < 0.0) {
|
||||
result = result.translate(-leftOverhang, 0.0);
|
||||
} else if (rightOverhang > 0.0) {
|
||||
result = result.translate(-rightOverhang, 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
if (constraintAdjustment.slideY) {
|
||||
final double topOverhang = result.dy - displayRect.top;
|
||||
final double bottomOverhang = result.dy + childSize.height - displayRect.bottom;
|
||||
if (topOverhang < 0.0) {
|
||||
result = result.translate(0.0, -topOverhang);
|
||||
} else if (bottomOverhang > 0.0) {
|
||||
result = result.translate(0.0, -bottomOverhang);
|
||||
}
|
||||
}
|
||||
|
||||
if (_rectContains(displayRect, result & childSize)) {
|
||||
return result & childSize;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
Offset result =
|
||||
_constrainTo(parentRect, parentAnchor._anchorPositionFor(anchorRect) + offset) +
|
||||
childAnchor._offsetFor(childSize);
|
||||
|
||||
if (constraintAdjustment.resizeX) {
|
||||
final double leftOverhang = result.dx - displayRect.left;
|
||||
final double rightOverhang = result.dx + childSize.width - displayRect.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.resizeY) {
|
||||
final double topOverhang = result.dy - displayRect.top;
|
||||
final double bottomOverhang = result.dy + childSize.height - displayRect.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(displayRect, result & childSize)) {
|
||||
return result & childSize;
|
||||
}
|
||||
}
|
||||
|
||||
return defaultResult;
|
||||
}
|
||||
}
|
||||
|
||||
bool _rectContains(Rect r1, Rect r2) {
|
||||
return r1.left <= r2.left && r1.right >= r2.right && r1.top <= r2.top && r1.bottom >= r2.bottom;
|
||||
}
|
||||
|
||||
Offset _constrainTo(Rect r, Offset p) {
|
||||
return Offset(clampDouble(p.dx, r.left, r.right), clampDouble(p.dy, r.top, r.bottom));
|
||||
}
|
||||
|
||||
Offset _flipX(Offset offset) {
|
||||
return Offset(-offset.dx, offset.dy);
|
||||
}
|
||||
|
||||
Offset _flipY(Offset offset) {
|
||||
return Offset(offset.dx, -offset.dy);
|
||||
}
|
||||
@ -23,6 +23,7 @@ import 'package:flutter/rendering.dart';
|
||||
|
||||
import '../foundation/_features.dart';
|
||||
import '_window.dart';
|
||||
import '_window_positioner.dart';
|
||||
import 'binding.dart';
|
||||
|
||||
/// A Win32 window handle.
|
||||
@ -169,6 +170,18 @@ class WindowingOwnerWin32 extends WindowingOwner {
|
||||
);
|
||||
}
|
||||
|
||||
@internal
|
||||
@override
|
||||
TooltipWindowController createTooltipWindowController({
|
||||
required TooltipWindowControllerDelegate delegate,
|
||||
required BoxConstraints preferredConstraints,
|
||||
required Rect anchorRect,
|
||||
required WindowPositioner positioner,
|
||||
required BaseWindowController parent,
|
||||
}) {
|
||||
throw UnimplementedError('Tooltip windows are not yet implemented on Windows.');
|
||||
}
|
||||
|
||||
/// Register a new [WindowsMessageHandler].
|
||||
///
|
||||
/// The handler will be triggered for unhandled messages for all top level
|
||||
|
||||
503
packages/flutter/test/widgets/window_positioner_test.dart
Normal file
503
packages/flutter/test/widgets/window_positioner_test.dart
Normal file
@ -0,0 +1,503 @@
|
||||
// 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.
|
||||
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/src/widgets/_window_positioner.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
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: true, resizeX: true),
|
||||
);
|
||||
|
||||
final Rect childRect = positioner.placeWindow(
|
||||
childSize: clientChildSize,
|
||||
anchorRect: overlappingRight,
|
||||
parentRect: clientParentRect,
|
||||
displayRect: 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: true),
|
||||
);
|
||||
|
||||
final Rect childRect = positioner.placeWindow(
|
||||
childSize: clientChildSize,
|
||||
anchorRect: overlappingAbove,
|
||||
parentRect: clientParentRect,
|
||||
displayRect: 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: true, resizeX: true),
|
||||
);
|
||||
|
||||
final Rect childRect = positioner.placeWindow(
|
||||
childSize: clientChildSize,
|
||||
anchorRect: midRight,
|
||||
parentRect: clientParentRect,
|
||||
displayRect: 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: true),
|
||||
);
|
||||
|
||||
final Rect childRect = positioner.placeWindow(
|
||||
childSize: clientChildSize,
|
||||
anchorRect: midTop,
|
||||
parentRect: clientParentRect,
|
||||
displayRect: 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: true, resizeY: true),
|
||||
);
|
||||
|
||||
final Rect childRect = positioner.placeWindow(
|
||||
childSize: clientChildSize,
|
||||
anchorRect: belowLeft,
|
||||
parentRect: clientParentRect,
|
||||
displayRect: 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', () {
|
||||
Offset anchorPositionFor(WindowPositionerAnchor anchor, Rect rect) {
|
||||
return switch (anchor) {
|
||||
WindowPositionerAnchor.center => rect.center,
|
||||
WindowPositionerAnchor.top => rect.topCenter,
|
||||
WindowPositionerAnchor.bottom => rect.bottomCenter,
|
||||
WindowPositionerAnchor.left => rect.centerLeft,
|
||||
WindowPositionerAnchor.right => rect.centerRight,
|
||||
WindowPositionerAnchor.topLeft => rect.topLeft,
|
||||
WindowPositionerAnchor.bottomLeft => rect.bottomLeft,
|
||||
WindowPositionerAnchor.topRight => rect.topRight,
|
||||
WindowPositionerAnchor.bottomRight => rect.bottomRight,
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
displayRect: displayArea,
|
||||
);
|
||||
|
||||
expect(
|
||||
anchorPositionFor(childAnchor, childRect),
|
||||
anchorPositionFor(parentAnchor, 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: true),
|
||||
);
|
||||
|
||||
final Rect anchorRect = anchorRectFor(rectangleNearRhs);
|
||||
final Rect childRect = positioner.placeWindow(
|
||||
childSize: childSize,
|
||||
anchorRect: anchorRect,
|
||||
parentRect: parentRect,
|
||||
displayRect: 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: true),
|
||||
);
|
||||
|
||||
final Rect anchorRect = anchorRectFor(rectangleNearBottom);
|
||||
final Rect childRect = positioner.placeWindow(
|
||||
childSize: childSize,
|
||||
anchorRect: anchorRect,
|
||||
parentRect: parentRect,
|
||||
displayRect: 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: true, flipY: true),
|
||||
);
|
||||
|
||||
final Rect anchorRect = anchorRectFor(rectangleNearBothBottomRight);
|
||||
final Rect childRect = positioner.placeWindow(
|
||||
childSize: childSize,
|
||||
anchorRect: anchorRect,
|
||||
parentRect: parentRect,
|
||||
displayRect: 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: true),
|
||||
);
|
||||
|
||||
final Rect anchorRect = anchorRectFor(rectangleNearRhs);
|
||||
final Rect childRect = positioner.placeWindow(
|
||||
childSize: childSize,
|
||||
anchorRect: anchorRect,
|
||||
parentRect: parentRect,
|
||||
displayRect: 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: true),
|
||||
);
|
||||
|
||||
final Rect anchorRect = anchorRectFor(rectangleNearLeftSide);
|
||||
final Rect childRect = positioner.placeWindow(
|
||||
childSize: childSize,
|
||||
anchorRect: anchorRect,
|
||||
parentRect: parentRect,
|
||||
displayRect: 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: true),
|
||||
);
|
||||
|
||||
final Rect anchorRect = anchorRectFor(rectangleNearBottom);
|
||||
final Rect childRect = positioner.placeWindow(
|
||||
childSize: childSize,
|
||||
anchorRect: anchorRect,
|
||||
parentRect: parentRect,
|
||||
displayRect: 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: true),
|
||||
);
|
||||
|
||||
final Rect anchorRect = anchorRectFor(rectangleNearAllSides);
|
||||
final Rect childRect = positioner.placeWindow(
|
||||
childSize: childSize,
|
||||
anchorRect: anchorRect,
|
||||
parentRect: parentRect,
|
||||
displayRect: 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: true, slideY: true),
|
||||
);
|
||||
|
||||
final Rect anchorRect = anchorRectFor(rectangleNearBothBottomRight);
|
||||
final Rect childRect = positioner.placeWindow(
|
||||
childSize: childSize,
|
||||
anchorRect: anchorRect,
|
||||
parentRect: parentRect,
|
||||
displayRect: 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: true),
|
||||
);
|
||||
|
||||
final Rect anchorRect = anchorRectFor(rectangleNearRhs);
|
||||
final Rect childRect = positioner.placeWindow(
|
||||
childSize: childSize,
|
||||
anchorRect: anchorRect,
|
||||
parentRect: parentRect,
|
||||
displayRect: 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: true),
|
||||
);
|
||||
|
||||
final Rect anchorRect = anchorRectFor(rectangleNearLeftSide);
|
||||
final Rect childRect = positioner.placeWindow(
|
||||
childSize: childSize,
|
||||
anchorRect: anchorRect,
|
||||
parentRect: parentRect,
|
||||
displayRect: 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: true),
|
||||
);
|
||||
|
||||
final Rect anchorRect = anchorRectFor(rectangleNearAllSides);
|
||||
final Rect childRect = positioner.placeWindow(
|
||||
childSize: childSize,
|
||||
anchorRect: anchorRect,
|
||||
parentRect: parentRect,
|
||||
displayRect: 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: true),
|
||||
);
|
||||
|
||||
final Rect anchorRect = anchorRectFor(rectangleNearAllSides);
|
||||
final Rect childRect = positioner.placeWindow(
|
||||
childSize: childSize,
|
||||
anchorRect: anchorRect,
|
||||
parentRect: parentRect,
|
||||
displayRect: 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: true, resizeY: true),
|
||||
);
|
||||
|
||||
final Rect anchorRect = anchorRectFor(rectangleNearBothBottomRight);
|
||||
final Rect childRect = positioner.placeWindow(
|
||||
childSize: childSize,
|
||||
anchorRect: anchorRect,
|
||||
parentRect: parentRect,
|
||||
displayRect: displayArea,
|
||||
);
|
||||
|
||||
final Size expectedSize = Size(
|
||||
displayArea.right - (anchorRect.left + anchorRect.width),
|
||||
displayArea.bottom - (anchorRect.top + anchorRect.height),
|
||||
);
|
||||
|
||||
expect(childRect.size, expectedSize);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -13,6 +13,8 @@ import 'package:flutter/src/widgets/_window.dart'
|
||||
RegularWindow,
|
||||
RegularWindowController,
|
||||
RegularWindowControllerDelegate,
|
||||
TooltipWindow,
|
||||
TooltipWindowController,
|
||||
WindowScope,
|
||||
WindowingOwner,
|
||||
createDefaultWindowingOwner;
|
||||
@ -108,6 +110,26 @@ class _StubDialogWindowController extends DialogWindowController {
|
||||
void destroy() {}
|
||||
}
|
||||
|
||||
class _StubTooltipWindowController extends TooltipWindowController {
|
||||
_StubTooltipWindowController({required this.tester}) : super.empty() {
|
||||
rootView = FakeView(tester.view);
|
||||
}
|
||||
|
||||
final WidgetTester tester;
|
||||
|
||||
@override
|
||||
BaseWindowController get parent => _StubRegularWindowController(tester);
|
||||
|
||||
@override
|
||||
Size get contentSize => Size.zero;
|
||||
|
||||
@override
|
||||
void setConstraints(BoxConstraints constraints) {}
|
||||
|
||||
@override
|
||||
void destroy() {}
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('Windowing', () {
|
||||
group('isWindowingEnabled is false', () {
|
||||
@ -146,6 +168,16 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('TooltipWindow throws UnsupportedError', (WidgetTester tester) async {
|
||||
expect(
|
||||
() => TooltipWindow(
|
||||
controller: _StubTooltipWindowController(tester: tester),
|
||||
child: const Text('Test'),
|
||||
),
|
||||
throwsUnsupportedError,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Accessing WindowScope.of throws UnsupportedError', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(LookupBoundary(child: Container()));
|
||||
final BuildContext context = tester.element(find.byType(Container));
|
||||
@ -213,6 +245,26 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Can access WindowScope.of for tooltip windows', (WidgetTester tester) async {
|
||||
final _StubTooltipWindowController controller = _StubTooltipWindowController(
|
||||
tester: tester,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
TooltipWindow(
|
||||
controller: controller,
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
final BaseWindowController scope = WindowScope.of(context);
|
||||
expect(scope, isA<TooltipWindowController>());
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Can access WindowScope.maybeOf for regular windows', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
@ -251,6 +303,28 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Can access WindowScope.maybeOf for tooltip windows', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final _StubTooltipWindowController controller = _StubTooltipWindowController(
|
||||
tester: tester,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
TooltipWindow(
|
||||
controller: controller,
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
final BaseWindowController? scope = WindowScope.maybeOf(context);
|
||||
expect(scope, isA<TooltipWindowController>());
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Can access WindowScope.contentSizeOf for regular windows', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
@ -291,6 +365,28 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Can access WindowScope.contentSizeOf for tooltip windows', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final _StubTooltipWindowController controller = _StubTooltipWindowController(
|
||||
tester: tester,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
TooltipWindow(
|
||||
controller: controller,
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
final Size size = WindowScope.contentSizeOf(context);
|
||||
expect(size, equals(Size.zero));
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Can access WindowScope.maybeContentSizeOf for regular windows', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
@ -331,6 +427,28 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Can access WindowScope.maybeContentSizeOf for tooltip windows', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final _StubTooltipWindowController controller = _StubTooltipWindowController(
|
||||
tester: tester,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
TooltipWindow(
|
||||
controller: controller,
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
final Size? size = WindowScope.maybeContentSizeOf(context);
|
||||
expect(size, equals(Size.zero));
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Can access WindowScope.titleOf for regular windows', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
@ -369,6 +487,28 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Can access WindowScope.titleOf for tooltip windows', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final _StubTooltipWindowController controller = _StubTooltipWindowController(
|
||||
tester: tester,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
TooltipWindow(
|
||||
controller: controller,
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
final String title = WindowScope.titleOf(context);
|
||||
expect(title, equals(''));
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Can access WindowScope.maybeTitleOf for regular windows', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
@ -409,6 +549,28 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Can access WindowScope.maybeTitleOf for tooltip windows', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final _StubTooltipWindowController controller = _StubTooltipWindowController(
|
||||
tester: tester,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
TooltipWindow(
|
||||
controller: controller,
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
final String? title = WindowScope.maybeTitleOf(context);
|
||||
expect(title, equals(''));
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Can access WindowScope.isActivatedOf for regular windows', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
@ -449,6 +611,28 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Can access WindowScope.isActivatedOf for tooltip windows', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final _StubTooltipWindowController controller = _StubTooltipWindowController(
|
||||
tester: tester,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
TooltipWindow(
|
||||
controller: controller,
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
final bool isActivated = WindowScope.isActivatedOf(context);
|
||||
expect(isActivated, equals(false));
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Can access WindowScope.maybeIsActivatedOf for regular windows', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
@ -489,6 +673,28 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Can access WindowScope.maybeIsActivatedOf for tooltip windows', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final _StubTooltipWindowController controller = _StubTooltipWindowController(
|
||||
tester: tester,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
TooltipWindow(
|
||||
controller: controller,
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
final bool? isActivated = WindowScope.maybeIsActivatedOf(context);
|
||||
expect(isActivated, equals(false));
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Can access WindowScope.isMinimizedOf for regular windows', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
@ -529,6 +735,28 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Can access WindowScope.isMinimizedOf for tooltip windows', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final _StubTooltipWindowController controller = _StubTooltipWindowController(
|
||||
tester: tester,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
TooltipWindow(
|
||||
controller: controller,
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
final bool isMinimized = WindowScope.isMinimizedOf(context);
|
||||
expect(isMinimized, equals(false));
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Can access WindowScope.maybeIsMinimizedOf for regular windows', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
@ -569,6 +797,28 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Can access WindowScope.maybeIsMinimizedOf for tooltip windows', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final _StubTooltipWindowController controller = _StubTooltipWindowController(
|
||||
tester: tester,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
TooltipWindow(
|
||||
controller: controller,
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
final bool? isMinimized = WindowScope.maybeIsMinimizedOf(context);
|
||||
expect(isMinimized, equals(false));
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Can access WindowScope.isMaximizedOf for regular windows', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
@ -609,6 +859,28 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Can access WindowScope.isMaximizedOf for tooltip windows', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final _StubTooltipWindowController controller = _StubTooltipWindowController(
|
||||
tester: tester,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
TooltipWindow(
|
||||
controller: controller,
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
final bool isMaximized = WindowScope.isMaximizedOf(context);
|
||||
expect(isMaximized, equals(false));
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Can access WindowScope.maybeIsMaximizedOf for regular windows', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
@ -649,6 +921,28 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Can access WindowScope.maybeIsMaximizedOf for tooltip windows', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final _StubTooltipWindowController controller = _StubTooltipWindowController(
|
||||
tester: tester,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
TooltipWindow(
|
||||
controller: controller,
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
final bool? isMaximized = WindowScope.maybeIsMaximizedOf(context);
|
||||
expect(isMaximized, equals(false));
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Can access WindowScope.isFullscreenOf for regular windows', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
@ -689,6 +983,28 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Can access WindowScope.isFullscreenOf for tooltip windows', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final _StubTooltipWindowController controller = _StubTooltipWindowController(
|
||||
tester: tester,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
TooltipWindow(
|
||||
controller: controller,
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
final bool isFullscreen = WindowScope.isFullscreenOf(context);
|
||||
expect(isFullscreen, equals(false));
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Can access WindowScope.maybeIsFullscreenOf for regular windows', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
@ -728,6 +1044,28 @@ void main() {
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Can access WindowScope.maybeIsFullscreenOf for tooltip windows', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final _StubTooltipWindowController controller = _StubTooltipWindowController(
|
||||
tester: tester,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
TooltipWindow(
|
||||
controller: controller,
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
final bool? isFullscreen = WindowScope.maybeIsFullscreenOf(context);
|
||||
expect(isFullscreen, equals(false));
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user