tooltip.rs

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