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(Label::new(meta).size(LabelSize::Small).color(Color::Muted))
164            })
165        })
166    }
167}
168
169pub fn tooltip_container<V, ContentsBuilder: FnOnce(Div, &mut Window, &mut Context<V>) -> Div>(
170    window: &mut Window,
171    cx: &mut Context<V>,
172    f: ContentsBuilder,
173) -> impl IntoElement + use<V, ContentsBuilder> {
174    let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
175
176    // padding to avoid tooltip appearing right below the mouse cursor
177    div().pl_2().pt_2p5().child(
178        v_flex()
179            .elevation_2(cx)
180            .font(ui_font)
181            .text_ui(cx)
182            .text_color(cx.theme().colors().text)
183            .py_1()
184            .px_2()
185            .map(|el| f(el, window, cx)),
186    )
187}
188
189pub struct LinkPreview {
190    link: SharedString,
191}
192
193impl LinkPreview {
194    pub fn new(url: &str, cx: &mut App) -> AnyView {
195        let mut wrapped_url = String::new();
196        for (i, ch) in url.chars().enumerate() {
197            if i == 500 {
198                wrapped_url.push('…');
199                break;
200            }
201            if i % 100 == 0 && i != 0 {
202                wrapped_url.push('\n');
203            }
204            wrapped_url.push(ch);
205        }
206        cx.new(|_| LinkPreview {
207            link: wrapped_url.into(),
208        })
209        .into()
210    }
211}
212
213impl Render for LinkPreview {
214    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
215        tooltip_container(window, cx, |el, _, _| {
216            el.child(
217                Label::new(self.link.clone())
218                    .size(LabelSize::XSmall)
219                    .color(Color::Muted),
220            )
221        })
222    }
223}
224
225impl Component for Tooltip {
226    fn scope() -> ComponentScope {
227        ComponentScope::None
228    }
229
230    fn description() -> Option<&'static str> {
231        Some(
232            "A tooltip that appears when hovering over an element, optionally showing a keybinding or additional metadata.",
233        )
234    }
235
236    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
237        Some(
238            example_group(vec![single_example(
239                "Text only",
240                Button::new("delete-example", "Delete")
241                    .tooltip(Tooltip::text("This is a tooltip!"))
242                    .into_any_element(),
243            )])
244            .into_any_element(),
245        )
246    }
247}