Unverified Commit e1fdb1aa authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

migrate `Tooltip` to use `OverlayPortal` (#127728)

https://github.com/flutter/flutter/issues/7151 isn't a problem with OverlayPortal so the test is removed.
Also removed some `mounted` checks since they're no longer needed.
parent 9e8143a0
...@@ -12,6 +12,7 @@ import 'package:flutter/widgets.dart'; ...@@ -12,6 +12,7 @@ import 'package:flutter/widgets.dart';
import 'colors.dart'; import 'colors.dart';
import 'feedback.dart'; import 'feedback.dart';
import 'text_theme.dart';
import 'theme.dart'; import 'theme.dart';
import 'tooltip_theme.dart'; import 'tooltip_theme.dart';
import 'tooltip_visibility.dart'; import 'tooltip_visibility.dart';
...@@ -388,23 +389,17 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin { ...@@ -388,23 +389,17 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
static const bool _defaultEnableFeedback = true; static const bool _defaultEnableFeedback = true;
static const TextAlign _defaultTextAlign = TextAlign.start; static const TextAlign _defaultTextAlign = TextAlign.start;
late double _height; final OverlayPortalController _overlayController = OverlayPortalController();
late EdgeInsetsGeometry _padding;
late EdgeInsetsGeometry _margin; // From InheritedWidgets
late Decoration _decoration;
late TextStyle _textStyle;
late TextAlign _textAlign;
late double _verticalOffset;
late bool _preferBelow;
late bool _excludeFromSemantics;
OverlayEntry? _entry;
late Duration _showDuration;
late Duration _hoverShowDuration;
late Duration _waitDuration;
late TooltipTriggerMode _triggerMode;
late bool _enableFeedback;
late bool _visible; late bool _visible;
late TooltipThemeData _tooltipTheme;
Duration get _showDuration => widget.showDuration ?? _tooltipTheme.showDuration ?? _defaultShowDuration;
Duration get _hoverShowDuration => widget.showDuration ?? _tooltipTheme.showDuration ?? _defaultHoverShowDuration;
Duration get _waitDuration => widget.waitDuration ?? _tooltipTheme.waitDuration ?? _defaultWaitDuration;
TooltipTriggerMode get _triggerMode => widget.triggerMode ?? _tooltipTheme.triggerMode ?? _defaultTriggerMode;
bool get _enableFeedback => widget.enableFeedback ?? _tooltipTheme.enableFeedback ?? _defaultEnableFeedback;
/// The plain text message for this tooltip. /// The plain text message for this tooltip.
/// ///
...@@ -438,14 +433,16 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin { ...@@ -438,14 +433,16 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
case AnimationStatus.dismissed: case AnimationStatus.dismissed:
entryNeedsUpdating = _animationStatus != AnimationStatus.dismissed; entryNeedsUpdating = _animationStatus != AnimationStatus.dismissed;
if (entryNeedsUpdating) { if (entryNeedsUpdating) {
_removeEntry(); Tooltip._openedTooltips.remove(this);
_overlayController.hide();
} }
case AnimationStatus.completed: case AnimationStatus.completed:
case AnimationStatus.forward: case AnimationStatus.forward:
case AnimationStatus.reverse: case AnimationStatus.reverse:
entryNeedsUpdating = _animationStatus == AnimationStatus.dismissed; entryNeedsUpdating = _animationStatus == AnimationStatus.dismissed;
if (entryNeedsUpdating) { if (entryNeedsUpdating) {
_createNewEntry(); _overlayController.show();
Tooltip._openedTooltips.add(this);
SemanticsService.tooltip(_tooltipMessage); SemanticsService.tooltip(_tooltipMessage);
} }
} }
...@@ -620,11 +617,6 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin { ...@@ -620,11 +617,6 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
// (even these tooltips are still hovered), // (even these tooltips are still hovered),
// iii. The last hovering device leaves the tooltip. // iii. The last hovering device leaves the tooltip.
void _handleMouseEnter(PointerEnterEvent event) { void _handleMouseEnter(PointerEnterEvent event) {
// The callback is also used in an OverlayEntry, so there's a chance that
// this widget is already unmounted.
if (!mounted) {
return;
}
// _handleMouseEnter is only called when the mouse starts to hover over this // _handleMouseEnter is only called when the mouse starts to hover over this
// tooltip (including the actual tooltip it shows on the overlay), and this // tooltip (including the actual tooltip it shows on the overlay), and this
// tooltip is the first to be hit in the widget tree's hit testing order. // tooltip is the first to be hit in the widget tree's hit testing order.
...@@ -646,7 +638,7 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin { ...@@ -646,7 +638,7 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
} }
void _handleMouseExit(PointerExitEvent event) { void _handleMouseExit(PointerExitEvent event) {
if (!mounted || _activeHoveringPointerDevices.isEmpty) { if (_activeHoveringPointerDevices.isEmpty) {
return; return;
} }
_activeHoveringPointerDevices.remove(event.device); _activeHoveringPointerDevices.remove(event.device);
...@@ -694,6 +686,7 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin { ...@@ -694,6 +686,7 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
_visible = TooltipVisibility.of(context); _visible = TooltipVisibility.of(context);
_tooltipTheme = TooltipTheme.of(context);
} }
// https://material.io/components/tooltips#specs // https://material.io/components/tooltips#specs
...@@ -719,8 +712,8 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin { ...@@ -719,8 +712,8 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
}; };
} }
double _getDefaultFontSize() { static double _getDefaultFontSize(TargetPlatform platform) {
return switch (Theme.of(context).platform) { return switch (platform) {
TargetPlatform.macOS || TargetPlatform.macOS ||
TargetPlatform.linux || TargetPlatform.linux ||
TargetPlatform.windows => 12.0, TargetPlatform.windows => 12.0,
...@@ -730,58 +723,50 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin { ...@@ -730,58 +723,50 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
}; };
} }
void _createNewEntry() { Widget _buildTooltipOverlay(BuildContext context) {
final OverlayState overlayState = Overlay.of( final OverlayState overlayState = Overlay.of(context, debugRequiredFor: widget);
context, final RenderBox box = this.context.findRenderObject()! as RenderBox;
debugRequiredFor: widget,
);
final RenderBox box = context.findRenderObject()! as RenderBox;
final Offset target = box.localToGlobal( final Offset target = box.localToGlobal(
box.size.center(Offset.zero), box.size.center(Offset.zero),
ancestor: overlayState.context.findRenderObject(), ancestor: overlayState.context.findRenderObject(),
); );
// We create this widget outside of the overlay entry's builder to prevent final (TextStyle defaultTextStyle, BoxDecoration defaultDecoration) = switch (Theme.of(context)) {
// updated values from happening to leak into the overlay when the overlay ThemeData(brightness: Brightness.dark, :final TextTheme textTheme, :final TargetPlatform platform) => (
// rebuilds. textTheme.bodyMedium!.copyWith(color: Colors.black, fontSize: _getDefaultFontSize(platform)),
final Widget overlay = Directionality( BoxDecoration(color: Colors.white.withOpacity(0.9), borderRadius: const BorderRadius.all(Radius.circular(4))),
textDirection: Directionality.of(context), ),
child: _TooltipOverlay( ThemeData(brightness: Brightness.light, :final TextTheme textTheme, :final TargetPlatform platform) => (
textTheme.bodyMedium!.copyWith(color: Colors.white, fontSize: _getDefaultFontSize(platform)),
BoxDecoration(color: Colors.grey[700]!.withOpacity(0.9), borderRadius: const BorderRadius.all(Radius.circular(4))),
),
};
final TooltipThemeData tooltipTheme = _tooltipTheme;
return _TooltipOverlay(
richMessage: widget.richMessage ?? TextSpan(text: widget.message), richMessage: widget.richMessage ?? TextSpan(text: widget.message),
height: _height, height: widget.height ?? tooltipTheme.height ?? _getDefaultTooltipHeight(),
padding: _padding, padding: widget.padding ?? tooltipTheme.padding ?? _getDefaultPadding(),
margin: _margin, margin: widget.margin ?? tooltipTheme.margin ?? _defaultMargin,
onEnter: _handleMouseEnter, onEnter: _handleMouseEnter,
onExit: _handleMouseExit, onExit: _handleMouseExit,
decoration: _decoration, decoration: widget.decoration ?? tooltipTheme.decoration ?? defaultDecoration,
textStyle: _textStyle, textStyle: widget.textStyle ?? tooltipTheme.textStyle ?? defaultTextStyle,
textAlign: _textAlign, textAlign: widget.textAlign ?? tooltipTheme.textAlign ?? _defaultTextAlign,
animation: CurvedAnimation( animation: CurvedAnimation(
parent: _controller, parent: _controller,
curve: Curves.fastOutSlowIn, curve: Curves.fastOutSlowIn,
), ),
target: target, target: target,
verticalOffset: _verticalOffset, verticalOffset: widget.verticalOffset ?? tooltipTheme.verticalOffset ?? _defaultVerticalOffset,
preferBelow: _preferBelow, preferBelow: widget.preferBelow ?? tooltipTheme.preferBelow ?? _defaultPreferBelow,
),
); );
final OverlayEntry entry = _entry = OverlayEntry(builder: (BuildContext context) => overlay);
overlayState.insert(entry);
Tooltip._openedTooltips.add(this);
}
void _removeEntry() {
Tooltip._openedTooltips.remove(this);
_entry?.remove();
_entry?.dispose();
_entry = null;
} }
@override @override
void dispose() { void dispose() {
GestureBinding.instance.pointerRouter.removeGlobalRoute(_handleGlobalPointerEvent); GestureBinding.instance.pointerRouter.removeGlobalRoute(_handleGlobalPointerEvent);
_removeEntry(); Tooltip._openedTooltips.remove(this);
_longPressRecognizer?.dispose(); _longPressRecognizer?.dispose();
_tapRecognizer?.dispose(); _tapRecognizer?.dispose();
_timer?.cancel(); _timer?.cancel();
...@@ -798,47 +783,9 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin { ...@@ -798,47 +783,9 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
return widget.child ?? const SizedBox.shrink(); return widget.child ?? const SizedBox.shrink();
} }
assert(debugCheckHasOverlay(context)); assert(debugCheckHasOverlay(context));
final ThemeData theme = Theme.of(context); final bool excludeFromSemantics = widget.excludeFromSemantics ?? _tooltipTheme.excludeFromSemantics ?? _defaultExcludeFromSemantics;
final TooltipThemeData tooltipTheme = TooltipTheme.of(context);
final TextStyle defaultTextStyle;
final BoxDecoration defaultDecoration;
if (theme.brightness == Brightness.dark) {
defaultTextStyle = theme.textTheme.bodyMedium!.copyWith(
color: Colors.black,
fontSize: _getDefaultFontSize(),
);
defaultDecoration = BoxDecoration(
color: Colors.white.withOpacity(0.9),
borderRadius: const BorderRadius.all(Radius.circular(4)),
);
} else {
defaultTextStyle = theme.textTheme.bodyMedium!.copyWith(
color: Colors.white,
fontSize: _getDefaultFontSize(),
);
defaultDecoration = BoxDecoration(
color: Colors.grey[700]!.withOpacity(0.9),
borderRadius: const BorderRadius.all(Radius.circular(4)),
);
}
_height = widget.height ?? tooltipTheme.height ?? _getDefaultTooltipHeight();
_padding = widget.padding ?? tooltipTheme.padding ?? _getDefaultPadding();
_margin = widget.margin ?? tooltipTheme.margin ?? _defaultMargin;
_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;
_textAlign = widget.textAlign ?? tooltipTheme.textAlign ?? _defaultTextAlign;
_waitDuration = widget.waitDuration ?? tooltipTheme.waitDuration ?? _defaultWaitDuration;
_showDuration = widget.showDuration ?? tooltipTheme.showDuration ?? _defaultShowDuration;
_hoverShowDuration = widget.showDuration ?? tooltipTheme.showDuration ?? _defaultHoverShowDuration;
_triggerMode = widget.triggerMode ?? tooltipTheme.triggerMode ?? _defaultTriggerMode;
_enableFeedback = widget.enableFeedback ?? tooltipTheme.enableFeedback ?? _defaultEnableFeedback;
Widget result = Semantics( Widget result = Semantics(
tooltip: _excludeFromSemantics ? null : _tooltipMessage, tooltip: excludeFromSemantics ? null : _tooltipMessage,
child: widget.child, child: widget.child,
); );
...@@ -854,8 +801,11 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin { ...@@ -854,8 +801,11 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
), ),
); );
} }
return OverlayPortal(
return result; controller: _overlayController,
overlayChildBuilder: _buildTooltipOverlay,
child: result,
);
} }
} }
......
...@@ -1041,15 +1041,9 @@ void main() { ...@@ -1041,15 +1041,9 @@ void main() {
), ),
); );
// The tooltip overlay still on the tree and it will removed in the next frame. // The tooltip should be removed, including the overlay child.
expect(find.text(tooltipText), findsNothing);
// Dispatch the mouse in and out events before the overlay detached. expect(find.byTooltip(tooltipText), findsNothing);
await gesture.moveTo(tester.getCenter(find.text(tooltipText)));
await gesture.moveTo(Offset.zero);
await tester.pumpAndSettle();
// Go without crashes.
await gesture.removePointer();
}); });
testWidgetsWithLeakTracking('Calling ensureTooltipVisible on an unmounted TooltipState returns false', (WidgetTester tester) async { testWidgetsWithLeakTracking('Calling ensureTooltipVisible on an unmounted TooltipState returns false', (WidgetTester tester) async {
...@@ -1435,35 +1429,6 @@ void main() { ...@@ -1435,35 +1429,6 @@ void main() {
semantics.dispose(); semantics.dispose();
}); });
testWidgetsWithLeakTracking('Tooltip overlay does not update', (WidgetTester tester) async {
Widget buildApp(String text) {
return MaterialApp(
home: Center(
child: Tooltip(
message: text,
child: Container(
width: 100.0,
height: 100.0,
color: Colors.green[500],
),
),
),
);
}
await tester.pumpWidget(buildApp(tooltipText));
await tester.longPress(find.byType(Tooltip));
expect(find.text(tooltipText), findsOneWidget);
await tester.pumpWidget(buildApp('NEW'));
expect(find.text(tooltipText), findsOneWidget);
await tester.tapAt(const Offset(5.0, 5.0));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(find.text(tooltipText), findsNothing);
await tester.longPress(find.byType(Tooltip));
expect(find.text(tooltipText), findsNothing);
});
testWidgetsWithLeakTracking('Tooltip text scales with textScaleFactor', (WidgetTester tester) async { testWidgetsWithLeakTracking('Tooltip text scales with textScaleFactor', (WidgetTester tester) async {
Widget buildApp(String text, { required double textScaleFactor }) { Widget buildApp(String text, { required double textScaleFactor }) {
return MediaQuery( return MediaQuery(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment