tooltip.dart 20.9 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
Hixie's avatar
Hixie committed
2 3 4 5 6
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

7
import 'package:flutter/gestures.dart';
8
import 'package:flutter/rendering.dart';
Hixie's avatar
Hixie committed
9 10
import 'package:flutter/widgets.dart';

11
import 'colors.dart';
12
import 'feedback.dart';
13
import 'theme.dart';
14
import 'tooltip_theme.dart';
15 16 17

/// A material design tooltip.
///
18 19 20
/// Tooltips provide text labels which help explain the function of a button or
/// other user interface action. Wrap the button in a [Tooltip] widget and provide
/// a message which will be shown when the widget is long pressed.
21
///
22 23 24 25 26 27 28 29
/// Many widgets, such as [IconButton], [FloatingActionButton], and
/// [PopupMenuButton] have a `tooltip` property that, when non-null, causes the
/// widget to include a [Tooltip] in its build.
///
/// Tooltips improve the accessibility of visual widgets by proving a textual
/// representation of the widget, which, for example, can be vocalized by a
/// screen reader.
///
30 31
/// {@youtube 560 315 https://www.youtube.com/watch?v=EeEfD5fI-5Q}
///
32
/// {@tool dartpad --template=stateless_widget_scaffold_center}
33 34 35
///
/// This example show a basic [Tooltip] which has a [Text] as child.
/// [message] contains your label to be shown by the tooltip when
36 37
/// the child that Tooltip wraps is hovered over on web or desktop. On mobile,
/// the tooltip is shown when the widget is long pressed.
38 39 40
///
/// ```dart
/// Widget build(BuildContext context) {
41 42 43
///   return const Tooltip(
///     message: 'I am a Tooltip',
///     child: Text('Hover over the text to show a tooltip.'),
44 45 46 47 48
///   );
/// }
/// ```
/// {@end-tool}
///
49
/// {@tool dartpad --template=stateless_widget_scaffold_center}
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
///
/// This example covers most of the attributes available in Tooltip.
/// `decoration` has been used to give a gradient and borderRadius to Tooltip.
/// `height` has been used to set a specific height of the Tooltip.
/// `preferBelow` is false, the tooltip will prefer showing above [Tooltip]'s child widget.
/// However, it may show the tooltip below if there's not enough space
/// above the widget.
/// `textStyle` has been used to set the font size of the 'message'.
/// `showDuration` accepts a Duration to continue showing the message after the long
/// press has been released.
/// `waitDuration` accepts a Duration for which a mouse pointer has to hover over the child
/// widget before the tooltip is shown.
///
/// ```dart
/// Widget build(BuildContext context) {
65
///   return Tooltip(
66 67
///     message: 'I am a Tooltip',
///     child: const Text('Tap this text and hold down to show a tooltip.'),
68 69
///     decoration: BoxDecoration(
///       borderRadius: BorderRadius.circular(25),
70
///       gradient: const LinearGradient(colors: <Color>[Colors.amber, Colors.red]),
71 72
///     ),
///     height: 50,
73
///     padding: const EdgeInsets.all(8.0),
74
///     preferBelow: false,
75
///     textStyle: const TextStyle(
76
///       fontSize: 24,
77
///     ),
78 79
///     showDuration: const Duration(seconds: 2),
///     waitDuration: const Duration(seconds: 1),
80 81 82 83
///   );
/// }
/// ```
/// {@end-tool}
84
///
85 86
/// See also:
///
87
///  * <https://material.io/design/components/tooltips.html>
88
///  * [TooltipTheme] or [ThemeData.tooltipTheme]
89
class Tooltip extends StatefulWidget {
90 91
  /// Creates a tooltip.
  ///
92 93 94 95 96
  /// By default, tooltips should adhere to the
  /// [Material specification](https://material.io/design/components/tooltips.html#spec).
  /// If the optional constructor parameters are not defined, the values
  /// provided by [TooltipTheme.of] will be used if a [TooltipTheme] is present
  /// or specified in [ThemeData].
97
  ///
98 99
  /// All parameters that are defined in the constructor will
  /// override the default values _and_ the values in [TooltipTheme.of].
100
  const Tooltip({
101 102
    Key? key,
    required this.message,
103 104
    this.height,
    this.padding,
105
    this.margin,
106 107 108
    this.verticalOffset,
    this.preferBelow,
    this.excludeFromSemantics,
109
    this.decoration,
110 111 112
    this.textStyle,
    this.waitDuration,
    this.showDuration,
113
    this.child,
114 115
  }) : assert(message != null),
       super(key: key);
Hixie's avatar
Hixie committed
116

117
  /// The text to display in the tooltip.
Hixie's avatar
Hixie committed
118
  final String message;
119

120
  /// The height of the tooltip's [child].
121
  ///
122
  /// If the [child] is null, then this is the tooltip's intrinsic height.
123
  final double? height;
124

125
  /// The amount of space by which to inset the tooltip's [child].
126 127
  ///
  /// Defaults to 16.0 logical pixels in each direction.
128
  final EdgeInsetsGeometry? padding;
129

130 131 132 133 134 135 136 137 138 139 140
  /// The empty space that surrounds the tooltip.
  ///
  /// Defines the tooltip's outer [Container.margin]. By default, a
  /// long tooltip will span the width of its window. If long enough,
  /// a tooltip might also span the window's height. This property allows
  /// one to define how much space the tooltip must be inset from the edges
  /// of their display window.
  ///
  /// If this property is null, then [TooltipThemeData.margin] is used.
  /// If [TooltipThemeData.margin] is also null, the default margin is
  /// 0.0 logical pixels on all sides.
141
  final EdgeInsetsGeometry? margin;
142

143
  /// The vertical gap between the widget and the displayed tooltip.
144 145 146 147 148 149
  ///
  /// When [preferBelow] is set to true and tooltips have sufficient space to
  /// display themselves, this property defines how much vertical space
  /// tooltips will position themselves under their corresponding widgets.
  /// Otherwise, tooltips will position themselves above their corresponding
  /// widgets with the given offset.
150
  final double? verticalOffset;
151

152 153 154 155 156
  /// Whether the tooltip defaults to being displayed below the widget.
  ///
  /// Defaults to true. If there is insufficient space to display the tooltip in
  /// the preferred direction, the tooltip will be displayed in the opposite
  /// direction.
157
  final bool? preferBelow;
158

159 160
  /// Whether the tooltip's [message] should be excluded from the semantics
  /// tree.
161
  ///
162
  /// Defaults to false. A tooltip will add a [Semantics] label that is set to
163 164
  /// [Tooltip.message]. Set this property to true if the app is going to
  /// provide its own custom semantics label.
165
  final bool? excludeFromSemantics;
166

167
  /// The widget below this widget in the tree.
168
  ///
169
  /// {@macro flutter.widgets.ProxyWidget.child}
170
  final Widget? child;
Hixie's avatar
Hixie committed
171

172 173
  /// Specifies the tooltip's shape and background color.
  ///
174 175 176 177
  /// The tooltip shape defaults to a rounded rectangle with a border radius of
  /// 4.0. Tooltips will also default to an opacity of 90% and with the color
  /// [Colors.grey[700]] if [ThemeData.brightness] is [Brightness.dark], and
  /// [Colors.white] if it is [Brightness.light].
178
  final Decoration? decoration;
179

180 181 182 183
  /// The style to use for the message of the tooltip.
  ///
  /// If null, the message's [TextStyle] will be determined based on
  /// [ThemeData]. If [ThemeData.brightness] is set to [Brightness.dark],
184 185 186 187
  /// [TextTheme.bodyText2] of [ThemeData.textTheme] will be used with
  /// [Colors.white]. Otherwise, if [ThemeData.brightness] is set to
  /// [Brightness.light], [TextTheme.bodyText2] of [ThemeData.textTheme] will be
  /// used with [Colors.black].
188
  final TextStyle? textStyle;
189 190 191

