blob: 2c4bc164c9ea3c1151ca0db007ea3164042d1de2 [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/// @docImport 'package:flutter/widgets.dart';
///
/// @docImport 'app.dart';
/// @docImport 'floating_action_button.dart';
/// @docImport 'icon_button.dart';
/// @docImport 'popup_menu.dart';
library;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'text_theme.dart';
import 'theme.dart';
import 'tooltip_theme.dart';
import 'tooltip_visibility.dart';
/// A Material Design tooltip.
///
/// 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.
///
/// 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.
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=EeEfD5fI-5Q}
///
/// {@tool dartpad}
/// This example show a basic [Tooltip] which has a [Text] as child.
/// [message] contains your label to be shown by the tooltip when
/// the child that Tooltip wraps is hovered over on web or desktop. On mobile,
/// the tooltip is shown when the widget is long pressed.
///
/// This tooltip will default to showing above the [Text] instead of below
/// because its ambient [TooltipThemeData.preferBelow] is false.
/// (See the use of [MaterialApp.theme].)
/// Setting that piece of theme data is recommended to avoid having a finger or
/// cursor hide the tooltip. For other ways to set that piece of theme data see:
///
/// * [Theme.data], [ThemeData.tooltipTheme]
/// * [TooltipTheme.data]
///
/// or it can be set directly on each tooltip with [Tooltip.preferBelow].
///
/// ** See code in examples/api/lib/material/tooltip/tooltip.0.dart **
/// {@end-tool}
///
/// {@tool dartpad}
/// This example covers most of the attributes available in Tooltip.
/// `decoration` has been used to give a gradient and borderRadius to Tooltip.
/// `constraints` has been used to set the minimum width of the Tooltip.
/// `preferBelow` is true; the tooltip will prefer showing below [Tooltip]'s child widget.
/// However, it may show the tooltip above if there's not enough space
/// below 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 or the mouse pointer exits the child widget.
/// `waitDuration` accepts a Duration for which a mouse pointer has to hover over the child
/// widget before the tooltip is shown.
///
/// ** See code in examples/api/lib/material/tooltip/tooltip.1.dart **
/// {@end-tool}
///
/// {@tool dartpad}
/// This example shows a rich [Tooltip] that specifies the [richMessage]
/// parameter instead of the [message] parameter (only one of these may be
/// non-null. Any [InlineSpan] can be specified for the [richMessage] attribute,
/// including [WidgetSpan].
///
/// ** See code in examples/api/lib/material/tooltip/tooltip.2.dart **
/// {@end-tool}
///
/// {@tool dartpad}
/// This example shows how [Tooltip] can be shown manually with [TooltipTriggerMode.manual]
/// by calling the [TooltipState.ensureTooltipVisible] function.
///
/// ** See code in examples/api/lib/material/tooltip/tooltip.3.dart **
/// {@end-tool}
///
/// See also:
///
/// * <https://material.io/design/components/tooltips.html>
/// * [TooltipTheme] or [ThemeData.tooltipTheme]
/// * [TooltipVisibility]
class Tooltip extends StatefulWidget {
/// Creates a tooltip.
///
/// 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].
///
/// All parameters that are defined in the constructor will
/// override the default values _and_ the values in [TooltipTheme.of].
///
/// Only one of [message] and [richMessage] may be non-null.
const Tooltip({
super.key,
this.message,
this.richMessage,
@Deprecated(
'Use Tooltip.constraints instead. '
'This feature was deprecated after v3.30.0-0.1.pre.',
)
this.height,
this.constraints,
this.padding,
this.margin,
this.verticalOffset,
this.preferBelow,
this.excludeFromSemantics,
this.decoration,
this.textStyle,
this.textAlign,
this.waitDuration,
this.showDuration,
this.exitDuration,
this.enableTapToDismiss = true,
this.triggerMode,
this.enableFeedback,
this.onTriggered,
this.mouseCursor,
this.ignorePointer,
this.positionDelegate,
this.child,
}) : assert(
(message == null) != (richMessage == null),
'Either `message` or `richMessage` must be specified',
),
assert(
height == null || constraints == null,
'Only one of `height` and `constraints` may be specified.',
);
/// The text to display in the tooltip.
///
/// Only one of [message] and [richMessage] may be non-null.
final String? message;
/// The rich text to display in the tooltip.
///
/// Only one of [message] and [richMessage] may be non-null.
final InlineSpan? richMessage;
/// The minimum height of the [Tooltip]'s message.
@Deprecated(
'Use Tooltip.constraints instead. '
'This feature was deprecated after v3.30.0-0.1.pre.',
)
final double? height;
/// Constrains the size of the [Tooltip]'s message.
///
/// If null, then the [TooltipThemeData.constraints] of the ambient [ThemeData.tooltipTheme]
/// will be used. If that is also null, then a default value will be picked based on the current
/// platform. For desktop platforms, the default value is `BoxConstraints(minHeight: 24.0)`,
/// while for mobile platforms the default value is `BoxConstraints(minHeight: 32.0)`.
final BoxConstraints? constraints;
/// The amount of space by which to inset the [Tooltip]'s message.
///
/// On mobile, defaults to 16.0 logical pixels horizontally and 4.0 vertically.
/// On desktop, defaults to 8.0 logical pixels horizontally and 4.0 vertically.
final EdgeInsetsGeometry? padding;
/// 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.
///
/// See also:
///
/// * [constraints], which allow setting an explicit size for the tooltip.
final EdgeInsetsGeometry? margin;
/// The vertical gap between the widget and the displayed tooltip.
///
/// 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.
final double? verticalOffset;
/// Whether the tooltip defaults to being displayed below the widget.
///
/// If there is insufficient space to display the tooltip in
/// the preferred direction, the tooltip will be displayed in the opposite
/// direction.
///
/// If this property is null, then [TooltipThemeData.preferBelow] is used.
/// If that is also null, the default value is true.
///
/// Applying [TooltipThemeData.preferBelow]: `false` for the entire app
/// is recommended to avoid having a finger or cursor hide a tooltip.
final bool? preferBelow;
/// Whether the tooltip's [message] or [richMessage] should be excluded from
/// the semantics tree.
///
/// Defaults to false. A tooltip will add a [Semantics] label that is set to
/// [Tooltip.message] if non-null, or the plain text value of
/// [Tooltip.richMessage] otherwise. Set this property to true if the app is
/// going to provide its own custom semantics label.
final bool? excludeFromSemantics;
/// The widget below this widget in the tree.
///
/// {@macro flutter.widgets.ProxyWidget.child}
final Widget? child;
/// Specifies the tooltip's shape and background color.
///
/// 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.light], and
/// [Colors.white] if it is [Brightness.dark].
final Decoration? decoration;
/// 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],
/// [TextTheme.bodyMedium] of [ThemeData.textTheme] will be used with
/// [Colors.white]. Otherwise, if [ThemeData.brightness] is set to
/// [Brightness.light], [TextTheme.bodyMedium] of [ThemeData.textTheme] will be
/// used with [Colors.black].
final TextStyle? textStyle;
/// How the message of the tooltip is aligned horizontally.
///
/// If this property is null, then [TooltipThemeData.textAlign] is used.
/// If [TooltipThemeData.textAlign] is also null, the default value is
/// [TextAlign.start].
final TextAlign? textAlign;
/// {@macro flutter.widgets.RawTooltip.hoverDelay}
final Duration? waitDuration;
/// {@macro flutter.widgets.RawTooltip.touchDelay}
///
/// See also:
///
/// * [exitDuration], which allows configuring the time until a pointer
/// disappears when hovering.
final Duration? showDuration;
/// {@macro flutter.widgets.RawTooltip.dismissDelay}
///
/// See also:
///
/// * [showDuration], which allows configuring the length of time that a
/// tooltip will be visible after touch events are released.
final Duration? exitDuration;
/// {@macro flutter.widgets.RawTooltip.enableTapToDismiss}
final bool enableTapToDismiss;
/// {@macro flutter.widgets.RawTooltip.triggerMode}
///
/// If this property is null, then [TooltipThemeData.triggerMode] is used.
/// If [TooltipThemeData.triggerMode] is also null, the default mode is
/// [TooltipTriggerMode.longPress].
final TooltipTriggerMode? triggerMode;
/// {@macro flutter.widgets.RawTooltip.enableFeedback}
final bool? enableFeedback;
/// {@macro flutter.widgets.RawTooltip.onTriggered}
final TooltipTriggeredCallback? onTriggered;
/// The cursor for a mouse pointer when it enters or is hovering over the
/// widget.
///
/// If this property is null, [MouseCursor.defer] will be used.
final MouseCursor? mouseCursor;
/// Whether this tooltip should be invisible to hit testing.
///
/// If no value is passed, pointer events are ignored unless the tooltip has a
/// [richMessage] instead of a [message].
///
/// See also:
///
/// * [IgnorePointer], for more information about how pointer events are
/// handled or ignored.
final bool? ignorePointer;
/// {@macro flutter.widgets.RawTooltip.positionDelegate}
final TooltipPositionDelegate? positionDelegate;
/// {@macro flutter.widgets.RawTooltip.dismissAllToolTips}
static bool dismissAllToolTips() {
return RawTooltip.dismissAllToolTips();
}
@override
State<Tooltip> createState() => TooltipState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(
StringProperty(
'message',
message,
showName: message == null,
defaultValue: message == null ? null : kNoDefaultValue,
),
);
properties.add(
StringProperty(
'richMessage',
richMessage?.toPlainText(),
showName: richMessage == null,
defaultValue: richMessage == null ? null : kNoDefaultValue,
),
);
properties.add(DoubleProperty('height', height, defaultValue: null));
properties.add(
DiagnosticsProperty<BoxConstraints>('constraints', constraints, defaultValue: null),
);
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: null));
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('margin', margin, defaultValue: null));
properties.add(DoubleProperty('vertical offset', verticalOffset, defaultValue: null));
properties.add(
FlagProperty(
'position',
value: preferBelow,
ifTrue: 'below',
ifFalse: 'above',
showName: true,
),
);
properties.add(
FlagProperty('semantics', value: excludeFromSemantics, ifTrue: 'excluded', showName: true),
);
properties.add(
DiagnosticsProperty<Duration>('wait duration', waitDuration, defaultValue: null),
);
properties.add(
DiagnosticsProperty<Duration>('show duration', showDuration, defaultValue: null),
);
properties.add(
DiagnosticsProperty<Duration>('exit duration', exitDuration, defaultValue: null),
);
properties.add(
DiagnosticsProperty<TooltipTriggerMode>('triggerMode', triggerMode, defaultValue: null),
);
properties.add(
FlagProperty('enableFeedback', value: enableFeedback, ifTrue: 'true', showName: true),
);
properties.add(DiagnosticsProperty<TextAlign>('textAlign', textAlign, defaultValue: null));
properties.add(
DiagnosticsProperty<TooltipPositionDelegate>(
'positionDelegate',
positionDelegate,
defaultValue: null,
),
);
}
}
/// Contains the state for a [Tooltip].
///
/// This class can be used to programmatically show the Tooltip, see the
/// [ensureTooltipVisible] method.
class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
static const double _defaultVerticalOffset = 24.0;
static const bool _defaultPreferBelow = true;
static const EdgeInsetsGeometry _defaultMargin = EdgeInsets.zero;
static const Duration _defaultShowDuration = Duration(milliseconds: 1500);
static const Duration _defaultExitDuration = Duration(milliseconds: 100);
static const Duration _defaultWaitDuration = Duration.zero;
static const bool _defaultExcludeFromSemantics = false;
static const TooltipTriggerMode _defaultTriggerMode = TooltipTriggerMode.longPress;
static const bool _defaultEnableFeedback = true;
static const TextAlign _defaultTextAlign = TextAlign.start;
final GlobalKey<RawTooltipState> _tooltipKey = GlobalKey<RawTooltipState>();
// From InheritedWidgets
late bool _visible;
late TooltipThemeData _tooltipTheme;
/// {@macro flutter.widgets.RawTooltipState.ensureTooltipVisible}
bool ensureTooltipVisible() {
return _tooltipKey.currentState?.ensureTooltipVisible() ?? false;
}
@protected
@override
void didChangeDependencies() {
super.didChangeDependencies();
_visible = TooltipVisibility.of(context);
_tooltipTheme = TooltipTheme.of(context);
}
// https://material.io/components/tooltips#specs
double _getDefaultTooltipHeight() {
return switch (Theme.of(context).platform) {
TargetPlatform.macOS || TargetPlatform.linux || TargetPlatform.windows => 24.0,
TargetPlatform.android || TargetPlatform.fuchsia || TargetPlatform.iOS => 32.0,
};
}
EdgeInsets _getDefaultPadding() {
return switch (Theme.of(context).platform) {
TargetPlatform.macOS ||
TargetPlatform.linux ||
TargetPlatform.windows => const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
TargetPlatform.android ||
TargetPlatform.fuchsia ||
TargetPlatform.iOS => const EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0),
};
}
static double _getDefaultFontSize(TargetPlatform platform) {
return switch (platform) {
TargetPlatform.macOS || TargetPlatform.linux || TargetPlatform.windows => 12.0,
TargetPlatform.android || TargetPlatform.fuchsia || TargetPlatform.iOS => 14.0,
};
}
Offset _getDefaultPositionDelegate(TooltipPositionContext context) {
final double effectiveVerticalOffset =
widget.verticalOffset ?? _tooltipTheme.verticalOffset ?? _defaultVerticalOffset;
final bool effectivePreferBelow =
widget.preferBelow ?? _tooltipTheme.preferBelow ?? _defaultPreferBelow;
final resolvedContext = TooltipPositionContext(
target: context.target,
targetSize: context.targetSize,
tooltipSize: context.tooltipSize,
overlaySize: context.overlaySize,
verticalOffset: effectiveVerticalOffset,
preferBelow: effectivePreferBelow,
);
return widget.positionDelegate?.call(resolvedContext) ??
positionDependentBox(
size: context.overlaySize,
childSize: context.tooltipSize,
target: context.target,
verticalOffset: effectiveVerticalOffset,
preferBelow: effectivePreferBelow,
);
}
@override
Widget build(BuildContext context) {
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 defaultConstraints = BoxConstraints(
minHeight: widget.height ?? _tooltipTheme.height ?? _getDefaultTooltipHeight(),
);
final Widget tooltipBox = _TooltipBox(
constraints: widget.constraints ?? _tooltipTheme.constraints ?? defaultConstraints,
textStyle: widget.textStyle ?? _tooltipTheme.textStyle ?? defaultTextStyle,
textAlign: widget.textAlign ?? _tooltipTheme.textAlign ?? _defaultTextAlign,
decoration: widget.decoration ?? _tooltipTheme.decoration ?? defaultDecoration,
padding: widget.padding ?? _tooltipTheme.padding ?? _getDefaultPadding(),
margin: widget.margin ?? _tooltipTheme.margin ?? _defaultMargin,
richMessage: widget.richMessage ?? TextSpan(text: widget.message),
);
Widget effectiveChild = MouseRegion(
cursor: widget.mouseCursor ?? MouseCursor.defer,
child: widget.child ?? const SizedBox.shrink(),
);
final bool excludeFromSemantics =
widget.excludeFromSemantics ??
_tooltipTheme.excludeFromSemantics ??
_defaultExcludeFromSemantics;
if (_visible) {
effectiveChild = RawTooltip(
key: _tooltipKey,
semanticsTooltip: excludeFromSemantics
? null
: widget.message ?? widget.richMessage?.toPlainText() ?? '',
tooltipBuilder: (BuildContext context, Animation<double> animation) => IgnorePointer(
ignoring: widget.ignorePointer ?? widget.message != null,
child: FadeTransition(opacity: animation, child: tooltipBox),
),
touchDelay: widget.showDuration ?? _tooltipTheme.showDuration ?? _defaultShowDuration,
triggerMode: widget.triggerMode ?? _tooltipTheme.triggerMode ?? _defaultTriggerMode,
enableFeedback:
widget.enableFeedback ?? _tooltipTheme.enableFeedback ?? _defaultEnableFeedback,
hoverDelay: widget.waitDuration ?? _tooltipTheme.waitDuration ?? _defaultWaitDuration,
enableTapToDismiss: widget.enableTapToDismiss,
onTriggered: widget.onTriggered,
dismissDelay: widget.exitDuration ?? _tooltipTheme.exitDuration ?? _defaultExitDuration,
positionDelegate: _getDefaultPositionDelegate,
child: effectiveChild,
);
}
return effectiveChild;
}
}
class _TooltipBox extends StatelessWidget {
const _TooltipBox({
required this.constraints,
required this.textStyle,
required this.textAlign,
required this.decoration,
required this.padding,
required this.margin,
required this.richMessage,
});
final BoxConstraints constraints;
final TextStyle textStyle;
final TextAlign textAlign;
final Decoration? decoration;
final EdgeInsetsGeometry? padding;
final EdgeInsetsGeometry? margin;
final InlineSpan richMessage;
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: constraints,
child: DefaultTextStyle(
style: textStyle,
textAlign: textAlign,
child: Container(
decoration: decoration,
padding: padding,
margin: margin,
child: Center(
widthFactor: 1.0,
heightFactor: 1.0,
child: Text.rich(richMessage, style: textStyle, textAlign: textAlign),
),
),
),
);
}
}