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';
import 'colors.dart';
import 'feedback.dart';
import 'text_theme.dart';
import 'theme.dart';
import 'tooltip_theme.dart';
import 'tooltip_visibility.dart';
......@@ -388,23 +389,17 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
static const bool _defaultEnableFeedback = true;
static const TextAlign _defaultTextAlign = TextAlign.start;
late double _height;
late EdgeInsetsGeometry _padding;
late EdgeInsetsGeometry _margin;
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;
final OverlayPortalController _overlayController = OverlayPortalController();
// From InheritedWidgets
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.
///
......@@ -438,14 +433,16 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
case AnimationStatus.dismissed:
entryNeedsUpdating = _animationStatus != AnimationStatus.dismissed;
if (entryNeedsUpdating) {
_removeEntry();
Tooltip._openedTooltips.remove(this);
_overlayController.hide();
}
case AnimationStatus.completed:
case AnimationStatus.forward:
case AnimationStatus.reverse:
entryNeedsUpdating = _animationStatus == AnimationStatus.dismissed;
if (entryNeedsUpdating) {
_createNewEntry();
_overlayController.show();
Tooltip._openedTooltips.add(this);
SemanticsService.tooltip(_tooltipMessage);
}
}
......@@ -620,11 +617,6 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
// (even these tooltips are still hovered),
// iii. The last hovering device leaves the tooltip.
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
// 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.
......@@ -646,7 +638,7 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
}
void _handleMouseExit(PointerExitEvent event) {
if (!mounted || _activeHoveringPointerDevices.isEmpty) {
if (_activeHoveringPointerDevices.isEmpty) {
return;
}
_activeHoveringPointerDevices.remove(event.device);
......@@ -694,6 +686,7 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
void didChangeDependencies() {
super.didChangeDependencies();
_visible = TooltipVisibility.of(context);
_tooltipTheme = TooltipTheme.of(context);
}
// https://material.io/components/tooltips#specs
......@@ -719,8 +712,8 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
};
}
double _getDefaultFontSize() {
return switch (Theme.of(context).platform) {
static double _getDefaultFontSize(TargetPlatform platform) {
return switch (platform) {
TargetPlatform.macOS ||
TargetPlatform.linux ||
TargetPlatform.windows => 12.0,
......@@ -730,58 +723,50 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
};
}
void _createNewEntry() {
final OverlayState overlayState = Overlay.of(
context,
debugRequiredFor: widget,
);
final RenderBox box = context.findRenderObject()! as RenderBox;
Widget _buildTooltipOverlay(BuildContext context) {
final OverlayState overlayState = Overlay.of(context, debugRequiredFor: widget);
final RenderBox box = this.context.findRenderObject()! as RenderBox;
final Offset target = box.localToGlobal(
box.size.center(Offset.zero),
ancestor: overlayState.context.findRenderObject(),
);
// 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.
final Widget overlay = Directionality(
textDirection: Directionality.of(context),
child: _TooltipOverlay(
final (TextStyle defaultTextStyle, BoxDecoration defaultDecoration) = switch (Theme.of(context)) {
ThemeData(brightness: Brightness.dark, :final TextTheme textTheme, :final TargetPlatform platform) => (
textTheme.bodyMedium!.copyWith(color: Colors.black, fontSize: _getDefaultFontSize(platform)),
BoxDecoration(color: Colors.white.withOpacity(0.9), borderRadius: const BorderRadius.all(Radius.circular(4))),
),
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),
height: _height,
padding: _padding,
margin: _margin,
height: widget.height ?? tooltipTheme.height ?? _getDefaultTooltipHeight(),
padding: widget.padding ?? tooltipTheme.padding ?? _getDefaultPadding(),
margin: widget.margin ?? tooltipTheme.margin ?? _defaultMargin,
onEnter: _handleMouseEnter,
onExit: _handleMouseExit,
decoration: _decoration,
textStyle: _textStyle,
textAlign: _textAlign,
decoration: widget.decoration ?? tooltipTheme.decoration ?? defaultDecoration,
textStyle: widget.textStyle ?? tooltipTheme.textStyle ?? defaultTextStyle,
textAlign: widget.textAlign ?? tooltipTheme.textAlign ?? _defaultTextAlign,
animation: CurvedAnimation(
parent: _controller,
curve: Curves.fastOutSlowIn,
),
target: target,
verticalOffset: _verticalOffset,
preferBelow: _preferBelow,
),
verticalOffset: widget.verticalOffset ?? tooltipTheme.verticalOffset ?? _defaultVerticalOffset,
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
void dispose() {
GestureBinding.instance.pointerRouter.removeGlobalRoute(_handleGlobalPointerEvent);
_removeEntry();
Tooltip._openedTooltips.remove(this);
_longPressRecognizer?.dispose();
_tapRecognizer?.dispose();
_timer?.cancel();
......@@ -798,47 +783,9 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
return widget.child ?? const SizedBox.shrink();
}
assert(debugCheckHasOverlay(context));
final ThemeData theme = Theme.of(context);
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;
final bool excludeFromSemantics = widget.excludeFromSemantics ?? _tooltipTheme.excludeFromSemantics ?? _defaultExcludeFromSemantics;
Widget result = Semantics(
tooltip: _excludeFromSemantics ? null : _tooltipMessage,
tooltip: excludeFromSemantics ? null : _tooltipMessage,
child: widget.child,
);
......@@ -854,8 +801,11 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
),
);
}
return result;
return OverlayPortal(
controller: _overlayController,
overlayChildBuilder: _buildTooltipOverlay,
child: result,
);
}
}
......
......@@ -1041,15 +1041,9 @@ void main() {
),
);
// The tooltip overlay still on the tree and it will removed in the next frame.
// Dispatch the mouse in and out events before the overlay detached.
await gesture.moveTo(tester.getCenter(find.text(tooltipText)));
await gesture.moveTo(Offset.zero);
await tester.pumpAndSettle();
// Go without crashes.
await gesture.removePointer();
// The tooltip should be removed, including the overlay child.
expect(find.text(tooltipText), findsNothing);
expect(find.byTooltip(tooltipText), findsNothing);
});
testWidgetsWithLeakTracking('Calling ensureTooltipVisible on an unmounted TooltipState returns false', (WidgetTester tester) async {
......@@ -1435,35 +1429,6 @@ void main() {
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 {
Widget buildApp(String text, { required double textScaleFactor }) {
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