// 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:math' as math; import 'dart:ui' show DisplayFeature, DisplayFeatureState; import 'basic.dart'; import 'debug.dart'; import 'framework.dart'; import 'media_query.dart'; /// Positions [child] such that it avoids overlapping any [DisplayFeature] that /// splits the screen into sub-screens. /// /// A [DisplayFeature] splits the screen into sub-screens when both these /// conditions are met: /// /// * it obstructs the screen, meaning the area it occupies is not 0 or the /// `state` is [DisplayFeatureState.postureHalfOpened]. /// * it is at least as tall as the screen, producing a left and right /// sub-screen or it is at least as wide as the screen, producing a top and /// bottom sub-screen /// /// After determining the sub-screens, the closest one to [anchorPoint] is used /// to render the content. /// /// If no [anchorPoint] is provided, then [Directionality] is used: /// /// * for [TextDirection.ltr], [anchorPoint] is `Offset.zero`, which will /// cause the content to appear in the top-left sub-screen. /// * for [TextDirection.rtl], [anchorPoint] is `Offset(double.maxFinite, 0)`, /// which will cause the content to appear in the top-right sub-screen. /// /// If no [anchorPoint] is provided, and there is no [Directionality] ancestor /// widget in the tree, then the widget asserts during build in debug mode. /// /// Similarly to [SafeArea], this widget assumes there is no added padding /// between it and the first [MediaQuery] ancestor. The [child] is wrapped in a /// new [MediaQuery] instance containing the [DisplayFeature]s that exist in the /// selected sub-screen, with coordinates relative to the sub-screen. Padding is /// also adjusted to zero out any sides that were avoided by this widget. /// /// See also: /// /// * [showDialog], which is a way to display a [DialogRoute]. /// * [showCupertinoDialog], which displays an iOS-style dialog. class DisplayFeatureSubScreen extends StatelessWidget { /// Creates a widget that positions its child so that it avoids display /// features. const DisplayFeatureSubScreen({ super.key, this.anchorPoint, required this.child, }); /// {@template flutter.widgets.DisplayFeatureSubScreen.anchorPoint} /// The anchor point used to pick the closest sub-screen. /// /// If the anchor point sits inside one of these sub-screens, then that /// sub-screen is picked. If not, then the sub-screen with the closest edge to /// the point is used. /// /// [Offset.zero] is the top-left corner of the available screen space. For a /// vertically split dual-screen device, this is the top-left corner of the /// left screen. /// /// When this is null, [Directionality] is used: /// /// * for [TextDirection.ltr], [anchorPoint] is [Offset.zero], which will /// cause the top-left sub-screen to be picked. /// * for [TextDirection.rtl], [anchorPoint] is /// `Offset(double.maxFinite, 0)`, which will cause the top-right /// sub-screen to be picked. /// {@endtemplate} final Offset? anchorPoint; /// The widget below this widget in the tree. /// /// The padding on the [MediaQuery] for the [child] will be suitably adjusted /// to zero out any sides that were avoided by this widget. The [MediaQuery] /// for the [child] will no longer contain any display features that split the /// screen into sub-screens. /// /// {@macro flutter.widgets.ProxyWidget.child} final Widget child; @override Widget build(BuildContext context) { assert(anchorPoint != null || debugCheckHasDirectionality( context, why: 'to determine which sub-screen DisplayFeatureSubScreen uses', alternative: "Alternatively, consider specifying the 'anchorPoint' argument on the DisplayFeatureSubScreen.", )); final MediaQueryData mediaQuery = MediaQuery.of(context); final Size parentSize = mediaQuery.size; final Rect wantedBounds = Offset.zero & parentSize; final Offset resolvedAnchorPoint = _capOffset(anchorPoint ?? _fallbackAnchorPoint(context), parentSize); final Iterable<Rect> subScreens = subScreensInBounds(wantedBounds, avoidBounds(mediaQuery)); final Rect closestSubScreen = _closestToAnchorPoint(subScreens, resolvedAnchorPoint); return Padding( padding: EdgeInsets.only( left: closestSubScreen.left, top: closestSubScreen.top, right: parentSize.width - closestSubScreen.right, bottom: parentSize.height - closestSubScreen.bottom, ), child: MediaQuery( data: mediaQuery.removeDisplayFeatures(closestSubScreen), child: child, ), ); } static Offset _fallbackAnchorPoint(BuildContext context) { final TextDirection textDirection = Directionality.of(context); switch (textDirection) { case TextDirection.rtl: return const Offset(double.maxFinite, 0); case TextDirection.ltr: return Offset.zero; } } /// Returns the areas of the screen that are obstructed by display features. /// /// A [DisplayFeature] obstructs the screen when the the area it occupies is /// not 0 or the `state` is [DisplayFeatureState.postureHalfOpened]. static Iterable<Rect> avoidBounds(MediaQueryData mediaQuery) { return mediaQuery.displayFeatures .where((DisplayFeature d) => d.bounds.shortestSide > 0 || d.state == DisplayFeatureState.postureHalfOpened) .map((DisplayFeature d) => d.bounds); } /// Returns the closest sub-screen to the [anchorPoint]. static Rect _closestToAnchorPoint(Iterable<Rect> subScreens, Offset anchorPoint) { Rect closestScreen = subScreens.first; double closestDistance = _distanceFromPointToRect(anchorPoint, closestScreen); for (final Rect screen in subScreens) { final double subScreenDistance = _distanceFromPointToRect(anchorPoint, screen); if (subScreenDistance < closestDistance) { closestScreen = screen; closestDistance = subScreenDistance; } } return closestScreen; } static double _distanceFromPointToRect(Offset point, Rect rect) { // Cases for point position relative to rect: // 1 2 3 // 4 [R] 5 // 6 7 8 if (point.dx < rect.left) { if (point.dy < rect.top) { // Case 1 return (point - rect.topLeft).distance; } else if (point.dy > rect.bottom) { // Case 6 return (point - rect.bottomLeft).distance; } else { // Case 4 return rect.left - point.dx; } } else if (point.dx > rect.right) { if (point.dy < rect.top) { // Case 3 return (point - rect.topRight).distance; } else if (point.dy > rect.bottom) { // Case 8 return (point - rect.bottomRight).distance; } else { // Case 5 return point.dx - rect.right; } } else { if (point.dy < rect.top) { // Case 2 return rect.top - point.dy; } else if (point.dy > rect.bottom) { // Case 7 return point.dy - rect.bottom; } else { // Case R return 0; } } } /// Returns sub-screens resulted by dividing [wantedBounds] along items of /// [avoidBounds] that are at least as tall or as wide. static Iterable<Rect> subScreensInBounds(Rect wantedBounds, Iterable<Rect> avoidBounds) { Iterable<Rect> subScreens = <Rect>[wantedBounds]; for (final Rect bounds in avoidBounds) { final List<Rect> newSubScreens = <Rect>[]; for (final Rect screen in subScreens) { if (screen.top >= bounds.top && screen.bottom <= bounds.bottom) { // Display feature splits the screen vertically if (screen.left < bounds.left) { // There is a smaller sub-screen, left of the display feature newSubScreens.add(Rect.fromLTWH( screen.left, screen.top, bounds.left - screen.left, screen.height, )); } if (screen.right > bounds.right) { // There is a smaller sub-screen, right of the display feature newSubScreens.add(Rect.fromLTWH( bounds.right, screen.top, screen.right - bounds.right, screen.height, )); } } else if (screen.left >= bounds.left && screen.right <= bounds.right) { // Display feature splits the sub-screen horizontally if (screen.top < bounds.top) { // There is a smaller sub-screen, above the display feature newSubScreens.add(Rect.fromLTWH( screen.left, screen.top, screen.width, bounds.top - screen.top, )); } if (screen.bottom > bounds.bottom) { // There is a smaller sub-screen, below the display feature newSubScreens.add(Rect.fromLTWH( screen.left, bounds.bottom, screen.width, screen.bottom - bounds.bottom, )); } } else { newSubScreens.add(screen); } } subScreens = newSubScreens; } return subScreens; } static Offset _capOffset(Offset offset, Size maximum) { if (offset.dx >= 0 && offset.dx <= maximum.width && offset.dy >=0 && offset.dy <= maximum.height) { return offset; } else { return Offset( math.min(math.max(0, offset.dx), maximum.width), math.min(math.max(0, offset.dy), maximum.height), ); } } }