  /// The length of time that a pointer must hover over a tooltip's widget
  /// before the tooltip will be shown.
192
  ///
193 194 195
  /// Once the pointer leaves the widget, the tooltip will immediately
  /// disappear.
  ///
196
  /// Defaults to 0 milliseconds (tooltips are shown immediately upon hover).
197
  final Duration? waitDuration;
198

199 200
  /// The length of time that the tooltip will be shown after a long press
  /// is released.
201 202
  ///
  /// Defaults to 1.5 seconds.
203
  final Duration? showDuration;
204

205
  @override
206
  State<Tooltip> createState() => _TooltipState();
Hixie's avatar
Hixie committed
207

208
  @override
209 210
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
211
    properties.add(StringProperty('message', message, showName: false));
212 213
    properties.add(DoubleProperty('height', height, defaultValue: null));
    properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: null));
214
    properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('margin', margin, defaultValue: null));
215 216 217 218 219
    properties.add(DoubleProperty('vertical offset', verticalOffset, defaultValue: null));
    properties.add(FlagProperty('position', value: preferBelow, ifTrue: 'below', ifFalse: 'above', showName: true, defaultValue: null));
    properties.add(FlagProperty('semantics', value: excludeFromSemantics, ifTrue: 'excluded', showName: true, defaultValue: null));
    properties.add(DiagnosticsProperty<Duration>('wait duration', waitDuration, defaultValue: null));
    properties.add(DiagnosticsProperty<Duration>('show duration', showDuration, defaultValue: null));
