1use std::{cell::Cell, rc::Rc, time::Duration};
2
3use gpui::{
4 Action, AnyElement, AnyView, App, AppContext, Bounds, Context, ElementId, IntoElement,
5 MouseButton, ParentElement, Pixels, Render, SharedString, StatefulInteractiveElement,
6 StyleRefinement, Styled, Window, div, prelude::FluentBuilder, px,
7};
8use gpui_base::{
9 Tooltip as BaseTooltip, TooltipOverlay as BaseTooltipOverlay,
10 TooltipRequest as BaseTooltipRequest, TooltipTransition as BaseTooltipTransition,
11};
12
13use crate::{
14 ActiveTheme, Placement, StyledExt,
15 animation::{EffectTransition, ease_in_out_cubic, ease_out_cubic},
16 kbd::Kbd,
17 root::Root,
18 text::Text,
19};
20
21pub(crate) fn init(_cx: &mut App) {
22 }
24
25enum TooltipContext {
28 Text(Text),
29 Element(Box<dyn Fn(&mut Window, &mut App) -> AnyElement>),
30}
31
32pub struct Tooltip {
35 style: StyleRefinement,
36 content: TooltipContext,
37 key_binding: Option<Kbd>,
38 action: Option<(Box<dyn Action>, Option<SharedString>)>,
39}
40
41impl Tooltip {
42 pub fn new(text: impl Into<Text>) -> Self {
44 Self {
45 style: StyleRefinement::default(),
46 content: TooltipContext::Text(text.into()),
47 key_binding: None,
48 action: None,
49 }
50 }
51
52 pub fn element<E, F>(builder: F) -> Self
54 where
55 E: IntoElement,
56 F: Fn(&mut Window, &mut App) -> E + 'static,
57 {
58 Self {
59 style: StyleRefinement::default(),
60 key_binding: None,
61 action: None,
62 content: TooltipContext::Element(Box::new(move |window, cx| {
63 builder(window, cx).into_any_element()
64 })),
65 }
66 }
67
68 pub fn action(mut self, action: &dyn Action, context: Option<&str>) -> Self {
70 self.action = Some((action.boxed_clone(), context.map(SharedString::new)));
71 self
72 }
73
74 pub fn key_binding(mut self, key_binding: Option<Kbd>) -> Self {
76 self.key_binding = key_binding;
77 self
78 }
79
80 pub fn build(self, _: &mut Window, cx: &mut App) -> AnyView {
82 cx.new(|_| self).into()
83 }
84}
85
86impl FluentBuilder for Tooltip {}
87impl Styled for Tooltip {
88 fn style(&mut self) -> &mut StyleRefinement {
89 &mut self.style
90 }
91}
92impl Render for Tooltip {
93 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
94 let key_binding = if let Some(key_binding) = &self.key_binding {
95 Some(key_binding.clone())
96 } else {
97 if let Some((action, context)) = &self.action {
98 Kbd::binding_for_action(
99 action.as_ref(),
100 context.as_ref().map(|s| s.as_ref()),
101 window,
102 )
103 } else {
104 None
105 }
106 };
107
108 div().child(
109 BaseTooltip::new("tooltip-popup")
111 .h_flex()
112 .font_family(cx.theme().font_family.clone())
113 .m_3()
114 .bg(cx.theme().tokens.popover)
115 .text_color(cx.theme().popover_foreground)
116 .bg(cx.theme().tokens.popover)
117 .border_1()
118 .border_color(cx.theme().border)
119 .shadow_md()
120 .rounded(cx.theme().radius)
121 .justify_between()
122 .py_0p5()
123 .px_2()
124 .text_sm()
125 .gap_3()
126 .refine_style(&self.style)
127 .map(|this| {
128 this.child(div().map(|this| match self.content {
129 TooltipContext::Text(ref text) => this.child(text.clone()),
130 TooltipContext::Element(ref builder) => this.child(builder(window, cx)),
131 }))
132 })
133 .when_some(key_binding, |this, kbd| {
134 this.child(
135 div()
136 .text_xs()
137 .flex_shrink_0()
138 .text_color(cx.theme().muted_foreground)
139 .child(kbd.appearance(false)),
140 )
141 }),
142 )
143 }
144}
145
146const ENTER_DURATION: Duration = Duration::from_millis(150);
150const SLIDE_DURATION: Duration = Duration::from_millis(200);
152pub(crate) fn render_tooltip(
153 content_view: AnyView,
154 transition: BaseTooltipTransition,
155 _: &mut Window,
156 _: &mut App,
157) -> AnyElement {
158 div().child(content_view).map(|element| match transition {
159 BaseTooltipTransition::Switch {
160 epoch,
161 previous,
162 current,
163 } => {
164 let same_row = (current.origin.y - previous.origin.y).abs() < px(10.);
165 if !same_row {
166 return element.into_any_element();
167 }
168 let dx = current.center().x - previous.center().x;
169 EffectTransition::new(SLIDE_DURATION)
170 .ease(ease_in_out_cubic)
171 .slide_x(-dx, px(0.))
172 .apply(
173 element,
174 ElementId::NamedInteger("tooltip-slide".into(), epoch as u64),
175 )
176 .into_any_element()
177 }
178 BaseTooltipTransition::Enter { epoch } => EffectTransition::new(ENTER_DURATION)
179 .ease(ease_out_cubic)
180 .slide_y(px(4.), px(0.))
181 .fade(0.0, 1.0)
182 .apply(
183 element,
184 ElementId::NamedInteger("tooltip-enter".into(), epoch as u64),
185 )
186 .into_any_element(),
187 })
188}
189
190#[derive(Default)]
197pub(crate) struct ComponentTooltip {
198 pub text: Option<(
199 SharedString,
200 Option<(Rc<Box<dyn Action>>, Option<SharedString>)>,
201 )>,
202 pub builder: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyView>>,
203}
204
205impl ComponentTooltip {
206 pub fn apply<E: ManagedTooltipExt>(self, el: E) -> E {
208 if let Some(builder) = self.builder {
209 el.managed_tooltip(move |window, cx| builder(window, cx))
210 } else if let Some((text, action)) = self.text {
211 el.managed_tooltip(move |window, cx| {
212 Tooltip::new(text.clone())
213 .when_some(action.clone(), |this, (action, context)| {
214 this.action(
215 action.boxed_clone().as_ref(),
216 context.as_ref().map(|c| c.as_ref()),
217 )
218 })
219 .build(window, cx)
220 })
221 } else {
222 el
223 }
224 }
225}
226
227pub(crate) trait ManagedTooltipExt:
230 StatefulInteractiveElement + crate::ElementExt + Sized
231{
232 fn managed_tooltip(
233 self,
234 build_tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
235 ) -> Self {
236 self.managed_tooltip_with_placement(None, build_tooltip)
237 }
238
239 fn managed_tooltip_at(
240 self,
241 placement: Placement,
242 build_tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
243 ) -> Self {
244 self.managed_tooltip_with_placement(Some(placement), build_tooltip)
245 }
246
247 fn managed_tooltip_with_placement(
248 self,
249 preferred_placement: Option<Placement>,
250 build_tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
251 ) -> Self {
252 let build_tooltip = Rc::new(build_tooltip);
253 let trigger_bounds_cell: Rc<Cell<Bounds<Pixels>>> = Rc::new(Cell::new(Bounds::default()));
254 let bounds_writer = trigger_bounds_cell.clone();
255
256 self.on_prepaint(move |bounds, _, _| {
257 bounds_writer.set(bounds);
258 })
259 .on_hover({
260 let trigger_bounds_cell = trigger_bounds_cell.clone();
261 let build_tooltip = build_tooltip.clone();
262 move |hovered, window, cx| {
263 if let Some(overlay) = Root::tooltip_overlay(window, cx) {
264 if *hovered {
265 let bounds = trigger_bounds_cell.get();
266 overlay.update(cx, |o: &mut BaseTooltipOverlay, cx| {
267 let build = build_tooltip.clone();
268 let request = BaseTooltipRequest::new(bounds, move |window, cx| {
269 build(window, cx)
270 });
271 let request = match preferred_placement {
272 Some(placement) => request.placement(placement),
273 None => request,
274 };
275 o.request_show(request, window, cx);
276 });
277 } else {
278 overlay.update(cx, |o: &mut BaseTooltipOverlay, cx| {
279 o.request_hide(window, cx);
280 });
281 }
282 }
283 }
284 })
285 .on_mouse_down(MouseButton::Left, move |_, window, cx| {
286 if let Some(overlay) = Root::tooltip_overlay(window, cx) {
287 overlay.update(cx, |overlay, cx| {
288 overlay.hide(cx);
289 });
290 }
291 })
292 }
293}
294
295impl<E: StatefulInteractiveElement + crate::ElementExt> ManagedTooltipExt for E {}