tooltip.rs

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