1#![allow(missing_docs)]
2
3use gpui::{Action, AnyView, FocusHandle, IntoElement, Render, VisualContext};
4use settings::Settings;
5use theme::ThemeSettings;
6
7use crate::prelude::*;
8use crate::{h_flex, v_flex, Color, KeyBinding, Label, LabelSize, StyledExt};
9
10pub struct Tooltip {
11 title: SharedString,
12 meta: Option<SharedString>,
13 key_binding: Option<KeyBinding>,
14}
15
16impl Tooltip {
17 pub fn text(title: impl Into<SharedString>, cx: &mut WindowContext) -> AnyView {
18 cx.new_view(|_cx| Self {
19 title: title.into(),
20 meta: None,
21 key_binding: None,
22 })
23 .into()
24 }
25
26 pub fn for_action(
27 title: impl Into<SharedString>,
28 action: &dyn Action,
29 cx: &mut WindowContext,
30 ) -> AnyView {
31 cx.new_view(|cx| Self {
32 title: title.into(),
33 meta: None,
34 key_binding: KeyBinding::for_action(action, cx),
35 })
36 .into()
37 }
38
39 pub fn for_action_in(
40 title: impl Into<SharedString>,
41 action: &dyn Action,
42 focus_handle: &FocusHandle,
43 cx: &mut WindowContext,
44 ) -> AnyView {
45 cx.new_view(|cx| Self {
46 title: title.into(),
47 meta: None,
48 key_binding: KeyBinding::for_action_in(action, focus_handle, cx),
49 })
50 .into()
51 }
52 pub fn with_meta(
53 title: impl Into<SharedString>,
54 action: Option<&dyn Action>,
55 meta: impl Into<SharedString>,
56 cx: &mut WindowContext,
57 ) -> AnyView {
58 cx.new_view(|cx| Self {
59 title: title.into(),
60 meta: Some(meta.into()),
61 key_binding: action.and_then(|action| KeyBinding::for_action(action, cx)),
62 })
63 .into()
64 }
65
66 pub fn new(title: impl Into<SharedString>) -> Self {
67 Self {
68 title: title.into(),
69 meta: None,
70 key_binding: None,
71 }
72 }
73
74 pub fn meta(mut self, meta: impl Into<SharedString>) -> Self {
75 self.meta = Some(meta.into());
76 self
77 }
78
79 pub fn key_binding(mut self, key_binding: impl Into<Option<KeyBinding>>) -> Self {
80 self.key_binding = key_binding.into();
81 self
82 }
83}
84
85impl Render for Tooltip {
86 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
87 tooltip_container(cx, |el, _| {
88 el.child(
89 h_flex()
90 .gap_4()
91 .child(self.title.clone())
92 .when_some(self.key_binding.clone(), |this, key_binding| {
93 this.justify_between().child(key_binding)
94 }),
95 )
96 .when_some(self.meta.clone(), |this, meta| {
97 this.child(Label::new(meta).size(LabelSize::Small).color(Color::Muted))
98 })
99 })
100 }
101}
102
103pub fn tooltip_container<V>(
104 cx: &mut ViewContext<V>,
105 f: impl FnOnce(Div, &mut ViewContext<V>) -> Div,
106) -> impl IntoElement {
107 let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
108
109 // padding to avoid tooltip appearing right below the mouse cursor
110 div().pl_2().pt_2p5().child(
111 v_flex()
112 .elevation_2(cx)
113 .font(ui_font)
114 .text_ui(cx)
115 .text_color(cx.theme().colors().text)
116 .py_1()
117 .px_2()
118 .map(|el| f(el, cx)),
119 )
120}
121
122pub struct LinkPreview {
123 link: SharedString,
124}
125
126impl LinkPreview {
127 pub fn new(url: &str, cx: &mut WindowContext) -> AnyView {
128 let mut wrapped_url = String::new();
129 for (i, ch) in url.chars().enumerate() {
130 if i == 500 {
131 wrapped_url.push('…');
132 break;
133 }
134 if i % 100 == 0 && i != 0 {
135 wrapped_url.push('\n');
136 }
137 wrapped_url.push(ch);
138 }
139 cx.new_view(|_cx| LinkPreview {
140 link: wrapped_url.into(),
141 })
142 .into()
143 }
144}
145
146impl Render for LinkPreview {
147 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
148 tooltip_container(cx, |el, _| {
149 el.child(
150 Label::new(self.link.clone())
151 .size(LabelSize::XSmall)
152 .color(Color::Muted),
153 )
154 })
155 }
156}