tooltip.rs

  1use gpui::{Action, AnyElement, AnyView, AppContext as _, FocusHandle, IntoElement, Render};
  2use settings::Settings;
  3use theme::ThemeSettings;
  4
  5use crate::prelude::*;
  6use crate::{Color, KeyBinding, Label, LabelSize, StyledExt, h_flex, v_flex};
  7
  8#[derive(RegisterComponent)]
  9pub struct Tooltip {
 10    title: SharedString,
 11    meta: Option<SharedString>,
 12    key_binding: Option<KeyBinding>,
 13}
 14
 15impl Tooltip {
 16    pub fn simple(title: impl Into<SharedString>, cx: &mut App) -> AnyView {
 17        cx.new(|_| Self {
 18            title: title.into(),
 19            meta: None,
 20            key_binding: None,
 21        })
 22        .into()
 23    }
 24
 25    pub fn text(title: impl Into<SharedString>) -> impl Fn(&mut Window, &mut App) -> AnyView {
 26        let title = title.into();
 27        move |_, cx| {
 28            cx.new(|_| Self {
 29                title: title.clone(),
 30                meta: None,
 31                key_binding: None,
 32            })
 33            .into()
 34        }
 35    }
 36
 37    pub fn for_action_title<Title: Into<SharedString>>(
 38        title: Title,
 39        action: &dyn Action,
 40    ) -> impl Fn(&mut Window, &mut App) -> AnyView + use<Title> {
 41        let title = title.into();
 42        let action = action.boxed_clone();
 43        move |window, cx| {
 44            cx.new(|cx| Self {
 45                title: title.clone(),
 46                meta: None,
 47                key_binding: KeyBinding::for_action(action.as_ref(), window, cx),
 48            })
 49            .into()
 50        }
 51    }
 52
 53    pub fn for_action_title_in<Str: Into<SharedString>>(
 54        title: Str,
 55        action: &dyn Action,
 56        focus_handle: &FocusHandle,
 57    ) -> impl Fn(&mut Window, &mut App) -> AnyView + use<Str> {
 58        let title = title.into();
 59        let action = action.boxed_clone();
 60        let focus_handle = focus_handle.clone();
 61        move |window, cx| {
 62            cx.new(|cx| Self {
 63                title: title.clone(),
 64                meta: None,
 65                key_binding: KeyBinding::for_action_in(action.as_ref(), &focus_handle, window, cx),
 66            })
 67            .into()
 68        }
 69    }
 70
 71    pub fn for_action(
 72        title: impl Into<SharedString>,
 73        action: &dyn Action,
 74        window: &mut Window,
 75        cx: &mut App,
 76    ) -> AnyView {
 77        cx.new(|cx| Self {
 78            title: title.into(),
 79            meta: None,
 80            key_binding: KeyBinding::for_action(action, window, cx),
 81        })
 82        .into()
 83    }
 84
 85    pub fn for_action_in(
 86        title: impl Into<SharedString>,
 87        action: &dyn Action,
 88        focus_handle: &FocusHandle,
 89        window: &mut Window,
 90        cx: &mut App,
 91    ) -> AnyView {
 92        cx.new(|cx| Self {
 93            title: title.into(),
 94            meta: None,
 95            key_binding: KeyBinding::for_action_in(action, focus_handle, window, cx),
 96        })
 97        .into()
 98    }
 99
100    pub fn with_meta(
101        title: impl Into<SharedString>,
102        action: Option<&dyn Action>,
103        meta: impl Into<SharedString>,
104        window: &mut Window,
105        cx: &mut App,
106    ) -> AnyView {
107        cx.new(|cx| Self {
108            title: title.into(),
109            meta: Some(meta.into()),
110            key_binding: action.and_then(|action| KeyBinding::for_action(action, window, cx)),
111        })
112        .into()
113    }
114
115    pub fn with_meta_in(
116        title: impl Into<SharedString>,
117        action: Option<&dyn Action>,
118        meta: impl Into<SharedString>,
119        focus_handle: &FocusHandle,
120        window: &mut Window,
121        cx: &mut App,
122    ) -> AnyView {
123        cx.new(|cx| Self {
124            title: title.into(),
125            meta: Some(meta.into()),
126            key_binding: action
127                .and_then(|action| KeyBinding::for_action_in(action, focus_handle, window, cx)),
128        })
129        .into()
130    }
131
132    pub fn new(title: impl Into<SharedString>) -> Self {
133        Self {
134            title: title.into(),
135            meta: None,
136            key_binding: None,
137        }
138    }
139
140    pub fn meta(mut self, meta: impl Into<SharedString>) -> Self {
141        self.meta = Some(meta.into());
142        self
143    }
144
145    pub fn key_binding(mut self, key_binding: impl Into<Option<KeyBinding>>) -> Self {
146        self.key_binding = key_binding.into();
147        self
148    }
149}
150
151impl Render for Tooltip {
152    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
153        tooltip_container(window, cx, |el, _, _| {
154            el.child(
155                h_flex()
156                    .gap_4()
157                    .child(div().max_w_72().child(self.title.clone()))
158                    .when_some(self.key_binding.clone(), |this, key_binding| {
159                        this.justify_between().child(key_binding)
160                    }),
161            )
162            .when_some(self.meta.clone(), |this, meta| {
163                this.child(
164                    div()
165                        .max_w_72()
166                        .child(Label::new(meta).size(LabelSize::Small).color(Color::Muted)),
167                )
168            })
169        })
170    }
171}
172
173pub fn tooltip_container<V, ContentsBuilder: FnOnce(Div, &mut Window, &mut Context<V>) -> Div>(
174    window: &mut Window,
175    cx: &mut Context<V>,
176    f: ContentsBuilder,
177) -> impl IntoElement + use<V, ContentsBuilder> {
178    let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
179
180    // padding to avoid tooltip appearing right below the mouse cursor
181    div().pl_2().pt_2p5().child(
182        v_flex()
183            .elevation_2(cx)
184            .font(ui_font)
185            .text_ui(cx)
186            .text_color(cx.theme().colors().text)
187            .py_1()
188            .px_2()
189            .map(|el| f(el, window, cx)),
190    )
191}
192
193pub struct LinkPreview {
194    link: SharedString,
195}
196
197impl LinkPreview {
198    pub fn new(url: &str, cx: &mut App) -> AnyView {
199        let mut wrapped_url = String::new();
200        for (i, ch) in url.chars().enumerate() {
201            if i == 500 {
202                wrapped_url.push('…');
203                break;
204            }
205            if i % 100 == 0 && i != 0 {
206                wrapped_url.push('\n');
207            }
208            wrapped_url.push(ch);
209        }
210        cx.new(|_| LinkPreview {
211            link: wrapped_url.into(),
212        })
213        .into()
214    }
215}
216
217impl Render for LinkPreview {
218    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
219        tooltip_container(window, cx, |el, _, _| {
220            el.child(
221                Label::new(self.link.clone())
222                    .size(LabelSize::XSmall)
223                    .color(Color::Muted),
224            )
225        })
226    }
227}
228
229impl Component for Tooltip {
230    fn scope() -> ComponentScope {
231        ComponentScope::None
232    }
233
234    fn description() -> Option<&'static str> {
235        Some(
236            "A tooltip that appears when hovering over an element, optionally showing a keybinding or additional metadata.",
237        )
238    }
239
240    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
241        Some(
242            example_group(vec![single_example(
243                "Text only",
244                Button::new("delete-example", "Delete")
245                    .tooltip(Tooltip::text("This is a tooltip!"))
246                    .into_any_element(),
247            )])
248            .into_any_element(),
249        )
250    }
251}