1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:math' as math;
6import 'dart:ui' show DisplayFeature, DisplayFeatureState;
7
8import 'basic.dart';
9import 'debug.dart';
10import 'framework.dart';
11import 'media_query.dart';
12
13/// Positions [child] such that it avoids overlapping any [DisplayFeature] that
14/// splits the screen into sub-screens.
15///
16/// A [DisplayFeature] splits the screen into sub-screens when both these
17/// conditions are met:
18///
19/// * it obstructs the screen, meaning the area it occupies is not 0 or the
20/// `state` is [DisplayFeatureState.postureHalfOpened].
21/// * it is at least as tall as the screen, producing a left and right
22/// sub-screen or it is at least as wide as the screen, producing a top and
23/// bottom sub-screen
24///
25/// After determining the sub-screens, the closest one to [anchorPoint] is used
26/// to render the content.
27///
28/// If no [anchorPoint] is provided, then [Directionality] is used:
29///
30/// * for [TextDirection.ltr], [anchorPoint] is `Offset.zero`, which will
31/// cause the content to appear in the top-left sub-screen.
32/// * for [TextDirection.rtl], [anchorPoint] is `Offset(double.maxFinite, 0)`,
33/// which will cause the content to appear in the top-right sub-screen.
34///
35/// If no [anchorPoint] is provided, and there is no [Directionality] ancestor
36/// widget in the tree, then the widget asserts during build in debug mode.
37///
38/// Similarly to [SafeArea], this widget assumes there is no added padding
39/// between it and the first [MediaQuery] ancestor. The [child] is wrapped in a
40/// new [MediaQuery] instance containing the [DisplayFeature]s that exist in the
41/// selected sub-screen, with coordinates relative to the sub-screen. Padding is
42/// also adjusted to zero out any sides that were avoided by this widget.
43///
44/// See also:
45///
46/// * [showDialog], which is a way to display a [DialogRoute].
47/// * [showCupertinoDialog], which displays an iOS-style dialog.
48class DisplayFeatureSubScreen extends StatelessWidget {
49 /// Creates a widget that positions its child so that it avoids display
50 /// features.
51 const DisplayFeatureSubScreen({
52 super.key,
53 this.anchorPoint,
54 required this.child,
55 });
56
57 /// {@template flutter.widgets.DisplayFeatureSubScreen.anchorPoint}
58 /// The anchor point used to pick the closest sub-screen.
59 ///
60 /// If the anchor point sits inside one of these sub-screens, then that
61 /// sub-screen is picked. If not, then the sub-screen with the closest edge to
62 /// the point is used.
63 ///
64 /// [Offset.zero] is the top-left corner of the available screen space. For a
65 /// vertically split dual-screen device, this is the top-left corner of the
66 /// left screen.
67 ///
68 /// When this is null, [Directionality] is used:
69 ///
70 /// * for [TextDirection.ltr], [anchorPoint] is [Offset.zero], which will
71 /// cause the top-left sub-screen to be picked.
72 /// * for [TextDirection.rtl], [anchorPoint] is
73 /// `Offset(double.maxFinite, 0)`, which will cause the top-right
74 /// sub-screen to be picked.
75 /// {@endtemplate}
76 final Offset? anchorPoint;
77
78 /// The widget below this widget in the tree.
79 ///
80 /// The padding on the [MediaQuery] for the [child] will be suitably adjusted
81 /// to zero out any sides that were avoided by this widget. The [MediaQuery]
82 /// for the [child] will no longer contain any display features that split the
83 /// screen into sub-screens.
84 ///
85 /// {@macro flutter.widgets.ProxyWidget.child}
86 final Widget child;
87
88 @override
89 Widget build(BuildContext context) {
90 assert(anchorPoint != null || debugCheckHasDirectionality(
91 context,
92 why: 'to determine which sub-screen DisplayFeatureSubScreen uses',
93 alternative: "Alternatively, consider specifying the 'anchorPoint' argument on the DisplayFeatureSubScreen.",
94 ));
95 final MediaQueryData mediaQuery = MediaQuery.of(context);
96 final Size parentSize = mediaQuery.size;
97 final Rect wantedBounds = Offset.zero & parentSize;
98 final Offset resolvedAnchorPoint = _capOffset(anchorPoint ?? _fallbackAnchorPoint(context), parentSize);
99 final Iterable<Rect> subScreens = subScreensInBounds(wantedBounds, avoidBounds(mediaQuery));
100 final Rect closestSubScreen = _closestToAnchorPoint(subScreens, resolvedAnchorPoint);
101
102 return Padding(
103 padding: EdgeInsets.only(
104 left: closestSubScreen.left,
105 top: closestSubScreen.top,
106 right: parentSize.width - closestSubScreen.right,
107 bottom: parentSize.height - closestSubScreen.bottom,
108 ),
109 child: MediaQuery(
110 data: mediaQuery.removeDisplayFeatures(closestSubScreen),
111 child: child,
112 ),
113 );
114 }
115
116 static Offset _fallbackAnchorPoint(BuildContext context) {
117 final TextDirection textDirection = Directionality.of(context);
118 switch (textDirection) {
119 case TextDirection.rtl:
120 return const Offset(double.maxFinite, 0);
121 case TextDirection.ltr:
122 return Offset.zero;
123 }
124 }
125
126 /// Returns the areas of the screen that are obstructed by display features.
127 ///
128 /// A [DisplayFeature] obstructs the screen when the area it occupies is
129 /// not 0 or the `state` is [DisplayFeatureState.postureHalfOpened].
130 static Iterable<Rect> avoidBounds(MediaQueryData mediaQuery) {
131 return mediaQuery.displayFeatures
132 .where((DisplayFeature d) => d.bounds.shortestSide > 0 ||
133 d.state == DisplayFeatureState.postureHalfOpened)
134 .map((DisplayFeature d) => d.bounds);
135 }
136
137 /// Returns the closest sub-screen to the [anchorPoint].
138 static Rect _closestToAnchorPoint(Iterable<Rect> subScreens, Offset anchorPoint) {
139 Rect closestScreen = subScreens.first;
140 double closestDistance = _distanceFromPointToRect(anchorPoint, closestScreen);
141 for (final Rect screen in subScreens) {
142 final double subScreenDistance = _distanceFromPointToRect(anchorPoint, screen);
143 if (subScreenDistance < closestDistance) {
144 closestScreen = screen;
145 closestDistance = subScreenDistance;
146 }
147 }
148 return closestScreen;
149 }
150
151 static double _distanceFromPointToRect(Offset point, Rect rect) {
152 // Cases for point position relative to rect:
153 // 1 2 3
154 // 4 [R] 5
155 // 6 7 8
156 if (point.dx < rect.left) {
157 if (point.dy < rect.top) {
158 // Case 1
159 return (point - rect.topLeft).distance;
160 } else if (point.dy > rect.bottom) {
161 // Case 6
162 return (point - rect.bottomLeft).distance;
163 } else {
164 // Case 4
165 return rect.left - point.dx;
166 }
167 } else if (point.dx > rect.right) {
168 if (point.dy < rect.top) {
169 // Case 3
170 return (point - rect.topRight).distance;
171 } else if (point.dy > rect.bottom) {
172 // Case 8
173 return (point - rect.bottomRight).distance;
174 } else {
175 // Case 5
176 return point.dx - rect.right;
177 }
178 } else {
179 if (point.dy < rect.top) {
180 // Case 2
181 return rect.top - point.dy;
182 } else if (point.dy > rect.bottom) {
183 // Case 7
184 return point.dy - rect.bottom;
185 } else {
186 // Case R
187 return 0;
188 }
189 }
190 }
191
192 /// Returns sub-screens resulted by dividing [wantedBounds] along items of
193 /// [avoidBounds] that are at least as tall or as wide.
194 static Iterable<Rect> subScreensInBounds(Rect wantedBounds, Iterable<Rect> avoidBounds) {
195 Iterable<Rect> subScreens = <Rect>[wantedBounds];
196 for (final Rect bounds in avoidBounds) {
197 final List<Rect> newSubScreens = <Rect>[];
198 for (final Rect screen in subScreens) {
199 if (screen.top >= bounds.top && screen.bottom <= bounds.bottom) {
200 // Display feature splits the screen vertically
201 if (screen.left < bounds.left) {
202 // There is a smaller sub-screen, left of the display feature
203 newSubScreens.add(Rect.fromLTWH(
204 screen.left,
205 screen.top,
206 bounds.left - screen.left,
207 screen.height,
208 ));
209 }
210 if (screen.right > bounds.right) {
211 // There is a smaller sub-screen, right of the display feature
212 newSubScreens.add(Rect.fromLTWH(
213 bounds.right,
214 screen.top,
215 screen.right - bounds.right,
216 screen.height,
217 ));
218 }
219 } else if (screen.left >= bounds.left && screen.right <= bounds.right) {
220 // Display feature splits the sub-screen horizontally
221 if (screen.top < bounds.top) {
222 // There is a smaller sub-screen, above the display feature
223 newSubScreens.add(Rect.fromLTWH(
224 screen.left,
225 screen.top,
226 screen.width,
227 bounds.top - screen.top,
228 ));
229 }
230 if (screen.bottom > bounds.bottom) {
231 // There is a smaller sub-screen, below the display feature
232 newSubScreens.add(Rect.fromLTWH(
233 screen.left,
234 bounds.bottom,
235 screen.width,
236 screen.bottom - bounds.bottom,
237 ));
238 }
239 } else {
240 newSubScreens.add(screen);
241 }
242 }
243 subScreens = newSubScreens;
244 }
245 return subScreens;
246 }
247
248 static Offset _capOffset(Offset offset, Size maximum) {
249 if (offset.dx >= 0 && offset.dx <= maximum.width
250 && offset.dy >=0 && offset.dy <= maximum.height) {
251 return offset;
252 } else {
253 return Offset(
254 math.min(math.max(0, offset.dx), maximum.width),
255 math.min(math.max(0, offset.dy), maximum.height),
256 );
257 }
258 }
259}
260