guise/overlay/
hovercard.rs1use std::time::Duration;
22
23use gpui::prelude::*;
24use gpui::{deferred, div, px, relative, AnyElement, App, Context, IntoElement, Task, Window};
25
26use super::Placement;
27use crate::theme::theme;
28
29type Builder = Box<dyn Fn(&mut Window, &mut App) -> AnyElement + 'static>;
30
31pub struct HoverCard {
33 open: bool,
34 trigger: Builder,
35 content: Builder,
36 placement: Placement,
37 width: Option<f32>,
38 open_delay: Duration,
39 close_delay: Duration,
40 over_trigger: bool,
41 over_card: bool,
42 pending: Option<Task<()>>,
45}
46
47impl HoverCard {
48 pub fn new(
49 _cx: &mut Context<Self>,
50 trigger: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
51 content: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
52 ) -> Self {
53 HoverCard {
54 open: false,
55 trigger: Box::new(trigger),
56 content: Box::new(content),
57 placement: Placement::Bottom,
58 width: None,
59 open_delay: Duration::from_millis(300),
60 close_delay: Duration::from_millis(150),
61 over_trigger: false,
62 over_card: false,
63 pending: None,
64 }
65 }
66
67 pub fn placement(mut self, placement: Placement) -> Self {
68 self.placement = placement;
69 self
70 }
71
72 pub fn width(mut self, width: f32) -> Self {
74 self.width = Some(width);
75 self
76 }
77
78 pub fn open_delay(mut self, delay: Duration) -> Self {
80 self.open_delay = delay;
81 self
82 }
83
84 pub fn close_delay(mut self, delay: Duration) -> Self {
87 self.close_delay = delay;
88 self
89 }
90
91 pub fn is_open(&self) -> bool {
92 self.open
93 }
94
95 pub fn close(&mut self, cx: &mut Context<Self>) {
97 self.pending = None;
98 self.over_trigger = false;
99 self.over_card = false;
100 self.open = false;
101 cx.notify();
102 }
103
104 fn hover_trigger(&mut self, hovered: bool, cx: &mut Context<Self>) {
105 self.over_trigger = hovered;
106 if hovered {
107 self.schedule_open(cx);
108 } else {
109 self.schedule_close(cx);
110 }
111 }
112
113 fn hover_card(&mut self, hovered: bool, cx: &mut Context<Self>) {
114 self.over_card = hovered;
115 if hovered {
116 self.pending = None;
118 } else {
119 self.schedule_close(cx);
120 }
121 }
122
123 fn schedule_open(&mut self, cx: &mut Context<Self>) {
124 if self.open {
125 self.pending = None;
127 return;
128 }
129 let delay = self.open_delay;
130 self.pending = Some(cx.spawn(async move |this, cx| {
131 cx.background_executor().timer(delay).await;
132 this.update(cx, |this, cx| {
133 if this.over_trigger && !this.open {
134 this.open = true;
135 cx.notify();
136 }
137 })
138 .ok();
139 }));
140 }
141
142 fn schedule_close(&mut self, cx: &mut Context<Self>) {
143 if !self.open {
144 self.pending = None;
146 return;
147 }
148 let delay = self.close_delay;
149 self.pending = Some(cx.spawn(async move |this, cx| {
150 cx.background_executor().timer(delay).await;
151 this.update(cx, |this, cx| {
152 if !this.over_trigger && !this.over_card && this.open {
153 this.open = false;
154 cx.notify();
155 }
156 })
157 .ok();
158 }));
159 }
160}
161
162impl Render for HoverCard {
163 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
164 let t = theme(cx);
165 let radius = t.radius(t.default_radius);
166 let surface = t.surface().hsla();
167 let border = t.border().hsla();
168
169 let trigger_el = (self.trigger)(window, cx);
170 let mut wrap = div().relative().child(
171 div()
172 .id("guise-hovercard-trigger")
173 .on_hover(cx.listener(|this, hovered: &bool, _window, cx| {
174 this.hover_trigger(*hovered, cx);
175 }))
176 .child(trigger_el),
177 );
178
179 if self.open {
180 let content_el = (self.content)(window, cx);
181 let base = div()
182 .id("guise-hovercard-card")
183 .on_hover(cx.listener(|this, hovered: &bool, _window, cx| {
184 this.hover_card(*hovered, cx);
185 }))
186 .absolute()
187 .flex()
188 .flex_col()
189 .p(px(12.0))
190 .rounded(px(radius))
191 .border_1()
192 .border_color(border)
193 .bg(surface)
194 .shadow_md()
195 .child(content_el);
196 let placed = match self.placement {
197 Placement::Bottom => base.top(relative(1.0)).mt(px(6.0)).left(px(0.0)),
198 Placement::BottomEnd => base.top(relative(1.0)).mt(px(6.0)).right(px(0.0)),
199 Placement::Top => base.bottom(relative(1.0)).mb(px(6.0)).left(px(0.0)),
200 Placement::TopEnd => base.bottom(relative(1.0)).mb(px(6.0)).right(px(0.0)),
201 };
202 let placed = match self.width {
203 Some(w) => placed.w(px(w)),
204 None => placed.min_w(px(180.0)),
205 };
206 wrap = wrap.child(deferred(placed));
207 }
208
209 wrap
210 }
211}