1use std::{cell::Cell, rc::Rc};
2
3use gpui_kit::{
4 AnyElement, AnyView, App, AppContext as _, Bounds, Context, Div, ElementId,
5 InteractiveElement as _, IntoElement, MouseButton, ParentElement as _, Pixels, Render,
6 SharedString, StatefulInteractiveElement, Styled as _, Window,
7 base::{
8 ElementExt as _, Keyframe, Keyframes, Placement, TestSupportExt as _, Timing,
9 Tooltip as BaseTooltip, TooltipRequest, TooltipTransition, animate_keyframes,
10 },
11 div,
12};
13
14use crate::{ActiveTheme as _, Theme, TooltipHost};
15use gpui_kit::{StyledText, TextLayout};
16
17pub(crate) struct TruncatedLabel {
22 label: SharedString,
23 text: StyledText,
24}
25
26impl TruncatedLabel {
27 pub(crate) fn new(label: SharedString) -> Self {
28 Self {
29 text: StyledText::new(label.clone()),
30 label,
31 }
32 }
33
34 pub(crate) fn into_parts(self) -> (StyledText, TruncationCheck) {
36 let layout = self.text.layout().clone();
37 (
38 self.text,
39 TruncationCheck {
40 label: self.label,
41 layout,
42 },
43 )
44 }
45}
46
47#[derive(Clone)]
49pub(crate) struct TruncationCheck {
50 label: SharedString,
51 layout: TextLayout,
52}
53
54impl TruncationCheck {
55 pub(crate) fn is_truncated(&self) -> bool {
56 self.layout.text() != self.label.as_ref()
57 }
58
59 pub(crate) fn hovered(
63 &self,
64 trigger: &TooltipTrigger,
65 hovered: bool,
66 window: &mut Window,
67 cx: &mut App,
68 ) {
69 if self.is_truncated() || !hovered {
70 trigger.hovered(hovered, window, cx);
71 }
72 }
73}
74
75thread_local! {
76 static RAMP: Keyframes<f32> =
79 Keyframes::try_new([Keyframe::new(0., 0f32), Keyframe::new(1., 1f32)])
80 .expect("two ordered stops");
81}
82
83pub struct Tooltip {
89 text: SharedString,
90}
91
92impl Tooltip {
93 pub fn new(text: impl Into<SharedString>) -> Self {
95 Self { text: text.into() }
96 }
97
98 pub fn build(text: impl Into<SharedString>, cx: &mut App) -> AnyView {
100 let text = text.into();
101 cx.new(|_| Self::new(text)).into()
102 }
103}
104
105impl Render for Tooltip {
106 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
107 div()
109 .id("gpui-cn-tooltip")
110 .test_support()
111 .child(surface(cx).child(self.text.clone()))
112 }
113}
114
115pub fn surface(cx: &App) -> Div {
119 let theme = cx.theme();
120 div()
121 .flex()
122 .items_center()
123 .h(theme.metrics.row)
124 .px(theme.text_control.size)
125 .rounded(theme.radius_lg())
126 .bg(theme.tooltip)
127 .text_color(theme.tooltip_foreground)
128 .border_1()
129 .border_color(theme.border())
130 .text_size(theme.text_control.size)
131 .whitespace_nowrap()
132}
133
134pub(crate) fn render_surface(
140 view: AnyView,
141 transition: TooltipTransition,
142 window: &mut Window,
143 cx: &mut App,
144) -> AnyElement {
145 let normal = Theme::global(cx).motion.normal;
146 let opacity = match transition {
147 TooltipTransition::Enter { epoch } if !normal.is_zero() => RAMP.with(|ramp| {
148 animate_keyframes(
149 ElementId::from(("gpui-cn-tooltip-enter", epoch)),
150 ramp,
151 Timing::new(normal),
152 window,
153 cx,
154 )
155 .value
156 }),
157 _ => 1.,
158 };
159 BaseTooltip::new("gpui-cn-tooltip-positioner")
160 .opacity(opacity)
161 .child(view)
162 .into_any_element()
163}
164
165pub struct TooltipTrigger {
174 owner: ElementId,
175 build: Rc<dyn Fn(&mut App) -> AnyView>,
176 placement: Option<Placement>,
177 bounds: Rc<Cell<Bounds<Pixels>>>,
178}
179
180impl TooltipTrigger {
181 pub fn new(
185 owner: ElementId,
186 placement: Option<Placement>,
187 build: impl Fn(&mut App) -> AnyView + 'static,
188 ) -> Self {
189 Self {
190 owner,
191 build: Rc::new(build),
192 placement,
193 bounds: Rc::new(Cell::new(Bounds::default())),
194 }
195 }
196
197 pub fn text(owner: ElementId, placement: Option<Placement>, text: SharedString) -> Self {
199 Self::new(owner, placement, move |cx| Tooltip::build(text.clone(), cx))
200 }
201
202 pub fn track_bounds(&self) -> impl Fn(Bounds<Pixels>, &mut Window, &mut App) + 'static {
204 let bounds = self.bounds.clone();
205 move |resolved, _, _| bounds.set(resolved)
206 }
207
208 pub fn hovered(&self, hovered: bool, window: &mut Window, cx: &mut App) {
210 let Some(overlay) = TooltipHost::overlay(window, cx) else {
211 return;
212 };
213 if hovered {
214 let build = self.build.clone();
215 let request = TooltipRequest::new(self.bounds.get(), move |_, cx| build(cx));
216 let request = match self.placement {
217 Some(placement) => request.with_placement(placement),
218 None => request,
219 };
220 TooltipHost::note_requester(window, &self.owner, cx);
221 overlay.update(cx, |overlay, cx| overlay.request_show(request, window, cx));
222 } else if TooltipHost::is_requester(window, &self.owner, cx) {
223 overlay.update(cx, |overlay, cx| overlay.request_hide(window, cx));
224 }
225 }
226
227 pub fn pressed(window: &mut Window, cx: &mut App) {
229 if let Some(overlay) = TooltipHost::overlay(window, cx) {
230 overlay.update(cx, |overlay, cx| overlay.hide(cx));
231 }
232 }
233
234 pub fn native(&self) -> impl Fn(&mut Window, &mut App) -> AnyView + 'static {
236 let build = self.build.clone();
237 move |_, cx| build(cx)
238 }
239}
240
241pub trait TooltipExt: StatefulInteractiveElement + Sized {
254 fn managed_tooltip(self, text: impl Into<SharedString>, window: &Window, cx: &App) -> Self {
256 let text = text.into();
257 self.managed_tooltip_with(None, move |cx| Tooltip::build(text.clone(), cx), window, cx)
258 }
259
260 fn managed_tooltip_at(
263 self,
264 placement: Placement,
265 text: impl Into<SharedString>,
266 window: &Window,
267 cx: &App,
268 ) -> Self {
269 let text = text.into();
270 self.managed_tooltip_with(
271 Some(placement),
272 move |cx| Tooltip::build(text.clone(), cx),
273 window,
274 cx,
275 )
276 }
277
278 fn managed_tooltip_with(
280 self,
281 placement: Option<Placement>,
282 build: impl Fn(&mut App) -> AnyView + 'static,
283 window: &Window,
284 cx: &App,
285 ) -> Self;
286}
287
288impl<E: StatefulInteractiveElement + gpui_kit::ParentElement> TooltipExt for E {
289 fn managed_tooltip_with(
290 mut self,
291 placement: Option<Placement>,
292 build: impl Fn(&mut App) -> AnyView + 'static,
293 window: &Window,
294 cx: &App,
295 ) -> Self {
296 let owner = self
297 .interactivity()
298 .element_id
299 .clone()
300 .expect("a stateful element has an id");
301 let trigger = TooltipTrigger::new(owner, placement, build);
302 if TooltipHost::overlay(window, cx).is_none() {
303 return self.tooltip(trigger.native());
304 }
305 self.on_prepaint(trigger.track_bounds())
306 .on_hover(move |hovered, window, cx| trigger.hovered(*hovered, window, cx))
307 .on_mouse_down(MouseButton::Left, |_, window, cx| {
308 TooltipTrigger::pressed(window, cx);
309 })
310 }
311}