tooltip.rs

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