Hixie's avatar
Hixie committed
220
  }
Hixie's avatar
Hixie committed
221 222
}

223
class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
224 225
  static const double _defaultVerticalOffset = 24.0;
  static const bool _defaultPreferBelow = true;
226
  static const EdgeInsetsGeometry _defaultMargin = EdgeInsets.zero;
227 228
  static const Duration _fadeInDuration = Duration(milliseconds: 150);
  static const Duration _fadeOutDuration = Duration(milliseconds: 75);
229
  static const Duration _defaultShowDuration = Duration(milliseconds: 1500);
230
  static const Duration _defaultWaitDuration = Duration.zero;
231 232
  static const bool _defaultExcludeFromSemantics = false;

233 234 235 236 237 238 239 240 241 242 243 244 245 246 247
  late double height;
  late EdgeInsetsGeometry padding;
  late EdgeInsetsGeometry margin;
  late Decoration decoration;
  late TextStyle textStyle;
  late double verticalOffset;
  late bool preferBelow;
  late bool excludeFromSemantics;
  late AnimationController _controller;
  OverlayEntry? _entry;
  Timer? _hideTimer;
  Timer? _showTimer;
  late Duration showDuration;
  late Duration waitDuration;
  late bool _mouseIsConnected;
248
  bool _longPressActivated = false;
Hixie's avatar
Hixie committed
249

250
  @override
Hixie's avatar
Hixie committed
251 252
  void initState() {
    super.initState();
253
    _mouseIsConnected = RendererBinding.instance!.mouseTracker.mouseIsConnected;
254 255 256 257 258
    _controller = AnimationController(
      duration: _fadeInDuration,
      reverseDuration: _fadeOutDuration,
      vsync: this,
    )
259
      ..addStatusListener(_handleStatusChanged);
260
    // Listen to see when a mouse is added.
261
    RendererBinding.instance!.mouseTracker.addListener(_handleMouseTrackerChange);
262 263
    // Listen to global pointer events so that we can hide a tooltip immediately
    // if some other control is clicked on.
264
    GestureBinding.instance!.pointerRouter.addGlobalRoute(_handlePointerEvent);
265 266
  }

267 268
  // https://material.io/components/tooltips#specs
  double _getDefaultTooltipHeight() {
269
    final ThemeData theme = Theme.of(context);
270 271 272 273 274 275 276 277 278 279 280
    switch (theme.platform) {
      case TargetPlatform.macOS:
      case TargetPlatform.linux:
      case TargetPlatform.windows:
        return 24.0;
      default:
        return 32.0;
    }
  }

  EdgeInsets _getDefaultPadding() {
281
    final ThemeData theme = Theme.of(context);
282 283 284 285 286 287 288 289 290 291 292
    switch (theme.platform) {
      case TargetPlatform.macOS:
      case TargetPlatform.linux:
      case TargetPlatform.windows:
        return const EdgeInsets.symmetric(horizontal: 8.0);
      default:
        return const EdgeInsets.symmetric(horizontal: 16.0);
    }
  }

  double _getDefaultFontSize() {
293
    final ThemeData theme = Theme.of(context);
294 295 296 297 298 299 300 301 302 303
    switch (theme.platform) {
      case TargetPlatform.macOS:
      case TargetPlatform.linux:
      case TargetPlatform.windows:
        return 10.0;
      default:
        return 14.0;
    }
  }

