Skip to main content

gpui_kit/overlay/
tooltip.rs

1//! Hover-delayed help for a control that is already usable without it.
2//!
3//! A tooltip is never actionable and never carries the only copy of something
4//! the user needs in order to act, because it cannot be reached by keyboard,
5//! by touch, or by anyone who does not hover.
6//!
7//! The delay, the placement, and the dismissal come from GPUI's own hover
8//! machinery ([`gpui::StatefulInteractiveElement::tooltip`]); this module supplies the
9//! themed surface it renders and the semantic node it publishes.
10
11use gpui::{
12    AnyView, App, AppContext as _, Context, IntoElement, ParentElement, Render, RenderOnce,
13    SharedString, Styled, Window, div, px,
14};
15use gpui_kit_semantics::{NodeSpec, Role, Semantic};
16use gpui_kit_theme::{ActiveTheme, Elevation, Radius, Space};
17
18use crate::foundation::{Ident, StyledExt};
19use crate::motion::{Animated, Entrance};
20
21/// A themed help surface.
22#[derive(Debug, Clone, IntoElement)]
23pub struct Tooltip {
24    ident: Ident,
25    text: SharedString,
26    describes: Option<SharedString>,
27}
28
29impl Tooltip {
30    pub fn new(ident: impl Into<Ident>, text: impl Into<SharedString>) -> Self {
31        Self {
32            ident: ident.into(),
33            text: text.into(),
34            describes: None,
35        }
36    }
37
38    /// Records the control this explains in deterministic semantic snapshots.
39    ///
40    /// GPUI does not currently expose a native cross-tree described-by
41    /// relation. Callers should also publish this help as a literal accessible
42    /// description on the role-bearing trigger when that association matters.
43    pub fn describes(mut self, control: impl Into<SharedString>) -> Self {
44        self.describes = Some(control.into());
45        self
46    }
47
48    /// Wraps the surface in the view GPUI's hover machinery renders.
49    pub fn view(self, cx: &mut App) -> AnyView {
50        cx.new(|_| TooltipView(self)).into()
51    }
52}
53
54impl RenderOnce for Tooltip {
55    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
56        let theme = cx.theme().clone();
57        let mut spec =
58            NodeSpec::new(self.ident.semantic_id(), Role::Tooltip).text(self.text.clone());
59        if let Some(control) = self.describes.clone() {
60            spec = spec.describes(control);
61        }
62
63        // The surface arrives rather than appearing. It publishes its node
64        // from the settled box and only the pixels travel, so a reader that
65        // asks where the tooltip is gets the answer it will still be giving
66        // once the arrival has finished.
67        let surface = div()
68            .max_w(px(260.0))
69            .px_token(&theme, Space::Sm)
70            .py_token(&theme, Space::Xs)
71            .radius(&theme, Radius::Small)
72            .bg(theme.colors.overlay)
73            .elevation(&theme, Elevation::Overlay)
74            .text_size(px(theme.typography.label.size))
75            .line_height(px(theme.typography.label.line_height))
76            .text_color(theme.colors.text)
77            .child(self.text.clone());
78
79        div()
80            .child(surface.animate_in(self.ident.child("in").element_id(), cx, Entrance::Menu))
81            .semantic_in(cx, spec)
82    }
83}
84
85struct TooltipView(Tooltip);
86
87impl Render for TooltipView {
88    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
89        self.0.clone()
90    }
91}
92
93/// Attaches hover help to a control.
94///
95/// Hover tracking needs an element identity, so the element must already carry
96/// an id.
97pub trait Tooltipped: gpui::StatefulInteractiveElement + Sized {
98    /// Shows `text` after GPUI's hover delay, published as help for the
99    /// control identified by `control` in deterministic semantic snapshots.
100    fn tip(self, control: impl Into<Ident>, text: impl Into<SharedString>) -> Self {
101        let control = control.into();
102        let text = text.into();
103        self.tooltip(move |_window, cx| {
104            Tooltip::new(control.child("tooltip"), text.clone())
105                .describes(control.semantic_id())
106                .view(cx)
107        })
108    }
109}
110
111impl<E: gpui::StatefulInteractiveElement + Sized> Tooltipped for E {}