Skip to main content

gpui_macos/
tooltip.rs

1//! Short, non-interactive explanations with GPUI's tooltip lifecycle.
2use crate::ActiveTheme;
3use gpui_kit::rems;
4use gpui_kit::{
5    App, AppContext, Context, Render, SharedString, StatefulInteractiveElement, Window, prelude::*,
6};
7use std::time::Duration;
8
9/// A composable tooltip surface, retaining the base tooltip role and styling API.
10pub fn tooltip(text: impl Into<SharedString>, cx: &App) -> gpui_kit::base::Tooltip {
11    let t = cx.macos();
12    gpui_kit::base::Tooltip::new("macos-tooltip")
13        .max_w(rems(20.))
14        .px(rems(0.625))
15        .py(rems(0.375))
16        .border_1()
17        .rounded(rems(0.375))
18        .border_color(t.glass_border())
19        .bg(t.glass_fill())
20        .shadow(t.control_shadow())
21        .text_color(t.foreground)
22        .font_family(t.font.clone())
23        .text_size(rems(0.6875))
24        .child(text.into())
25}
26
27/// Attach a macOS tooltip without changing the control's concrete type.
28/// GPUI owns hover delay, placement, dismissal, and cleanup when the owner leaves.
29/// Keep an accessible label on icon-only controls; a tooltip is supplementary.
30pub fn with_tooltip<T: StatefulInteractiveElement>(control: T, text: impl Into<SharedString>) -> T {
31    let text = text.into();
32    control
33        .tooltip_show_delay(Duration::from_millis(400))
34        .tooltip(move |_, cx| cx.new(|_| TooltipText(text.clone())).into())
35}
36
37struct TooltipText(SharedString);
38impl Render for TooltipText {
39    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
40        gpui_kit::div()
41            .debug_selector(|| "macos-tooltip-surface".into())
42            .child(tooltip(self.0.clone(), cx))
43    }
44}
45
46#[cfg(test)]
47mod tests {
48    use super::*;
49    use gpui_kit::px;
50    use gpui_kit::{Render, TestAppContext, point};
51    struct Harness;
52    impl Render for Harness {
53        fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
54            gpui_kit::div().size_full().child(with_tooltip(
55                crate::button("favorite", "Favorite", crate::ButtonVariant::Secondary, cx),
56                "Add to favorites",
57            ))
58        }
59    }
60    #[gpui_kit::test]
61    fn tooltip_waits_for_hover_and_hides_on_pointer_leave(cx: &mut TestAppContext) {
62        cx.update(crate::init);
63        let (_, cx) = cx.add_window_view(|_, _| Harness);
64        cx.update(|window, cx| {
65            window.draw(cx).clear(cx);
66        });
67        cx.simulate_event(gpui_kit::MouseMoveEvent {
68            position: point(px(10.), px(10.)),
69            pressed_button: None,
70            modifiers: Default::default(),
71        });
72        cx.update(|window, cx| {
73            window.draw(cx).clear(cx);
74        });
75        cx.dispatcher.advance_clock(Duration::from_millis(399));
76        cx.run_until_parked();
77        cx.update(|window, cx| {
78            window.draw(cx).clear(cx);
79        });
80        assert!(cx.debug_bounds("macos-tooltip-surface").is_none());
81        cx.dispatcher.advance_clock(Duration::from_millis(1));
82        cx.run_until_parked();
83        cx.update(|window, cx| {
84            window.draw(cx).clear(cx);
85        });
86        assert!(cx.debug_bounds("macos-tooltip-surface").is_some());
87        cx.simulate_event(gpui_kit::MouseMoveEvent {
88            position: point(px(400.), px(400.)),
89            pressed_button: None,
90            modifiers: Default::default(),
91        });
92        cx.dispatcher.advance_clock(Duration::from_secs(1));
93        cx.run_until_parked();
94        cx.update(|window, cx| {
95            window.draw(cx).clear(cx);
96        });
97        assert!(cx.debug_bounds("macos-tooltip-surface").is_none());
98    }
99}