304 305 306 307 308
  // Forces a rebuild if a mouse has been added or removed.
  void _handleMouseTrackerChange() {
    if (!mounted) {
      return;
    }
309
    final bool mouseIsConnected = RendererBinding.instance!.mouseTracker.mouseIsConnected;
310 311 312 313 314
    if (mouseIsConnected != _mouseIsConnected) {
      setState((){
        _mouseIsConnected = mouseIsConnected;
      });
    }
315 316 317
  }

  void _handleStatusChanged(AnimationStatus status) {
318 319 320 321 322 323 324 325 326
    if (status == AnimationStatus.dismissed) {
      _hideTooltip(immediately: true);
    }
  }

  void _hideTooltip({ bool immediately = false }) {
    _showTimer?.cancel();
    _showTimer = null;
    if (immediately) {
327
      _removeEntry();
328 329 330
      return;
    }
    if (_longPressActivated) {
331
      // Tool tips activated by long press should stay around for the showDuration.
332
      _hideTimer ??= Timer(showDuration, _controller.reverse);
333 334 335 336 337 338 339 340 341 342 343 344 345 346 347
    } else {
      // Tool tips activated by hover should disappear as soon as the mouse
      // leaves the control.
      _controller.reverse();
    }
    _longPressActivated = false;
  }

  void _showTooltip({ bool immediately = false }) {
    _hideTimer?.cancel();
    _hideTimer = null;
    if (immediately) {
      ensureTooltipVisible();
      return;
    }
348
    _showTimer ??= Timer(waitDuration, ensureTooltipVisible);
Hixie's avatar
Hixie committed
349 350
  }

351 352
  /// Shows the tooltip if it is not already visible.
  ///
353 354
  /// Returns `false` when the tooltip was already visible or if the context has
  /// become null.
355
  bool ensureTooltipVisible() {
356 357
    _showTimer?.cancel();
    _showTimer = null;
358
    if (_entry != null) {
359 360 361
      // Stop trying to hide, if we were.
      _hideTimer?.cancel();
      _hideTimer = null;
362
      _controller.forward();
363
      return false; // Already visible.
364
    }
365 366 367 368 369 370
    _createNewEntry();
    _controller.forward();
    return true;
  }

  void _createNewEntry() {
371 372 373
    final OverlayState overlayState = Overlay.of(
      context,
      debugRequiredFor: widget,
374
    )!;
375

376
    final RenderBox box = context.findRenderObject()! as RenderBox;
377 378 379 380
    final Offset target = box.localToGlobal(
      box.size.center(Offset.zero),
      ancestor: overlayState.context.findRenderObject(),
    );
381

382 383 384
    // We create this widget outside of the overlay entry's builder to prevent
    // updated values from happening to leak into the overlay when the overlay
    // rebuilds.
385
    final Widget overlay = Directionality(
386
      textDirection: Directionality.of(context),
387 388 389 390 391 392 393 394 395 396 397 398 399 400
      child: _TooltipOverlay(
        message: widget.message,
        height: height,
        padding: padding,
        margin: margin,
        decoration: decoration,
        textStyle: textStyle,
        animation: CurvedAnimation(
          parent: _controller,
          curve: Curves.fastOutSlowIn,
        ),
        target: target,
        verticalOffset: verticalOffset,
        preferBelow: preferBelow,
401 402
      ),
    );
403
    _entry = OverlayEntry(builder: (BuildContext context) => overlay);
404
    overlayState.insert(_entry!);
405
    SemanticsService.tooltip(widget.message);
Hixie's avatar
Hixie committed
406 407
  }

408
  void _removeEntry() {
409 410 411 412 413
    _hideTimer?.cancel();
    _hideTimer = null;
    _showTimer?.cancel();
    _showTimer = null;
    _entry?.remove();
414
    _entry = null;
Hixie's avatar
Hixie committed
415 416
  }

417
  void _handlePointerEvent(PointerEvent event) {
418 419 420 421 422 423 424 425
    if (_entry == null) {
      return;
    }
    if (event is PointerUpEvent || event is PointerCancelEvent) {
      _hideTooltip();
    } else if (event is PointerDownEvent) {
      _hideTooltip(immediately: true);
    }
Hixie's avatar
Hixie committed
426 427
  }

