LCOV - code coverage report
Current view: top level - menu - mongol_tooltip.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 75 216 34.7 %
Date: 2021-08-02 17:55:49 Functions: 0 0 -

          Line data    Source code
       1             : // Copyright 2014 The Flutter Authors.
       2             : // Copyright 2021 Suragch.
       3             : // All rights reserved.
       4             : // Use of this source code is governed by a BSD-style license that can be
       5             : // found in the LICENSE file.
       6             : 
       7             : // Copyright 2014 The Flutter Authors. All rights reserved.
       8             : // Use of this source code is governed by a BSD-style license that can be
       9             : // found in the LICENSE file.
      10             : 
      11             : import 'dart:async';
      12             : import 'dart:math' as math;
      13             : 
      14             : import 'package:flutter/gestures.dart';
      15             : import 'package:flutter/material.dart'
      16             :     show
      17             :         ThemeData,
      18             :         Theme,
      19             :         Feedback,
      20             :         TooltipThemeData,
      21             :         TooltipTheme,
      22             :         Brightness,
      23             :         Colors;
      24             : import 'package:flutter/rendering.dart';
      25             : import 'package:flutter/widgets.dart';
      26             : import 'package:mongol/src/text/mongol_text.dart';
      27             : 
      28             : /// A Mongol material design tooltip.
      29             : ///
      30             : /// Tooltips provide text labels which help explain the function of a button or
      31             : /// other user interface action. Wrap the button in a [Tooltip] widget and provide
      32             : /// a message which will be shown when the widget is long pressed.
      33             : ///
      34             : /// Many widgets, such as [IconButton], [FloatingActionButton], and
      35             : /// [MongolPopupMenuButton] have a `tooltip` property that, when non-null, causes the
      36             : /// widget to include a [Tooltip] in its build.
      37             : ///
      38             : /// Tooltips improve the accessibility of visual widgets by proving a textual
      39             : /// representation of the widget, which, for example, can be vocalized by a
      40             : /// screen reader.
      41             : ///
      42             : /// {@youtube 560 315 https://www.youtube.com/watch?v=EeEfD5fI-5Q}
      43             : ///
      44             : /// {@tool dartpad --template=stateless_widget_scaffold_center}
      45             : ///
      46             : /// This example show a basic [MongolTooltip] which has a [MongolText] as child.
      47             : /// [message] contains your label to be shown by the tooltip when
      48             : /// the child that MongolTooltip wraps is hovered over on web or desktop. On mobile,
      49             : /// the tooltip is shown when the widget is long pressed.
      50             : ///
      51             : /// ```dart
      52             : /// Widget build(BuildContext context) {
      53             : ///   return const MongolTooltip(
      54             : ///     message: 'I am a Tooltip',
      55             : ///     child: MongolText('Hover over the text to show a tooltip.'),
      56             : ///   );
      57             : /// }
      58             : /// ```
      59             : /// {@end-tool}
      60             : ///
      61             : /// {@tool dartpad --template=stateless_widget_scaffold_center}
      62             : ///
      63             : /// This example covers most of the attributes available in MongolTooltip.
      64             : /// `decoration` has been used to give a gradient and borderRadius to MongolTooltip.
      65             : /// `width` has been used to set a specific width of the MongolTooltip.
      66             : /// `preferRight` is false, the tooltip will prefer showing left of [MongolTooltip]'s child widget.
      67             : /// However, it may show the tooltip to the right if there's not enough space
      68             : /// to the left of the widget.
      69             : /// `textStyle` has been used to set the font size of the 'message'.
      70             : /// `showDuration` accepts a Duration to continue showing the message after the long
      71             : /// press has been released.
      72             : /// `waitDuration` accepts a Duration for which a mouse pointer has to hover over the child
      73             : /// widget before the tooltip is shown.
      74             : ///
      75             : /// ```dart
      76             : /// Widget build(BuildContext context) {
      77             : ///   return MongolTooltip(
      78             : ///     message: 'I am a Tooltip',
      79             : ///     child: const Text('Tap this text and hold down to show a tooltip.'),
      80             : ///     decoration: BoxDecoration(
      81             : ///       borderRadius: BorderRadius.circular(25),
      82             : ///       gradient: const LinearGradient(colors: <Color>[Colors.amber, Colors.red]),
      83             : ///     ),
      84             : ///     width: 50,
      85             : ///     padding: const EdgeInsets.all(8.0),
      86             : ///     preferRight: false,
      87             : ///     textStyle: const TextStyle(
      88             : ///       fontSize: 24,
      89             : ///     ),
      90             : ///     showDuration: const Duration(seconds: 2),
      91             : ///     waitDuration: const Duration(seconds: 1),
      92             : ///   );
      93             : /// }
      94             : /// ```
      95             : /// {@end-tool}
      96             : ///
      97             : /// See also:
      98             : ///
      99             : ///  * <https://material.io/design/components/tooltips.html>
     100             : ///  * [TooltipTheme] or [ThemeData.tooltipTheme]
     101             : class MongolTooltip extends StatefulWidget {
     102             :   /// Creates a Mongol tooltip.
     103             :   ///
     104             :   /// By default, tooltips should adhere to the
     105             :   /// [Material specification](https://material.io/design/components/tooltips.html#spec).
     106             :   /// If the optional constructor parameters are not defined, the values
     107             :   /// provided by [TooltipTheme.of] will be used if a [TooltipTheme] is present
     108             :   /// or specified in [ThemeData].
     109             :   ///
     110             :   /// All parameters that are defined in the constructor will
     111             :   /// override the default values _and_ the values in [TooltipTheme.of].
     112           2 :   const MongolTooltip({
     113             :     Key? key,
     114             :     required this.message,
     115             :     this.width,
     116             :     this.padding,
     117             :     this.margin,
     118             :     this.horizontalOffset,
     119             :     this.preferRight,
     120             :     this.excludeFromSemantics,
     121             :     this.decoration,
     122             :     this.textStyle,
     123             :     this.waitDuration,
     124             :     this.showDuration,
     125             :     this.child,
     126           2 :   }) : super(key: key);
     127             : 
     128             :   /// The text to display in the tooltip.
     129             :   final String message;
     130             : 
     131             :   /// The width of the tooltip's [child].
     132             :   ///
     133             :   /// If the [child] is null, then this is the tooltip's intrinsic width.
     134             :   final double? width;
     135             : 
     136             :   /// The amount of space by which to inset the tooltip's [child].
     137             :   ///
     138             :   /// Defaults to 16.0 logical pixels in each direction.
     139             :   final EdgeInsetsGeometry? padding;
     140             : 
     141             :   /// The empty space that surrounds the tooltip.
     142             :   ///
     143             :   /// Defines the tooltip's outer [Container.margin]. By default, a
     144             :   /// long tooltip will span the height of its window. If tall enough,
     145             :   /// a tooltip might also span the window's width. This property allows
     146             :   /// one to define how much space the tooltip must be inset from the edges
     147             :   /// of their display window.
     148             :   ///
     149             :   /// If this property is null, then [TooltipThemeData.margin] is used.
     150             :   /// If [TooltipThemeData.margin] is also null, the default margin is
     151             :   /// 0.0 logical pixels on all sides.
     152             :   final EdgeInsetsGeometry? margin;
     153             : 
     154             :   /// The horizontal gap between the widget and the displayed tooltip.
     155             :   ///
     156             :   /// When [preferRight] is set to true and tooltips have sufficient space to
     157             :   /// display themselves, this property defines how much horizontal space
     158             :   /// tooltips will position themselves to the right of their corresponding widgets.
     159             :   /// Otherwise, tooltips will position themselves to the left of their corresponding
     160             :   /// widgets with the given offset.
     161             :   final double? horizontalOffset;
     162             : 
     163             :   /// Whether the tooltip defaults to being displayed to the right of the widget.
     164             :   ///
     165             :   /// Defaults to true. If there is insufficient space to display the tooltip in
     166             :   /// the preferred direction, the tooltip will be displayed in the opposite
     167             :   /// direction.
     168             :   final bool? preferRight;
     169             : 
     170             :   /// Whether the tooltip's [message] should be excluded from the semantics
     171             :   /// tree.
     172             :   ///
     173             :   /// Defaults to false. A tooltip will add a [Semantics] label that is set to
     174             :   /// [MongolTooltip.message]. Set this property to true if the app is going to
     175             :   /// provide its own custom semantics label.
     176             :   final bool? excludeFromSemantics;
     177             : 
     178             :   /// The widget below this widget in the tree.
     179             :   ///
     180             :   /// {@macro flutter.widgets.ProxyWidget.child}
     181             :   final Widget? child;
     182             : 
     183             :   /// Specifies the tooltip's shape and background color.
     184             :   ///
     185             :   /// The tooltip shape defaults to a rounded rectangle with a border radius of
     186             :   /// 4.0. Tooltips will also default to an opacity of 90% and with the color
     187             :   /// [Colors.grey[700]] if [ThemeData.brightness] is [Brightness.dark], and
     188             :   /// [Colors.white] if it is [Brightness.light].
     189             :   final Decoration? decoration;
     190             : 
     191             :   /// The style to use for the message of the tooltip.
     192             :   ///
     193             :   /// If null, the message's [TextStyle] will be determined based on
     194             :   /// [ThemeData]. If [ThemeData.brightness] is set to [Brightness.dark],
     195             :   /// [TextTheme.bodyText2] of [ThemeData.textTheme] will be used with
     196             :   /// [Colors.white]. Otherwise, if [ThemeData.brightness] is set to
     197             :   /// [Brightness.light], [TextTheme.bodyText2] of [ThemeData.textTheme] will be
     198             :   /// used with [Colors.black].
     199             :   final TextStyle? textStyle;
     200             : 
     201             :   /// The length of time that a pointer must hover over a tooltip's widget
     202             :   /// before the tooltip will be shown.
     203             :   ///
     204             :   /// Once the pointer leaves the widget, the tooltip will immediately
     205             :   /// disappear.
     206             :   ///
     207             :   /// Defaults to 0 milliseconds (tooltips are shown immediately upon hover).
     208             :   final Duration? waitDuration;
     209             : 
     210             :   /// The length of time that the tooltip will be shown after a long press
     211             :   /// is released.
     212             :   ///
     213             :   /// Defaults to 1.5 seconds.
     214             :   final Duration? showDuration;
     215             : 
     216           2 :   @override
     217           2 :   _MongolTooltipState createState() => _MongolTooltipState();
     218             : 
     219           0 :   @override
     220             :   void debugFillProperties(DiagnosticPropertiesBuilder properties) {
     221           0 :     super.debugFillProperties(properties);
     222           0 :     properties.add(StringProperty('message', message, showName: false));
     223           0 :     properties.add(DoubleProperty('width', width, defaultValue: null));
     224           0 :     properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding,
     225             :         defaultValue: null));
     226           0 :     properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('margin', margin,
     227             :         defaultValue: null));
     228           0 :     properties.add(DoubleProperty('horizontal offset', horizontalOffset,
     229             :         defaultValue: null));
     230           0 :     properties.add(FlagProperty('position',
     231           0 :         value: preferRight,
     232             :         ifTrue: 'right',
     233             :         ifFalse: 'left',
     234             :         showName: true,
     235             :         defaultValue: null));
     236           0 :     properties.add(FlagProperty('semantics',
     237           0 :         value: excludeFromSemantics,
     238             :         ifTrue: 'excluded',
     239             :         showName: true,
     240             :         defaultValue: null));
     241           0 :     properties.add(DiagnosticsProperty<Duration>('wait duration', waitDuration,
     242             :         defaultValue: null));
     243           0 :     properties.add(DiagnosticsProperty<Duration>('show duration', showDuration,
     244             :         defaultValue: null));
     245             :   }
     246             : }
     247             : 
     248             : class _MongolTooltipState extends State<MongolTooltip>
     249             :     with SingleTickerProviderStateMixin {
     250             :   static const double _defaultHorizontalOffset = 24.0;
     251             :   static const bool _defaultPreferRight = true;
     252             :   static const EdgeInsetsGeometry _defaultMargin = EdgeInsets.zero;
     253             :   static const Duration _fadeInDuration = Duration(milliseconds: 150);
     254             :   static const Duration _fadeOutDuration = Duration(milliseconds: 75);
     255             :   static const Duration _defaultShowDuration = Duration(milliseconds: 1500);
     256             :   static const Duration _defaultWaitDuration = Duration.zero;
     257             :   static const bool _defaultExcludeFromSemantics = false;
     258             : 
     259             :   late double width;
     260             :   late EdgeInsetsGeometry padding;
     261             :   late EdgeInsetsGeometry margin;
     262             :   late Decoration decoration;
     263             :   late TextStyle textStyle;
     264             :   late double horizontalOffset;
     265             :   late bool preferRight;
     266             :   late bool excludeFromSemantics;
     267             :   late AnimationController _controller;
     268             :   OverlayEntry? _entry;
     269             :   Timer? _hideTimer;
     270             :   Timer? _showTimer;
     271             :   late Duration showDuration;
     272             :   late Duration waitDuration;
     273             :   late bool _mouseIsConnected;
     274             :   bool _longPressActivated = false;
     275             : 
     276           2 :   @override
     277             :   void initState() {
     278           2 :     super.initState();
     279           8 :     _mouseIsConnected = RendererBinding.instance!.mouseTracker.mouseIsConnected;
     280           4 :     _controller = AnimationController(
     281             :       duration: _fadeInDuration,
     282             :       reverseDuration: _fadeOutDuration,
     283             :       vsync: this,
     284           4 :     )..addStatusListener(_handleStatusChanged);
     285             :     // Listen to see when a mouse is added.
     286           4 :     RendererBinding.instance!.mouseTracker
     287           4 :         .addListener(_handleMouseTrackerChange);
     288             :     // Listen to global pointer events so that we can hide a tooltip immediately
     289             :     // if some other control is clicked on.
     290           8 :     GestureBinding.instance!.pointerRouter.addGlobalRoute(_handlePointerEvent);
     291             :   }
     292             : 
     293             :   // https://material.io/components/tooltips#specs
     294           2 :   double _getDefaultTooltipWidth() {
     295           4 :     final ThemeData theme = Theme.of(context);
     296           2 :     switch (theme.platform) {
     297           2 :       case TargetPlatform.macOS:
     298           2 :       case TargetPlatform.linux:
     299           2 :       case TargetPlatform.windows:
     300             :         return 24.0;
     301             :       default:
     302             :         return 32.0;
     303             :     }
     304             :   }
     305             : 
     306           2 :   EdgeInsets _getDefaultPadding() {
     307           4 :     final ThemeData theme = Theme.of(context);
     308           2 :     switch (theme.platform) {
     309           2 :       case TargetPlatform.macOS:
     310           2 :       case TargetPlatform.linux:
     311           2 :       case TargetPlatform.windows:
     312             :         return const EdgeInsets.symmetric(vertical: 8.0);
     313             :       default:
     314             :         return const EdgeInsets.symmetric(vertical: 16.0);
     315             :     }
     316             :   }
     317             : 
     318           2 :   double _getDefaultFontSize() {
     319           4 :     final ThemeData theme = Theme.of(context);
     320           2 :     switch (theme.platform) {
     321           2 :       case TargetPlatform.macOS:
     322           2 :       case TargetPlatform.linux:
     323           2 :       case TargetPlatform.windows:
     324             :         return 10.0;
     325             :       default:
     326             :         return 14.0;
     327             :     }
     328             :   }
     329             : 
     330             :   // Forces a rebuild if a mouse has been added or removed.
     331           0 :   void _handleMouseTrackerChange() {
     332           0 :     if (!mounted) {
     333             :       return;
     334             :     }
     335             :     final bool mouseIsConnected =
     336           0 :         RendererBinding.instance!.mouseTracker.mouseIsConnected;
     337           0 :     if (mouseIsConnected != _mouseIsConnected) {
     338           0 :       setState(() {
     339           0 :         _mouseIsConnected = mouseIsConnected;
     340             :       });
     341             :     }
     342             :   }
     343             : 
     344           0 :   void _handleStatusChanged(AnimationStatus status) {
     345           0 :     if (status == AnimationStatus.dismissed) {
     346           0 :       _hideTooltip(immediately: true);
     347             :     }
     348             :   }
     349             : 
     350           0 :   void _hideTooltip({bool immediately = false}) {
     351           0 :     _showTimer?.cancel();
     352           0 :     _showTimer = null;
     353             :     if (immediately) {
     354           0 :       _removeEntry();
     355             :       return;
     356             :     }
     357           0 :     if (_longPressActivated) {
     358             :       // Tool tips activated by long press should stay around for the showDuration.
     359           0 :       _hideTimer ??= Timer(showDuration, _controller.reverse);
     360             :     } else {
     361             :       // Tool tips activated by hover should disappear as soon as the mouse
     362             :       // leaves the control.
     363           0 :       _controller.reverse();
     364             :     }
     365           0 :     _longPressActivated = false;
     366             :   }
     367             : 
     368           0 :   void _showTooltip({bool immediately = false}) {
     369           0 :     _hideTimer?.cancel();
     370           0 :     _hideTimer = null;
     371             :     if (immediately) {
     372           0 :       ensureTooltipVisible();
     373             :       return;
     374             :     }
     375           0 :     _showTimer ??= Timer(waitDuration, ensureTooltipVisible);
     376             :   }
     377             : 
     378             :   /// Shows the tooltip if it is not already visible.
     379             :   ///
     380             :   /// Returns `false` when the tooltip was already visible or if the context has
     381             :   /// become null.
     382           0 :   bool ensureTooltipVisible() {
     383           0 :     _showTimer?.cancel();
     384           0 :     _showTimer = null;
     385           0 :     if (_entry != null) {
     386             :       // Stop trying to hide, if we were.
     387           0 :       _hideTimer?.cancel();
     388           0 :       _hideTimer = null;
     389           0 :       _controller.forward();
     390             :       return false; // Already visible.
     391             :     }
     392           0 :     _createNewEntry();
     393           0 :     _controller.forward();
     394             :     return true;
     395             :   }
     396             : 
     397           0 :   void _createNewEntry() {
     398           0 :     final OverlayState overlayState = Overlay.of(
     399           0 :       context,
     400           0 :       debugRequiredFor: widget,
     401             :     )!;
     402             : 
     403           0 :     final RenderBox box = context.findRenderObject()! as RenderBox;
     404           0 :     final Offset target = box.localToGlobal(
     405           0 :       box.size.center(Offset.zero),
     406           0 :       ancestor: overlayState.context.findRenderObject(),
     407             :     );
     408             : 
     409             :     // We create this widget outside of the overlay entry's builder to prevent
     410             :     // updated values from happening to leak into the overlay when the overlay
     411             :     // rebuilds.
     412           0 :     final Widget overlay = Directionality(
     413             :       textDirection: TextDirection.ltr,
     414           0 :       child: _MongolTooltipOverlay(
     415           0 :         message: widget.message,
     416           0 :         width: width,
     417           0 :         padding: padding,
     418           0 :         margin: margin,
     419           0 :         decoration: decoration,
     420           0 :         textStyle: textStyle,
     421           0 :         animation: CurvedAnimation(
     422           0 :           parent: _controller,
     423             :           curve: Curves.fastOutSlowIn,
     424             :         ),
     425             :         target: target,
     426           0 :         horizontalOffset: horizontalOffset,
     427           0 :         preferRight: preferRight,
     428             :       ),
     429             :     );
     430           0 :     _entry = OverlayEntry(builder: (BuildContext context) => overlay);
     431           0 :     overlayState.insert(_entry!);
     432           0 :     SemanticsService.tooltip(widget.message);
     433             :   }
     434             : 
     435           0 :   void _removeEntry() {
     436           0 :     _hideTimer?.cancel();
     437           0 :     _hideTimer = null;
     438           0 :     _showTimer?.cancel();
     439           0 :     _showTimer = null;
     440           0 :     _entry?.remove();
     441           0 :     _entry = null;
     442             :   }
     443             : 
     444           2 :   void _handlePointerEvent(PointerEvent event) {
     445           2 :     if (_entry == null) {
     446             :       return;
     447             :     }
     448           0 :     if (event is PointerUpEvent || event is PointerCancelEvent) {
     449           0 :       _hideTooltip();
     450           0 :     } else if (event is PointerDownEvent) {
     451           0 :       _hideTooltip(immediately: true);
     452             :     }
     453             :   }
     454             : 
     455           2 :   @override
     456             :   void deactivate() {
     457           2 :     if (_entry != null) {
     458           0 :       _hideTooltip(immediately: true);
     459             :     }
     460           2 :     _showTimer?.cancel();
     461           2 :     super.deactivate();
     462             :   }
     463             : 
     464           2 :   @override
     465             :   void dispose() {
     466           4 :     GestureBinding.instance!.pointerRouter
     467           4 :         .removeGlobalRoute(_handlePointerEvent);
     468           4 :     RendererBinding.instance!.mouseTracker
     469           4 :         .removeListener(_handleMouseTrackerChange);
     470           2 :     if (_entry != null) {
     471           0 :       _removeEntry();
     472             :     }
     473           4 :     _controller.dispose();
     474           2 :     super.dispose();
     475             :   }
     476             : 
     477           0 :   void _handleLongPress() {
     478           0 :     _longPressActivated = true;
     479           0 :     final bool tooltipCreated = ensureTooltipVisible();
     480             :     if (tooltipCreated) {
     481           0 :       Feedback.forLongPress(context);
     482             :     }
     483             :   }
     484             : 
     485           2 :   @override
     486             :   Widget build(BuildContext context) {
     487           4 :     assert(Overlay.of(context, debugRequiredFor: widget) != null);
     488           2 :     final ThemeData theme = Theme.of(context);
     489           2 :     final TooltipThemeData tooltipTheme = TooltipTheme.of(context);
     490             :     final TextStyle defaultTextStyle;
     491             :     final BoxDecoration defaultDecoration;
     492           4 :     if (theme.brightness == Brightness.dark) {
     493           0 :       defaultTextStyle = theme.textTheme.bodyText2!.copyWith(
     494             :         color: Colors.black,
     495           0 :         fontSize: _getDefaultFontSize(),
     496             :       );
     497           0 :       defaultDecoration = BoxDecoration(
     498           0 :         color: Colors.white.withOpacity(0.9),
     499             :         borderRadius: const BorderRadius.all(Radius.circular(4)),
     500             :       );
     501             :     } else {
     502           6 :       defaultTextStyle = theme.textTheme.bodyText2!.copyWith(
     503             :         color: Colors.white,
     504           2 :         fontSize: _getDefaultFontSize(),
     505             :       );
     506           2 :       defaultDecoration = BoxDecoration(
     507           4 :         color: Colors.grey[700]!.withOpacity(0.9),
     508             :         borderRadius: const BorderRadius.all(Radius.circular(4)),
     509             :       );
     510             :     }
     511             : 
     512          10 :     width = widget.width ?? tooltipTheme.height ?? _getDefaultTooltipWidth();
     513          10 :     padding = widget.padding ?? tooltipTheme.padding ?? _getDefaultPadding();
     514           8 :     margin = widget.margin ?? tooltipTheme.margin ?? _defaultMargin;
     515           6 :     horizontalOffset = widget.horizontalOffset ??
     516           2 :         tooltipTheme.verticalOffset ??
     517             :         _defaultHorizontalOffset;
     518           2 :     preferRight =
     519           6 :         widget.preferRight ?? tooltipTheme.preferBelow ?? _defaultPreferRight;
     520           6 :     excludeFromSemantics = widget.excludeFromSemantics ??
     521           2 :         tooltipTheme.excludeFromSemantics ??
     522             :         _defaultExcludeFromSemantics;
     523           2 :     decoration =
     524           6 :         widget.decoration ?? tooltipTheme.decoration ?? defaultDecoration;
     525           8 :     textStyle = widget.textStyle ?? tooltipTheme.textStyle ?? defaultTextStyle;
     526           6 :     waitDuration = widget.waitDuration ??
     527           2 :         tooltipTheme.waitDuration ??
     528             :         _defaultWaitDuration;
     529           6 :     showDuration = widget.showDuration ??
     530           2 :         tooltipTheme.showDuration ??
     531             :         _defaultShowDuration;
     532             : 
     533           2 :     Widget result = GestureDetector(
     534             :       behavior: HitTestBehavior.opaque,
     535           2 :       onLongPress: _handleLongPress,
     536             :       excludeFromSemantics: true,
     537           2 :       child: Semantics(
     538           6 :         label: excludeFromSemantics ? null : widget.message,
     539           4 :         child: widget.child,
     540             :       ),
     541             :     );
     542             : 
     543             :     // Only check for hovering if there is a mouse connected.
     544           2 :     if (_mouseIsConnected) {
     545           0 :       result = MouseRegion(
     546           0 :         onEnter: (PointerEnterEvent event) => _showTooltip(),
     547           0 :         onExit: (PointerExitEvent event) => _hideTooltip(),
     548             :         child: result,
     549             :       );
     550             :     }
     551             : 
     552             :     return result;
     553             :   }
     554             : }
     555             : 
     556             : /// A delegate for computing the layout of a tooltip to be displayed left or
     557             : /// right of a target specified in the global coordinate system.
     558             : class _MongolTooltipPositionDelegate extends SingleChildLayoutDelegate {
     559             :   /// Creates a delegate for computing the layout of a tooltip.
     560             :   ///
     561             :   /// The arguments must not be null.
     562           0 :   _MongolTooltipPositionDelegate({
     563             :     required this.target,
     564             :     required this.horizontalOffset,
     565             :     required this.preferRight,
     566             :   });
     567             : 
     568             :   /// The offset of the target the tooltip is positioned near in the global
     569             :   /// coordinate system.
     570             :   final Offset target;
     571             : 
     572             :   /// The amount of horizontal distance between the target and the displayed
     573             :   /// tooltip.
     574             :   final double horizontalOffset;
     575             : 
     576             :   /// Whether the tooltip is displayed to the right of its widget by default.
     577             :   ///
     578             :   /// If there is insufficient space to display the tooltip in the preferred
     579             :   /// direction, the tooltip will be displayed in the opposite direction.
     580             :   final bool preferRight;
     581             : 
     582           0 :   @override
     583             :   BoxConstraints getConstraintsForChild(BoxConstraints constraints) =>
     584           0 :       constraints.loosen();
     585             : 
     586           0 :   @override
     587             :   Offset getPositionForChild(Size size, Size childSize) {
     588           0 :     return positionMongolDependentBox(
     589             :       size: size,
     590             :       childSize: childSize,
     591           0 :       target: target,
     592           0 :       horizontalOffset: horizontalOffset,
     593           0 :       preferRight: preferRight,
     594             :     );
     595             :   }
     596             : 
     597           0 :   @override
     598             :   bool shouldRelayout(_MongolTooltipPositionDelegate oldDelegate) {
     599           0 :     return target != oldDelegate.target ||
     600           0 :         horizontalOffset != oldDelegate.horizontalOffset ||
     601           0 :         preferRight != oldDelegate.preferRight;
     602             :   }
     603             : }
     604             : 
     605             : class _MongolTooltipOverlay extends StatelessWidget {
     606           0 :   const _MongolTooltipOverlay({
     607             :     Key? key,
     608             :     required this.message,
     609             :     required this.width,
     610             :     this.padding,
     611             :     this.margin,
     612             :     this.decoration,
     613             :     this.textStyle,
     614             :     required this.animation,
     615             :     required this.target,
     616             :     required this.horizontalOffset,
     617             :     required this.preferRight,
     618           0 :   }) : super(key: key);
     619             : 
     620             :   final String message;
     621             :   final double width;
     622             :   final EdgeInsetsGeometry? padding;
     623             :   final EdgeInsetsGeometry? margin;
     624             :   final Decoration? decoration;
     625             :   final TextStyle? textStyle;
     626             :   final Animation<double> animation;
     627             :   final Offset target;
     628             :   final double horizontalOffset;
     629             :   final bool preferRight;
     630             : 
     631           0 :   @override
     632             :   Widget build(BuildContext context) {
     633           0 :     return Positioned.fill(
     634           0 :       child: IgnorePointer(
     635           0 :         child: CustomSingleChildLayout(
     636           0 :           delegate: _MongolTooltipPositionDelegate(
     637           0 :             target: target,
     638           0 :             horizontalOffset: horizontalOffset,
     639           0 :             preferRight: preferRight,
     640             :           ),
     641           0 :           child: FadeTransition(
     642           0 :             opacity: animation,
     643           0 :             child: ConstrainedBox(
     644           0 :               constraints: BoxConstraints(minWidth: width),
     645           0 :               child: DefaultTextStyle(
     646           0 :                 style: Theme.of(context).textTheme.bodyText2!,
     647           0 :                 child: Container(
     648           0 :                   decoration: decoration,
     649           0 :                   padding: padding,
     650           0 :                   margin: margin,
     651           0 :                   child: Center(
     652             :                     widthFactor: 1.0,
     653             :                     heightFactor: 1.0,
     654           0 :                     child: MongolText(
     655           0 :                       message,
     656           0 :                       style: textStyle,
     657             :                     ),
     658             :                   ),
     659             :                 ),
     660             :               ),
     661             :             ),
     662             :           ),
     663             :         ),
     664             :       ),
     665             :     );
     666             :   }
     667             : }
     668             : 
     669             : /// Position a child box within a container box, either left or right of a target
     670             : /// point.
     671             : ///
     672             : /// The container's size is described by `size`.
     673             : ///
     674             : /// The target point is specified by `target`, as an offset from the top left of
     675             : /// the container.
     676             : ///
     677             : /// The child box's size is given by `childSize`.
     678             : ///
     679             : /// The return value is the suggested distance from the top left of the
     680             : /// container box to the top left of the child box.
     681             : ///
     682             : /// The suggested position will be to the left of the target point if `preferRight` is
     683             : /// false, and to the right of the target point if it is true, unless it wouldn't fit on
     684             : /// the preferred side but would fit on the other side.
     685             : ///
     686             : /// The suggested position will place the nearest side of the child to the
     687             : /// target point `horizontalOffset` from the target point (even if it cannot fit
     688             : /// given that constraint).
     689             : ///
     690             : /// The suggested position will be at least `margin` away from the edge of the
     691             : /// container. If possible, the child will be positioned so that its center is
     692             : /// aligned with the target point. If the child cannot fit vertically within
     693             : /// the container given the margin, then the child will be centered in the
     694             : /// container.
     695             : ///
     696             : /// Used by [MongolTooltip] to position a tooltip relative to its parent.
     697             : ///
     698             : /// The arguments must not be null.
     699           0 : Offset positionMongolDependentBox({
     700             :   required Size size,
     701             :   required Size childSize,
     702             :   required Offset target,
     703             :   required bool preferRight,
     704             :   double horizontalOffset = 0.0,
     705             :   double margin = 10.0,
     706             : }) {
     707             :   // HORIZONTAL DIRECTION
     708             :   final bool fitsRight =
     709           0 :       target.dx + horizontalOffset + childSize.width <= size.width - margin;
     710             :   final bool fitsLeft =
     711           0 :       target.dx - horizontalOffset - childSize.width >= margin;
     712             :   final bool tooltipRight =
     713             :       preferRight ? fitsRight || !fitsLeft : !(fitsLeft || !fitsRight);
     714             :   double x;
     715             :   if (tooltipRight) {
     716           0 :     x = math.min(target.dx + horizontalOffset, size.width - margin);
     717             :   } else {
     718           0 :     x = math.max(target.dx - horizontalOffset - childSize.width, margin);
     719             :   }
     720             :   // VERTICAL DIRECTION
     721             :   double y;
     722           0 :   if (size.height - margin * 2.0 < childSize.height) {
     723           0 :     y = (size.height - childSize.height) / 2.0;
     724             :   } else {
     725             :     final double normalizedTargetY =
     726           0 :         target.dy.clamp(margin, size.height - margin);
     727           0 :     final double edge = margin + childSize.height / 2.0;
     728           0 :     if (normalizedTargetY < edge) {
     729             :       y = margin;
     730           0 :     } else if (normalizedTargetY > size.height - edge) {
     731           0 :       y = size.height - margin - childSize.height;
     732             :     } else {
     733           0 :       y = normalizedTargetY - childSize.height / 2.0;
     734             :     }
     735             :   }
     736           0 :   return Offset(x, y);
     737             : }

Generated by: LCOV version 1.15