From 24cab8999c5799da203c6a97e182c12b187b7a76 Mon Sep 17 00:00:00 2001 From: Hixie Date: Wed, 13 Jan 2016 17:01:53 -0800 Subject: [PATCH] Tooltips Introduces a new Tooltip class. Adds support for tooltips to IconButton and Scaffold. Adds some tooltips to various demos. Also some tweaks to stack.dart that I made before I decided not to go down a "CustomPositioned" route. --- .../lib/demo/page_selector_demo.dart | 6 +- examples/stocks/lib/stock_home.dart | 9 +- packages/flutter/lib/material.dart | 1 + .../flutter/lib/src/material/icon_button.dart | 20 +- .../flutter/lib/src/material/scaffold.dart | 6 +- .../flutter/lib/src/material/tooltip.dart | 292 ++++++++++++++ .../lib/src/rendering/custom_layout.dart | 2 + packages/flutter/lib/src/rendering/stack.dart | 26 +- .../flutter/test/widget/tooltip_test.dart | 357 ++++++++++++++++++ 9 files changed, 707 insertions(+), 12 deletions(-) create mode 100644 packages/flutter/lib/src/material/tooltip.dart create mode 100644 packages/flutter/test/widget/tooltip_test.dart diff --git a/examples/material_gallery/lib/demo/page_selector_demo.dart b/examples/material_gallery/lib/demo/page_selector_demo.dart index 77e8bfb6064..274e9fac808 100644 --- a/examples/material_gallery/lib/demo/page_selector_demo.dart +++ b/examples/material_gallery/lib/demo/page_selector_demo.dart @@ -73,7 +73,8 @@ class TabViewDemo extends StatelessComponent { children: [ new IconButton( icon: "navigation/arrow_back", - onPressed: () { _handleArrowButtonPress(context, -1); } + onPressed: () { _handleArrowButtonPress(context, -1); }, + tooltip: 'Back' ), new Row( children: _iconNames.map((String name) => _buildTabIndicator(context, name)).toList(), @@ -81,7 +82,8 @@ class TabViewDemo extends StatelessComponent { ), new IconButton( icon: "navigation/arrow_forward", - onPressed: () { _handleArrowButtonPress(context, 1); } + onPressed: () { _handleArrowButtonPress(context, 1); }, + tooltip: 'Forward' ) ], justifyContent: FlexJustifyContent.spaceBetween diff --git a/examples/stocks/lib/stock_home.dart b/examples/stocks/lib/stock_home.dart index 0cffe9fe71e..8a643ce46ff 100644 --- a/examples/stocks/lib/stock_home.dart +++ b/examples/stocks/lib/stock_home.dart @@ -154,11 +154,13 @@ class StockHomeState extends State { right: [ new IconButton( icon: "action/search", - onPressed: _handleSearchBegin + onPressed: _handleSearchBegin, + tooltip: 'Search' ), new IconButton( icon: "navigation/more_vert", - onPressed: _handleMenuShow + onPressed: _handleMenuShow, + tooltip: 'Show menu' ) ], tabBar: new TabBar( @@ -229,7 +231,8 @@ class StockHomeState extends State { left: new IconButton( icon: 'navigation/arrow_back', colorFilter: new ColorFilter.mode(Theme.of(context).accentColor, ui.TransferMode.srcATop), - onPressed: _handleSearchEnd + onPressed: _handleSearchEnd, + tooltip: 'Back' ), center: new Input( key: searchFieldKey, diff --git a/packages/flutter/lib/material.dart b/packages/flutter/lib/material.dart index fcbd9ab5685..f18e12becf3 100644 --- a/packages/flutter/lib/material.dart +++ b/packages/flutter/lib/material.dart @@ -53,6 +53,7 @@ export 'src/material/time_picker.dart'; export 'src/material/time_picker_dialog.dart'; export 'src/material/toggleable.dart'; export 'src/material/tool_bar.dart'; +export 'src/material/tooltip.dart'; export 'src/material/typography.dart'; export 'widgets.dart'; diff --git a/packages/flutter/lib/src/material/icon_button.dart b/packages/flutter/lib/src/material/icon_button.dart index e7b1c0be6f9..cb28f331ea8 100644 --- a/packages/flutter/lib/src/material/icon_button.dart +++ b/packages/flutter/lib/src/material/icon_button.dart @@ -7,6 +7,7 @@ import 'package:flutter/widgets.dart'; import 'icon.dart'; import 'icon_theme_data.dart'; import 'ink_well.dart'; +import 'tooltip.dart'; class IconButton extends StatelessComponent { const IconButton({ @@ -14,16 +15,18 @@ class IconButton extends StatelessComponent { this.icon, this.color, this.colorFilter, - this.onPressed + this.onPressed, + this.tooltip }) : super(key: key); final String icon; final IconThemeColor color; final ColorFilter colorFilter; final VoidCallback onPressed; + final String tooltip; Widget build(BuildContext context) { - return new InkResponse( + Widget result = new InkResponse( onTap: onPressed, child: new Padding( padding: const EdgeDims.all(8.0), @@ -34,10 +37,23 @@ class IconButton extends StatelessComponent { ) ) ); + if (tooltip != null) { + result = new Tooltip( + message: tooltip, + child: result + ); + } + return result; } void debugFillDescription(List description) { super.debugFillDescription(description); description.add('$icon'); + if (onPressed == null) + description.add('disabled'); + if (color != null) + description.add('$color'); + if (tooltip != null) + description.add('tooltip: "$tooltip"'); } } diff --git a/packages/flutter/lib/src/material/scaffold.dart b/packages/flutter/lib/src/material/scaffold.dart index abd78119453..0f9b9559e12 100644 --- a/packages/flutter/lib/src/material/scaffold.dart +++ b/packages/flutter/lib/src/material/scaffold.dart @@ -331,14 +331,16 @@ class ScaffoldState extends State { if (config.drawer != null) { left = new IconButton( icon: 'navigation/menu', - onPressed: openDrawer + onPressed: openDrawer, + tooltip: 'Open navigation menu' // TODO(ianh): Figure out how to localize this string ); } else { _shouldShowBackArrow ??= Navigator.canPop(context); if (_shouldShowBackArrow) { left = new IconButton( icon: 'navigation/arrow_back', - onPressed: () => Navigator.pop(context) + onPressed: () => Navigator.pop(context), + tooltip: 'Back' // TODO(ianh): Figure out how to localize this string ); } } diff --git a/packages/flutter/lib/src/material/tooltip.dart b/packages/flutter/lib/src/material/tooltip.dart new file mode 100644 index 00000000000..0a52c5b5eef --- /dev/null +++ b/packages/flutter/lib/src/material/tooltip.dart @@ -0,0 +1,292 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:math' as math; + +import 'package:flutter/animation.dart'; +import 'package:flutter/painting.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; + +import 'colors.dart'; +import 'theme.dart'; + +const double _kDefaultTooltipBorderRadius = 2.0; +const double _kDefaultTooltipHeight = 32.0; +const EdgeDims _kDefaultTooltipPadding = const EdgeDims.symmetric(horizontal: 16.0); +const double _kDefaultVerticalTooltipOffset = 24.0; +const EdgeDims _kDefaultTooltipScreenEdgeMargin = const EdgeDims.all(10.0); +const Duration _kDefaultTooltipFadeDuration = const Duration(milliseconds: 200); +const Duration _kDefaultTooltipShowDuration = const Duration(seconds: 2); + +class Tooltip extends StatefulComponent { + Tooltip({ + Key key, + this.message, + this.backgroundColor, + this.textColor, + this.style, + this.opacity: 0.9, + this.borderRadius: _kDefaultTooltipBorderRadius, + this.height: _kDefaultTooltipHeight, + this.padding: _kDefaultTooltipPadding, + this.verticalOffset: _kDefaultVerticalTooltipOffset, + this.screenEdgeMargin: _kDefaultTooltipScreenEdgeMargin, + this.preferBelow: true, + this.fadeDuration: _kDefaultTooltipFadeDuration, + this.showDuration: _kDefaultTooltipShowDuration, + this.child + }) : super(key: key) { + assert(message != null); + assert(opacity != null); + assert(borderRadius != null); + assert(height != null); + assert(padding != null); + assert(verticalOffset != null); + assert(screenEdgeMargin != null); + assert(preferBelow != null); + assert(fadeDuration != null); + assert(showDuration != null); + } + + final String message; + final Color backgroundColor; + final Color textColor; + final TextStyle style; + final double opacity; + final double borderRadius; + final double height; + final EdgeDims padding; + final double verticalOffset; + final EdgeDims screenEdgeMargin; + final bool preferBelow; + final Duration fadeDuration; + final Duration showDuration; + final Widget child; + + _TooltipState createState() => new _TooltipState(); +} + +class _TooltipState extends State { + + Performance _performance; + OverlayEntry _entry; + Timer _timer; + + void initState() { + super.initState(); + _performance = new Performance(duration: config.fadeDuration) + ..addStatusListener((PerformanceStatus status) { + switch (status) { + case PerformanceStatus.completed: + assert(_entry != null); + assert(_timer == null); + resetShowTimer(); + break; + case PerformanceStatus.dismissed: + assert(_entry != null); + assert(_timer == null); + _entry.remove(); + _entry = null; + break; + default: + break; + } + }); + } + + void didUpdateConfig(Tooltip oldConfig) { + super.didUpdateConfig(oldConfig); + if (config.fadeDuration != oldConfig.fadeDuration) + _performance.duration = config.fadeDuration; + if (_entry != null && + (config.message != oldConfig.message || + config.backgroundColor != oldConfig.backgroundColor || + config.style != oldConfig.style || + config.textColor != oldConfig.textColor || + config.borderRadius != oldConfig.borderRadius || + config.height != oldConfig.height || + config.padding != oldConfig.padding || + config.opacity != oldConfig.opacity || + config.verticalOffset != oldConfig.verticalOffset || + config.screenEdgeMargin != oldConfig.screenEdgeMargin || + config.preferBelow != oldConfig.preferBelow)) + _entry.markNeedsBuild(); + } + + void resetShowTimer() { + assert(_performance.status == PerformanceStatus.completed); + assert(_entry != null); + _timer = new Timer(config.showDuration, hideTooltip); + } + + void showTooltip() { + if (_entry == null) { + RenderBox box = context.findRenderObject(); + Point target = box.localToGlobal(box.size.center(Point.origin)); + _entry = new OverlayEntry(builder: (BuildContext context) { + TextStyle textStyle = (config.style ?? Theme.of(context).text.body1).copyWith(color: config.textColor ?? Colors.white); + return new _TooltipOverlay( + message: config.message, + backgroundColor: config.backgroundColor ?? Colors.grey[700], + style: textStyle, + borderRadius: config.borderRadius, + height: config.height, + padding: config.padding, + opacity: config.opacity, + performance: _performance, + target: target, + verticalOffset: config.verticalOffset, + screenEdgeMargin: config.screenEdgeMargin, + preferBelow: config.preferBelow + ); + }); + Overlay.of(context).insert(_entry); + } + _timer?.cancel(); + if (_performance.status != PerformanceStatus.completed) { + _timer = null; + _performance.forward(); + } else { + resetShowTimer(); + } + } + + void hideTooltip() { + assert(_entry != null); + _timer?.cancel(); + _timer = null; + _performance.reverse(); + } + + void deactivate() { + if (_entry != null) + hideTooltip(); + super.deactivate(); + } + + Widget build(BuildContext context) { + assert(Overlay.of(context) != null); + return new GestureDetector( + behavior: HitTestBehavior.opaque, + onLongPress: showTooltip, + child: config.child + ); + } +} + +class _TooltipPositionDelegate extends OneChildLayoutDelegate { + _TooltipPositionDelegate({ + this.target, + this.verticalOffset, + this.screenEdgeMargin, + this.preferBelow + }); + final Point target; + final double verticalOffset; + final EdgeDims screenEdgeMargin; + final bool preferBelow; + + BoxConstraints getConstraintsForChild(BoxConstraints constraints) => constraints.loosen(); + + Offset getPositionForChild(Size size, Size childSize) { + // VERTICAL DIRECTION + final bool fitsBelow = target.y + verticalOffset + childSize.height <= size.height - screenEdgeMargin.bottom; + final bool fitsAbove = target.y - verticalOffset - childSize.height >= screenEdgeMargin.top; + final bool tooltipBelow = preferBelow ? fitsBelow || !fitsAbove : !(fitsAbove || !fitsBelow); + double y; + if (tooltipBelow) + y = math.min(target.y + verticalOffset, size.height - screenEdgeMargin.bottom); + else + y = math.max(target.y - verticalOffset - childSize.height, screenEdgeMargin.top); + // HORIZONTAL DIRECTION + double normalizedTargetX = target.x.clamp(screenEdgeMargin.left, size.width - screenEdgeMargin.right); + double x; + if (normalizedTargetX < screenEdgeMargin.left + childSize.width / 2.0) { + x = screenEdgeMargin.left; + } else if (normalizedTargetX > size.width - screenEdgeMargin.right - childSize.width / 2.0) { + x = size.width - screenEdgeMargin.right - childSize.width; + } else { + x = normalizedTargetX + childSize.width / 2.0; + } + return new Offset(x, y); + } + + bool shouldRelayout(_TooltipPositionDelegate oldDelegate) { + return target != target + || verticalOffset != verticalOffset + || screenEdgeMargin != screenEdgeMargin + || preferBelow != preferBelow; + } +} + +class _TooltipOverlay extends StatelessComponent { + _TooltipOverlay({ + Key key, + this.message, + this.backgroundColor, + this.style, + this.borderRadius, + this.height, + this.padding, + this.opacity, + this.performance, + this.target, + this.verticalOffset, + this.screenEdgeMargin, + this.preferBelow + }) : super(key: key); + + final String message; + final Color backgroundColor; + final TextStyle style; + final double opacity; + final double borderRadius; + final double height; + final EdgeDims padding; + final PerformanceView performance; + final Point target; + final double verticalOffset; + final EdgeDims screenEdgeMargin; + final bool preferBelow; + + Widget build(BuildContext context) { + return new Positioned( + top: 0.0, + left: 0.0, + right: 0.0, + bottom: 0.0, + child: new IgnorePointer( + child: new CustomOneChildLayout( + delegate: new _TooltipPositionDelegate( + target: target, + verticalOffset: verticalOffset, + screenEdgeMargin: screenEdgeMargin, + preferBelow: preferBelow + ), + child: new FadeTransition( + performance: performance, + opacity: new AnimatedValue(0.0, end: 1.0, curve: Curves.ease), + child: new Opacity( + opacity: opacity, + child: new Container( + decoration: new BoxDecoration( + backgroundColor: backgroundColor, + borderRadius: borderRadius + ), + height: height, + padding: padding, + child: new Center( + widthFactor: 1.0, + child: new Text(message, style: style) + ) + ) + ) + ) + ) + ) + ); + } +} diff --git a/packages/flutter/lib/src/rendering/custom_layout.dart b/packages/flutter/lib/src/rendering/custom_layout.dart index f4be21ebcd1..abcaea39f8f 100644 --- a/packages/flutter/lib/src/rendering/custom_layout.dart +++ b/packages/flutter/lib/src/rendering/custom_layout.dart @@ -5,6 +5,8 @@ import 'box.dart'; import 'object.dart'; +// For OneChildLayoutDelegate and RenderCustomOneChildLayoutBox, see shifted_box.dart + class MultiChildLayoutParentData extends ContainerBoxParentDataMixin { /// An object representing the identity of this child. Object id; diff --git a/packages/flutter/lib/src/rendering/stack.dart b/packages/flutter/lib/src/rendering/stack.dart index ff691ff72ee..be1204eb609 100644 --- a/packages/flutter/lib/src/rendering/stack.dart +++ b/packages/flutter/lib/src/rendering/stack.dart @@ -14,6 +14,9 @@ import 'object.dart'; /// container, this class has no width and height members. To determine the /// width or height of the rectangle, convert it to a [Rect] using [toRect()] /// (passing the container's own Rect), and then examine that object. +/// +/// If you create the RelativeRect with null values, the methods on +/// RelativeRect will not work usefully (or at all). class RelativeRect { /// Creates a RelativeRect with the given values. @@ -125,7 +128,7 @@ class RelativeRect { int get hashCode => hashValues(left, top, right, bottom); - String toString() => "RelativeRect.fromLTRB(${left.toStringAsFixed(1)}, ${top.toStringAsFixed(1)}, ${right.toStringAsFixed(1)}, ${bottom.toStringAsFixed(1)})"; + String toString() => "RelativeRect.fromLTRB(${left?.toStringAsFixed(1)}, ${top?.toStringAsFixed(1)}, ${right?.toStringAsFixed(1)}, ${bottom?.toStringAsFixed(1)})"; } /// Parent data for use with [RenderStack] @@ -155,10 +158,10 @@ class StackParentData extends ContainerBoxParentDataMixin { /// Get or set the current values in terms of a RelativeRect object. RelativeRect get rect => new RelativeRect.fromLTRB(left, top, right, bottom); void set rect(RelativeRect value) { - left = value.left; top = value.top; right = value.right; bottom = value.bottom; + left = value.left; } void merge(StackParentData other) { @@ -185,7 +188,24 @@ class StackParentData extends ContainerBoxParentDataMixin { /// children in the stack. bool get isPositioned => top != null || right != null || bottom != null || left != null || width != null || height != null; - String toString() => '${super.toString()}; top=$top; right=$right; bottom=$bottom; left=$left; width=$width; height=$height'; + String toString() { + List values = []; + if (top != null) + values.add('top=$top'); + if (right != null) + values.add('right=$right'); + if (bottom != null) + values.add('bottom=$bottom'); + if (left != null) + values.add('left=$left'); + if (width != null) + values.add('width=$width'); + if (height != null) + values.add('height=$height'); + if (values.length == null) + return 'all null'; + return values.join('; '); + } } abstract class RenderStackBase extends RenderBox diff --git a/packages/flutter/test/widget/tooltip_test.dart b/packages/flutter/test/widget/tooltip_test.dart new file mode 100644 index 00000000000..f9539073f43 --- /dev/null +++ b/packages/flutter/test/widget/tooltip_test.dart @@ -0,0 +1,357 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:test/test.dart'; + +void main() { + test('Does tooltip end up in the right place - top left', () { + testWidgets((WidgetTester tester) { + GlobalKey key = new GlobalKey(); + tester.pumpWidget( + new Overlay( + initialEntries: [ + new OverlayEntry( + builder: (BuildContext context) { + return new Stack( + children: [ + new Positioned( + left: 0.0, + top: 0.0, + child: new Tooltip( + key: key, + message: 'TIP', + height: 20.0, + padding: const EdgeDims.all(5.0), + verticalOffset: 20.0, + screenEdgeMargin: const EdgeDims.all(10.0), + preferBelow: false, + fadeDuration: const Duration(seconds: 1), + showDuration: const Duration(seconds: 2), + child: new Container( + width: 0.0, + height: 0.0 + ) + ) + ), + ] + ); + } + ), + ] + ) + ); + key.currentState.showTooltip(); + tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0) + + /********************* 800x600 screen + *o * y=0 + *| * }- 20.0 vertical offset, of which 10.0 is in the screen edge margin + *+----+ * \- (5.0 padding in height) + *| | * |- 20 height + *+----+ * /- (5.0 padding in height) + * * + *********************/ + + RenderBox tip = tester.findText('TIP').renderObject.parent.parent.parent.parent.parent; + expect(tip.size.height, equals(20.0)); // 10.0 height + 5.0 padding * 2 (top, bottom) + expect(tip.localToGlobal(tip.size.topLeft(Point.origin)), equals(const Point(10.0, 20.0))); + }); + }); + + test('Does tooltip end up in the right place - center prefer above fits', () { + testWidgets((WidgetTester tester) { + GlobalKey key = new GlobalKey(); + tester.pumpWidget( + new Overlay( + initialEntries: [ + new OverlayEntry( + builder: (BuildContext context) { + return new Stack( + children: [ + new Positioned( + left: 400.0, + top: 300.0, + child: new Tooltip( + key: key, + message: 'TIP', + height: 100.0, + padding: const EdgeDims.all(0.0), + verticalOffset: 100.0, + screenEdgeMargin: const EdgeDims.all(100.0), + preferBelow: false, + fadeDuration: const Duration(seconds: 1), + showDuration: const Duration(seconds: 2), + child: new Container( + width: 0.0, + height: 0.0 + ) + ) + ), + ] + ); + } + ), + ] + ) + ); + key.currentState.showTooltip(); + tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0) + + /********************* 800x600 screen + * ___ * }-100.0 margin + * |___| * }-100.0 height + * | * }-100.0 vertical offset + * o * y=300.0 + * * + * * + * * + *********************/ + + RenderBox tip = tester.findText('TIP').renderObject.parent; + expect(tip.size.height, equals(100.0)); + expect(tip.localToGlobal(tip.size.topLeft(Point.origin)).y, equals(100.0)); + expect(tip.localToGlobal(tip.size.bottomRight(Point.origin)).y, equals(200.0)); + }); + }); + + test('Does tooltip end up in the right place - center prefer above does not fit', () { + testWidgets((WidgetTester tester) { + GlobalKey key = new GlobalKey(); + tester.pumpWidget( + new Overlay( + initialEntries: [ + new OverlayEntry( + builder: (BuildContext context) { + return new Stack( + children: [ + new Positioned( + left: 400.0, + top: 299.0, + child: new Tooltip( + key: key, + message: 'TIP', + height: 100.0, + padding: const EdgeDims.all(0.0), + verticalOffset: 100.0, + screenEdgeMargin: const EdgeDims.all(100.0), + preferBelow: false, + fadeDuration: const Duration(seconds: 1), + showDuration: const Duration(seconds: 2), + child: new Container( + width: 0.0, + height: 0.0 + ) + ) + ), + ] + ); + } + ), + ] + ) + ); + key.currentState.showTooltip(); + tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0) + + // we try to put it here but it doesn't fit: + /********************* 800x600 screen + * ___ * }-100.0 margin + * |___| * }-100.0 height (starts at y=99.0) + * | * }-100.0 vertical offset + * o * y=299.0 + * * + * * + * * + *********************/ + + // so we put it here: + /********************* 800x600 screen + * * + * * + * o * y=299.0 + * _|_ * }-100.0 vertical offset + * |___| * }-100.0 height + * * }-100.0 margin + *********************/ + + RenderBox tip = tester.findText('TIP').renderObject.parent; + expect(tip.size.height, equals(100.0)); + expect(tip.localToGlobal(tip.size.topLeft(Point.origin)).y, equals(399.0)); + expect(tip.localToGlobal(tip.size.bottomRight(Point.origin)).y, equals(499.0)); + }); + }); + + test('Does tooltip end up in the right place - center prefer below fits', () { + testWidgets((WidgetTester tester) { + GlobalKey key = new GlobalKey(); + tester.pumpWidget( + new Overlay( + initialEntries: [ + new OverlayEntry( + builder: (BuildContext context) { + return new Stack( + children: [ + new Positioned( + left: 400.0, + top: 300.0, + child: new Tooltip( + key: key, + message: 'TIP', + height: 100.0, + padding: const EdgeDims.all(0.0), + verticalOffset: 100.0, + screenEdgeMargin: const EdgeDims.all(100.0), + preferBelow: true, + fadeDuration: const Duration(seconds: 1), + showDuration: const Duration(seconds: 2), + child: new Container( + width: 0.0, + height: 0.0 + ) + ) + ), + ] + ); + } + ), + ] + ) + ); + key.currentState.showTooltip(); + tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0) + + /********************* 800x600 screen + * * + * * + * o * y=300.0 + * _|_ * }-100.0 vertical offset + * |___| * }-100.0 height + * * }-100.0 margin + *********************/ + + RenderBox tip = tester.findText('TIP').renderObject.parent; + expect(tip.size.height, equals(100.0)); + expect(tip.localToGlobal(tip.size.topLeft(Point.origin)).y, equals(400.0)); + expect(tip.localToGlobal(tip.size.bottomRight(Point.origin)).y, equals(500.0)); + }); + }); + + test('Does tooltip end up in the right place - way off to the right', () { + testWidgets((WidgetTester tester) { + GlobalKey key = new GlobalKey(); + tester.pumpWidget( + new Overlay( + initialEntries: [ + new OverlayEntry( + builder: (BuildContext context) { + return new Stack( + children: [ + new Positioned( + left: 1600.0, + top: 300.0, + child: new Tooltip( + key: key, + message: 'TIP', + height: 10.0, + padding: const EdgeDims.all(0.0), + verticalOffset: 10.0, + screenEdgeMargin: const EdgeDims.all(10.0), + preferBelow: true, + fadeDuration: const Duration(seconds: 1), + showDuration: const Duration(seconds: 2), + child: new Container( + width: 0.0, + height: 0.0 + ) + ) + ), + ] + ); + } + ), + ] + ) + ); + key.currentState.showTooltip(); + tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0) + + /********************* 800x600 screen + * * + * * + * * y=300.0; target --> o + * ___| * }-10.0 vertical offset + * |___| * }-10.0 height + * * + * * }-10.0 margin + *********************/ + + RenderBox tip = tester.findText('TIP').renderObject.parent; + expect(tip.size.height, equals(10.0)); + expect(tip.localToGlobal(tip.size.topLeft(Point.origin)).y, equals(310.0)); + expect(tip.localToGlobal(tip.size.bottomRight(Point.origin)).x, equals(790.0)); + expect(tip.localToGlobal(tip.size.bottomRight(Point.origin)).y, equals(320.0)); + }); + }); + + test('Does tooltip end up in the right place - near the edge', () { + testWidgets((WidgetTester tester) { + GlobalKey key = new GlobalKey(); + tester.pumpWidget( + new Overlay( + initialEntries: [ + new OverlayEntry( + builder: (BuildContext context) { + return new Stack( + children: [ + new Positioned( + left: 780.0, + top: 300.0, + child: new Tooltip( + key: key, + message: 'TIP', + height: 10.0, + padding: const EdgeDims.all(0.0), + verticalOffset: 10.0, + screenEdgeMargin: const EdgeDims.all(10.0), + preferBelow: true, + fadeDuration: const Duration(seconds: 1), + showDuration: const Duration(seconds: 2), + child: new Container( + width: 0.0, + height: 0.0 + ) + ) + ), + ] + ); + } + ), + ] + ) + ); + key.currentState.showTooltip(); + tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0) + + /********************* 800x600 screen + * * + * * + * o * y=300.0 + * __| * }-10.0 vertical offset + * |___| * }-10.0 height + * * + * * }-10.0 margin + *********************/ + + RenderBox tip = tester.findText('TIP').renderObject.parent; + expect(tip.size.height, equals(10.0)); + expect(tip.localToGlobal(tip.size.topLeft(Point.origin)).y, equals(310.0)); + expect(tip.localToGlobal(tip.size.bottomRight(Point.origin)).x, equals(790.0)); + expect(tip.localToGlobal(tip.size.bottomRight(Point.origin)).y, equals(320.0)); + }); + }); +}