428
  @override
Hixie's avatar
Hixie committed
429
  void deactivate() {
430 431 432
    if (_entry != null) {
      _hideTooltip(immediately: true);
    }
433
    _showTimer?.cancel();
434
    super.deactivate();
Hixie's avatar
Hixie committed
435 436
  }

437 438
  @override
  void dispose() {
439 440
    GestureBinding.instance!.pointerRouter.removeGlobalRoute(_handlePointerEvent);
    RendererBinding.instance!.mouseTracker.removeListener(_handleMouseTrackerChange);
441 442
    if (_entry != null)
      _removeEntry();
443
    _controller.dispose();
444 445 446
    super.dispose();
  }

447
  void _handleLongPress() {
448
    _longPressActivated = true;
449 450 451 452 453
    final bool tooltipCreated = ensureTooltipVisible();
    if (tooltipCreated)
      Feedback.forLongPress(context);
  }

454
  @override
Hixie's avatar
Hixie committed
455
  Widget build(BuildContext context) {
456
    assert(Overlay.of(context, debugRequiredFor: widget) != null);
457
    final ThemeData theme = Theme.of(context);
458
    final TooltipThemeData tooltipTheme = TooltipTheme.of(context);
459 460
    final TextStyle defaultTextStyle;
    final BoxDecoration defaultDecoration;
461
    if (theme.brightness == Brightness.dark) {
462
      defaultTextStyle = theme.textTheme.bodyText2!.copyWith(
463
        color: Colors.black,
464
        fontSize: _getDefaultFontSize(),
465 466 467 468 469 470
      );
      defaultDecoration = BoxDecoration(
        color: Colors.white.withOpacity(0.9),
        borderRadius: const BorderRadius.all(Radius.circular(4)),
      );
    } else {
471
      defaultTextStyle = theme.textTheme.bodyText2!.copyWith(
472
        color: Colors.white,
473
        fontSize: _getDefaultFontSize(),
474 475
      );
      defaultDecoration = BoxDecoration(
476
        color: Colors.grey[700]!.withOpacity(0.9),
477 478 479 480
        borderRadius: const BorderRadius.all(Radius.circular(4)),
      );
    }

481 482
    height = widget.height ?? tooltipTheme.height ?? _getDefaultTooltipHeight();
    padding = widget.padding ?? tooltipTheme.padding ?? _getDefaultPadding();
483
    margin = widget.margin ?? tooltipTheme.margin ?? _defaultMargin;
484 485 486 487 488 489 490 491
    verticalOffset = widget.verticalOffset ?? tooltipTheme.verticalOffset ?? _defaultVerticalOffset;
    preferBelow = widget.preferBelow ?? tooltipTheme.preferBelow ?? _defaultPreferBelow;
    excludeFromSemantics = widget.excludeFromSemantics ?? tooltipTheme.excludeFromSemantics ?? _defaultExcludeFromSemantics;
    decoration = widget.decoration ?? tooltipTheme.decoration ?? defaultDecoration;
    textStyle = widget.textStyle ?? tooltipTheme.textStyle ?? defaultTextStyle;
    waitDuration = widget.waitDuration ?? tooltipTheme.waitDuration ?? _defaultWaitDuration;
    showDuration = widget.showDuration ?? tooltipTheme.showDuration ?? _defaultShowDuration;

492
    Widget result = GestureDetector(
493 494 495 496
      behavior: HitTestBehavior.opaque,
      onLongPress: _handleLongPress,
      excludeFromSemantics: true,
      child: Semantics(
497
        label: excludeFromSemantics ? null : widget.message,
498
        child: widget.child,
499
      ),
Hixie's avatar
Hixie committed
500
    );
501 502 503

    // Only check for hovering if there is a mouse connected.
    if (_mouseIsConnected) {
504 505 506
      result = MouseRegion(
        onEnter: (PointerEnterEvent event) => _showTooltip(),
        onExit: (PointerExitEvent event) => _hideTooltip(),
507 508 509 510 511
        child: result,
      );
    }

    return result;
Hixie's avatar
Hixie committed
512 513 514
  }
}

