Skip to main content

Module tooltip

Module tooltip 

Source
Expand description

Material 3 Expressive tooltips: a plain, auto-dismissing label and a rich panel with a title, supporting text and up to two action buttons — both opened by hovering (pointer platforms) or a long-press (touch), and floated above the app by the framework’s overlay portal.

§Riding the framework portal

Each panel’s own view is an owner: it holds one frust::authoring::OverlaySlot, lays its pod out against the window, and — on every paint where TooltipHover::is_open reads true — registers it with the root, which paints it after the whole main tree and hit-tests it before the main tree, per the band and input class the owner declared. An app mounts the owner unconditionally, exactly as before; it costs one layout of the panel per frame and, while closed, nothing else.

frust::overlay_portal’s declarative wrapper anchors to its own child’s bounds, which does not fit here: a tooltip’s trigger and panel are two independently-mounted sibling views, so the panel’s slot anchors instead to the rect the trigger captured into the shared handle below (OverlayAnchor::Rect).

Two consequences of floating a pod rather than hosting it in the main tree, both of them the portal’s own documented v1 shape rather than this port’s choice: a pod publishes no accessibility nodes (they would attach under the owner’s node, at the owner’s position rather than the floated rect’s), and a pod receives no broadcast events — neither panel needs one, since the trigger’s long-press timer lives in the main tree and neither panel’s own content runs a threshold-time gesture.

§The shared handle

TooltipHover is the one non-reactive latch a caller keeps per tooltip — the trigger writes it, the panel reads it, and crate::overlay::anchored’s own OverlayAnchor cell rides inside it (carrying the trigger’s window-space rect) so a caller never juggles two handles. It is not reactive (docs/CODE_STANDARDS.md’s convention for this catalog’s other paint-latched handles, e.g. crate::overlay::anchored::OverlayAnchor itself): writing it wakes nothing on its own, so every writer also calls PaintCtx::request_frame/relies on the pointer event that carried it — see each write site below. It is read live, during the layout/paint of the very frame that wrote it, which is what lets the panel appear and disappear without waiting for a rebuild.

§Hover vs. long-press

tooltip_trigger wraps an arbitrary child transparently (every event still reaches it) and does two independent things:

  • Hover is read the same way crate::overlay’s shadcn-ported sibling catalog reads it for its own tooltip (frust_shadcn::components::tooltip): PaintCtx::is_hovered is the authoritative signal (self-correcting every paint, the one pass that reliably learns a pointer left without a dedicated event), claimed for via EventCtx::claim_hover on an in-bounds Move so a hovered control inside the child still wins the claim and this wrapper still reads hovered through the path (the claim-ordering rule). A transition writes TooltipHover and requests a frame; the panel’s owner reads that latch live in its own paint, so the frame the write asked for is the frame the panel appears on — or vanishes on.
  • Long-press is delegated whole to frust::GestureDetector (nested as this trigger’s one child, wrapping the caller’s real child in turn) rather than hand-rolled: touch has no hover, and GestureDetector’s own paint-clock timer + frust_core::mark_pending_result_flush machinery is the only way a plugin crate reaches a threshold-time fire with no further pointer event (crate::toggle_button’s own module docs explain why that function is unreachable from a design-system plugin directly — see its “Long-press” section). The callback ignores the &mut State it is handed and writes straight into the shared, non-reactive TooltipHover instead.

TooltipTriggerView::enabled gates both: false suppresses hover opening it, the long-press callback becomes a no-op, and an already-open tooltip snaps shut the moment enabled goes false mid-show.

§Plain: auto-dismiss, long-press only

M3ETooltipState._scheduleHide (m3e_tooltips.dart:72-78) arms a MaterialMotion::EXTRA_LONG_4-long (1000ms) one-shot timer only from the onLongPress handler — hover’s onEnter never calls it, closing purely on onExit instead — and never for a rich tooltip (if (widget._isRich) return;). This port’s plain panel widget captures hover.via() == TooltipOpenVia::LongPress once, on the first paint of each show (the same instant upstream’s _show(); _scheduleHide(); pair runs synchronously together), and runs the matching paint-clock timer only when that was true — a hover-shown plain tooltip never starts one, matching upstream exactly. “Each show” is counted by TooltipHover itself, which ticks a counter every time the latch goes from closed to open; a pod that stays mounted across many shows re-arms from that edge rather than from its own View::build, which now happens only once.

§Rich: title/supporting/actions, dismiss on tap-elsewhere

A rich panel never runs the auto-dismiss timer above; it stays open while the trigger keeps its hover, closes on hover-exit exactly like the plain panel, and additionally dismisses on a tap anywhere outside itself — upstream’s own Positioned.fill(child: GestureDetector(onTap: _hide, behavior: HitTestBehavior.translucent)), ported as the portal’s own frust::OutsideTap::Notify { consume: false } rather than a hand-rolled translucent layer: the root tells the owner that a press landed outside every floated surface, the owner closes the latch, and the press carries on into the main tree — translucent, exactly as upstream. Its actions row is routed to normally, as frust::OverlayInput::Interactive content.

Structs§

RichTooltipView
A declarative rich tooltip. See rich_tooltip.
RichTooltipWidget
The retained widget for a RichTooltipView: one OverlaySlot holding the rich panel, registered on every paint the latch reads open and on no other. See TooltipWidget for what that gates, and this view’s own configure for how the two panels’ registrations differ.
TooltipAction
One rich-tooltip action button: a label plus its own press callback. Build with tooltip_action, attach with RichTooltipView::action.
TooltipHover
The shared, non-reactive handle a caller keeps one of per tooltip: whether the panel should be showing, how it was opened, whether the trigger is enabled, and the trigger’s captured window-space rect (the merged crate::overlay::anchored host’s own OverlayAnchor, carried inside rather than threaded separately). See the module docs.
TooltipTriggerView
A declarative tooltip trigger. See tooltip_trigger.
TooltipTriggerWidget
The retained widget for a TooltipTriggerView.
TooltipView
A declarative plain tooltip. See tooltip.
TooltipWidget
The retained widget for a TooltipView: one OverlaySlot holding the plain panel, registered on every paint the latch reads open and on no other.

Functions§

rich_tooltip
Build a rich tooltip over hover, to be mounted unconditionally beside tooltip_trigger’s own view. See tooltip for the mount contract this mirrors, and RichTooltipView::title/RichTooltipView::action for the upstream richTitle/actions slots.
tooltip
Build a plain tooltip over hover, to be mounted unconditionally beside tooltip_trigger’s own view — it floats its own panel above the app and registers nothing at all while closed (see the module docs’ portal section).
tooltip_action
Build a rich-tooltip action labeled label, firing on_press.
tooltip_trigger
Wrap child as a tooltip trigger writing into hover. See the module docs’ Hover vs. long-press section.