1use std::rc::Rc;
2
3use gpui::{
4 Anchor, AnyElement, App, Context, ElementId, InteractiveElement as _, IntoElement,
5 ParentElement as _, Render, RenderOnce, Stateful, StatefulInteractiveElement as _, Task,
6 Window, div, prelude::FluentBuilder as _,
7};
8use instant::Duration;
9
10use crate::Popup;
11
12type ContentBuilder = Box<
13 dyn FnOnce(
14 &mut HoverCardState,
15 &mut Window,
16 &mut Context<HoverCardState>,
17 ) -> Stateful<gpui::Div>,
18>;
19type OpenChangeHandler = Rc<dyn Fn(&bool, &mut Window, &mut App)>;
20
21#[derive(IntoElement)]
23pub struct HoverCard {
24 id: ElementId,
25 anchor: Anchor,
26 trigger: Option<AnyElement>,
27 content: Option<ContentBuilder>,
28 open_delay: Duration,
29 close_delay: Duration,
30 on_open_change: Option<OpenChangeHandler>,
31}
32
33impl HoverCard {
34 pub fn new(id: impl Into<ElementId>) -> Self {
35 Self {
36 id: id.into(),
37 anchor: Anchor::TopCenter,
38 trigger: None,
39 content: None,
40 open_delay: Duration::from_secs_f64(0.6),
41 close_delay: Duration::from_secs_f64(0.3),
42 on_open_change: None,
43 }
44 }
45
46 pub fn anchor(mut self, anchor: impl Into<Anchor>) -> Self {
47 self.anchor = anchor.into();
48 self
49 }
50
51 pub fn trigger(mut self, trigger: impl IntoElement) -> Self {
52 self.trigger = Some(trigger.into_any_element());
53 self
54 }
55
56 pub fn content<F>(mut self, content: F) -> Self
57 where
58 F: FnOnce(
59 &mut HoverCardState,
60 &mut Window,
61 &mut Context<HoverCardState>,
62 ) -> Stateful<gpui::Div>
63 + 'static,
64 {
65 self.content = Some(Box::new(content));
66 self
67 }
68
69 pub fn open_delay(mut self, duration: Duration) -> Self {
70 self.open_delay = duration;
71 self
72 }
73
74 pub fn close_delay(mut self, duration: Duration) -> Self {
75 self.close_delay = duration;
76 self
77 }
78
79 pub fn on_open_change(
80 mut self,
81 callback: impl Fn(&bool, &mut Window, &mut App) + 'static,
82 ) -> Self {
83 self.on_open_change = Some(Rc::new(callback));
84 self
85 }
86}
87
88pub struct HoverCardState {
90 open: bool,
91 open_delay: Duration,
92 close_delay: Duration,
93 on_open_change: Option<OpenChangeHandler>,
94 open_task: Option<Task<()>>,
95 close_task: Option<Task<()>>,
96 epoch: usize,
97 is_hovering_trigger: bool,
98 is_hovering_content: bool,
99}
100
101impl HoverCardState {
102 fn new(open_delay: Duration, close_delay: Duration) -> Self {
103 Self {
104 open: false,
105 open_delay,
106 close_delay,
107 on_open_change: None,
108 open_task: None,
109 close_task: None,
110 epoch: 0,
111 is_hovering_trigger: false,
112 is_hovering_content: false,
113 }
114 }
115
116 pub fn is_open(&self) -> bool {
117 self.open
118 }
119
120 fn sync(
121 &mut self,
122 open_delay: Duration,
123 close_delay: Duration,
124 on_open_change: Option<OpenChangeHandler>,
125 ) {
126 self.open_delay = open_delay;
127 self.close_delay = close_delay;
128 self.on_open_change = on_open_change;
129 }
130
131 fn schedule_open(&mut self, window: &mut Window, cx: &mut Context<Self>) {
132 self.cancel_tasks();
133 let epoch = self.next_epoch();
134 let delay = self.open_delay;
135 self.open_task = Some(cx.spawn_in(window, async move |this, cx| {
136 cx.background_executor().timer(delay).await;
137 let _ = this.update_in(cx, |state, window, cx| {
138 if state.epoch == epoch {
139 state.set_open(true, window, cx);
140 }
141 });
142 }));
143 }
144
145 fn schedule_close(&mut self, window: &mut Window, cx: &mut Context<Self>) {
146 self.cancel_tasks();
147 let epoch = self.next_epoch();
148 let delay = self.close_delay;
149 self.close_task = Some(cx.spawn_in(window, async move |this, cx| {
150 cx.background_executor().timer(delay).await;
151 let _ = this.update_in(cx, |state, window, cx| {
152 if state.epoch == epoch && !state.is_hovering_trigger && !state.is_hovering_content
153 {
154 state.set_open(false, window, cx);
155 }
156 });
157 }));
158 }
159
160 fn cancel_tasks(&mut self) {
161 self.epoch += 1;
162 self.open_task = None;
163 self.close_task = None;
164 }
165
166 fn next_epoch(&mut self) -> usize {
167 self.epoch += 1;
168 self.epoch
169 }
170
171 fn set_open(&mut self, open: bool, window: &mut Window, cx: &mut Context<Self>) {
172 if self.open == open {
173 return;
174 }
175
176 self.open = open;
177 cx.notify();
178 if let Some(on_open_change) = self.on_open_change.clone() {
182 on_open_change(&open, window, cx);
183 }
184 }
185
186 fn on_trigger_hover(&mut self, hovering: bool, window: &mut Window, cx: &mut Context<Self>) {
187 self.is_hovering_trigger = hovering;
188 if hovering {
189 self.schedule_open(window, cx);
190 } else if !self.is_hovering_content {
191 self.schedule_close(window, cx);
192 }
193 }
194
195 fn on_content_hover(&mut self, hovering: bool, window: &mut Window, cx: &mut Context<Self>) {
196 self.is_hovering_content = hovering;
197 if hovering {
198 self.cancel_tasks();
199 } else if !self.is_hovering_trigger {
200 self.schedule_close(window, cx);
201 }
202 }
203}
204
205impl Render for HoverCardState {
206 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
207 div()
208 }
209}
210
211impl RenderOnce for HoverCard {
212 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
213 let state = window.use_keyed_state(self.id.clone(), cx, |_, _| {
214 HoverCardState::new(self.open_delay, self.close_delay)
215 });
216 state.update(cx, |state, _| {
217 state.sync(self.open_delay, self.close_delay, self.on_open_change)
218 });
219 let open = state.read(cx).is_open();
220
221 let trigger = self.trigger.unwrap_or_else(|| div().into_any_element());
222 let popup = Popup::new(
223 self.id,
224 div().id("trigger").child(trigger).on_hover(
225 window.listener_for(&state, |state, hovered, window, cx| {
226 state.on_trigger_hover(*hovered, window, cx)
227 }),
228 ),
229 )
230 .anchor(self.anchor);
231
232 if !open {
233 return popup;
234 }
235
236 popup.when_some(self.content, |popup, content| {
237 let hover = window.listener_for(&state, |state, hovered, window, cx| {
238 state.on_content_hover(*hovered, window, cx)
239 });
240 popup.content(state.update(cx, |state, cx| content(state, window, cx).on_hover(hover)))
241 })
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use std::cell::RefCell;
248
249 use gpui::{Context, Render, Styled as _, TestAppContext, point, px};
250
251 use super::*;
252
253 #[derive(Default)]
254 struct Harness {
255 open_changes: Rc<RefCell<Vec<bool>>>,
256 }
257
258 impl Render for Harness {
259 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
260 let delay = Duration::from_millis(100);
261 let open_changes = self.open_changes.clone();
262 HoverCard::new("hover-card")
263 .open_delay(delay)
264 .close_delay(delay)
265 .on_open_change(move |open, _, _| open_changes.borrow_mut().push(*open))
266 .trigger(
267 div()
268 .debug_selector(|| "hover-card-trigger".into())
269 .size(px(20.)),
270 )
271 .content(|_, _, _| {
272 div()
273 .id("hover-card-content")
274 .debug_selector(|| "hover-card-content".into())
275 .size(px(10.))
276 })
277 }
278 }
279
280 #[gpui::test]
281 fn public_hover_card_owns_delayed_open_and_close(cx: &mut TestAppContext) {
282 let delay = Duration::from_millis(100);
283 let (_, cx) = cx.add_window_view(|_, _| Harness::default());
284 cx.update(|window, cx| window.draw(cx).clear(cx));
285
286 cx.simulate_mouse_move(point(px(10.), px(10.)), None, gpui::Modifiers::default());
287 cx.executor().advance_clock(delay);
288 cx.run_until_parked();
289 cx.update(|window, cx| {
290 window.draw(cx).clear(cx);
291 window.draw(cx).clear(cx);
292 });
293 assert!(cx.debug_bounds("hover-card-content").is_some());
294
295 cx.simulate_mouse_move(point(px(100.), px(100.)), None, gpui::Modifiers::default());
296 cx.executor().advance_clock(delay);
297 cx.run_until_parked();
298 cx.update(|window, cx| window.draw(cx).clear(cx));
299 assert!(cx.debug_bounds("hover-card-content").is_none());
300 }
301
302 #[gpui::test]
303 fn public_hover_card_reports_each_open_change(cx: &mut TestAppContext) {
304 let delay = Duration::from_millis(100);
305 let open_changes = Rc::new(RefCell::new(Vec::new()));
306 let (_, cx) = cx.add_window_view({
307 let open_changes = open_changes.clone();
308 move |_, _| Harness { open_changes }
309 });
310 cx.update(|window, cx| window.draw(cx).clear(cx));
311
312 cx.simulate_mouse_move(point(px(10.), px(10.)), None, gpui::Modifiers::default());
313 assert_eq!(*open_changes.borrow(), Vec::<bool>::new());
314
315 cx.executor().advance_clock(delay);
316 cx.run_until_parked();
317 cx.update(|window, cx| {
318 window.draw(cx).clear(cx);
319 window.draw(cx).clear(cx);
320 });
321 assert_eq!(*open_changes.borrow(), vec![true]);
322
323 cx.simulate_mouse_move(point(px(100.), px(100.)), None, gpui::Modifiers::default());
324 cx.executor().advance_clock(delay);
325 cx.run_until_parked();
326 cx.update(|window, cx| window.draw(cx).clear(cx));
327 assert_eq!(*open_changes.borrow(), vec![true, false]);
328 }
329}