515 516
/// A delegate for computing the layout of a tooltip to be displayed above or
/// bellow a target specified in the global coordinate system.
517
class _TooltipPositionDelegate extends SingleChildLayoutDelegate {
518 519 520
  /// Creates a delegate for computing the layout of a tooltip.
  ///
  /// The arguments must not be null.
Hixie's avatar
Hixie committed
521
  _TooltipPositionDelegate({
522 523 524
    required this.target,
    required this.verticalOffset,
    required this.preferBelow,
525 526 527
  }) : assert(target != null),
       assert(verticalOffset != null),
       assert(preferBelow != null);
528

529 530
  /// The offset of the target the tooltip is positioned near in the global
  /// coordinate system.
531
  final Offset target;
532 533 534

  /// The amount of vertical distance between the target and the displayed
  /// tooltip.
Hixie's avatar
Hixie committed
535
  final double verticalOffset;
536

537
  /// Whether the tooltip is displayed below its widget by default.
538 539 540
  ///
  /// If there is insufficient space to display the tooltip in the preferred
  /// direction, the tooltip will be displayed in the opposite direction.
Hixie's avatar
Hixie committed
541 542
  final bool preferBelow;

543
  @override
Hixie's avatar
Hixie committed
544 545
  BoxConstraints getConstraintsForChild(BoxConstraints constraints) => constraints.loosen();

546
  @override
Hixie's avatar
Hixie committed
547
  Offset getPositionForChild(Size size, Size childSize) {
548 549 550 551 552 553 554
    return positionDependentBox(
      size: size,
      childSize: childSize,
      target: target,
      verticalOffset: verticalOffset,
      preferBelow: preferBelow,
    );
Hixie's avatar
Hixie committed
555 556
  }

557
  @override
Hixie's avatar
Hixie committed
558
  bool shouldRelayout(_TooltipPositionDelegate oldDelegate) {
559 560 561
    return target != oldDelegate.target
        || verticalOffset != oldDelegate.verticalOffset
        || preferBelow != oldDelegate.preferBelow;
Hixie's avatar
Hixie committed
562 563 564
  }
}

565
class _TooltipOverlay extends StatelessWidget {
566
  const _TooltipOverlay({
567 568 569
    Key? key,
    required this.message,
    required this.height,
Hixie's avatar
Hixie committed
570
    this.padding,
571
    this.margin,
572
    this.decoration,
573
    this.textStyle,
574 575 576 577
    required this.animation,
    required this.target,
    required this.verticalOffset,
    required this.preferBelow,
Hixie's avatar
Hixie committed
578 579 580 581
  }) : super(key: key);

  final String message;
  final double height;
582 583 584 585
  final EdgeInsetsGeometry? padding;
  final EdgeInsetsGeometry? margin;
  final Decoration? decoration;
  final TextStyle? textStyle;
586
  final Animation<double> animation;
587
  final Offset target;
Hixie's avatar
Hixie committed
588 589 590
  final double verticalOffset;
  final bool preferBelow;

591
  @override
Hixie's avatar
Hixie committed
592
  Widget build(BuildContext context) {
593 594 595 596
    return Positioned.fill(
      child: IgnorePointer(
        child: CustomSingleChildLayout(
          delegate: _TooltipPositionDelegate(
Hixie's avatar
Hixie committed
597 598
            target: target,
            verticalOffset: verticalOffset,
599
            preferBelow: preferBelow,
Hixie's avatar
Hixie committed
600
          ),
601
          child: FadeTransition(
602
            opacity: animation,
603 604
            child: ConstrainedBox(
              constraints: BoxConstraints(minHeight: height),
605
              child: DefaultTextStyle(
606
                style: Theme.of(context).textTheme.bodyText2!,
607 608 609 610 611 612 613 614 615 616 617
                child: Container(
                  decoration: decoration,
                  padding: padding,
                  margin: margin,
                  child: Center(
                    widthFactor: 1.0,
                    heightFactor: 1.0,
                    child: Text(
                      message,
                      style: textStyle,
                    ),
618
                  ),
619 620 621 622 623 624
                ),
              ),
            ),
          ),
        ),
      ),
Hixie's avatar
Hixie committed
625 626 627
    );
  }
}