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 : }
|