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