tooltip.rs

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