1use std::rc::Rc;
2
3use gpui::{
4 Anchor, AnyElement, App, Context, ElementId, InteractiveElement as _, IntoElement,
5 ParentElement, RenderOnce, StyleRefinement, Styled, Window, div, prelude::FluentBuilder as _,
6};
7use gpui_base::HoverCard as BaseHoverCard;
8pub use gpui_base::HoverCardState;
9use instant::Duration;
10
11use crate::{StyledExt as _, popover::Popover};
12
13#[derive(IntoElement)]
19pub struct HoverCard {
20 id: ElementId,
21 style: StyleRefinement,
22 trigger_style: Option<StyleRefinement>,
25 anchor: Anchor,
26 trigger: Option<Box<dyn FnOnce(&mut Window, &App) -> AnyElement + 'static>>,
27 content: Option<
28 Rc<
29 dyn Fn(&mut HoverCardState, &mut Window, &mut Context<HoverCardState>) -> AnyElement
30 + 'static,
31 >,
32 >,
33 children: Vec<AnyElement>,
34 open_delay: Duration,
35 close_delay: Duration,
36 appearance: bool,
37 on_open_change: Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>,
38}
39
40impl HoverCard {
41 pub fn new(id: impl Into<ElementId>) -> Self {
43 Self {
44 id: id.into(),
45 style: StyleRefinement::default(),
46 trigger_style: None,
47 anchor: Anchor::TopCenter,
48 trigger: None,
49 content: None,
50 children: vec![],
51 open_delay: Duration::from_secs_f64(0.6),
52 close_delay: Duration::from_secs_f64(0.3),
53 appearance: true,
54 on_open_change: None,
55 }
56 }
57
58 pub fn anchor(mut self, anchor: impl Into<Anchor>) -> Self {
60 self.anchor = anchor.into();
61 self
62 }
63
64 pub fn trigger<T>(mut self, trigger: T) -> Self
66 where
67 T: IntoElement + 'static,
68 {
69 self.trigger = Some(Box::new(|_, _| trigger.into_any_element()));
70 self
71 }
72
73 pub fn trigger_style(mut self, style: StyleRefinement) -> Self {
79 self.trigger_style = Some(style);
80 self
81 }
82
83 pub fn content<F, E>(mut self, content: F) -> Self
85 where
86 F: Fn(&mut HoverCardState, &mut Window, &mut Context<HoverCardState>) -> E + 'static,
87 E: IntoElement + 'static,
88 {
89 self.content = Some(Rc::new(move |state, window, cx| {
90 content(state, window, cx).into_any_element()
91 }));
92 self
93 }
94
95 pub fn open_delay(mut self, duration: Duration) -> Self {
97 self.open_delay = duration;
98 self
99 }
100
101 pub fn close_delay(mut self, duration: Duration) -> Self {
103 self.close_delay = duration;
104 self
105 }
106
107 pub fn appearance(mut self, appearance: bool) -> Self {
109 self.appearance = appearance;
110 self
111 }
112
113 pub fn on_open_change<F>(mut self, callback: F) -> Self
115 where
116 F: Fn(&bool, &mut Window, &mut App) + 'static,
117 {
118 self.on_open_change = Some(Rc::new(callback));
119 self
120 }
121}
122
123impl Styled for HoverCard {
124 fn style(&mut self) -> &mut StyleRefinement {
125 &mut self.style
126 }
127}
128
129impl ParentElement for HoverCard {
130 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
131 self.children.extend(elements);
132 }
133}
134
135impl RenderOnce for HoverCard {
136 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
137 let Some(trigger) = self.trigger else {
138 return div().id("empty").into_any_element();
139 };
140
141 let anchor = self.anchor;
142 let appearance = self.appearance;
143 let content = self.content;
144 let children = self.children;
145 let style = self.style;
146 let trigger_style = self.trigger_style;
147
148 BaseHoverCard::new(self.id)
149 .anchor(anchor)
150 .open_delay(self.open_delay)
151 .close_delay(self.close_delay)
152 .trigger((trigger)(window, cx))
153 .content(move |state, window, cx| {
154 Popover::render_popover_content(anchor, appearance, window, cx)
155 .overflow_hidden()
156 .when_some(content, |this, content| {
157 this.child((content)(state, window, cx))
158 })
159 .children(children)
160 .refine_style(&style)
161 })
162 .when_some(self.on_open_change, |this, callback| {
163 this.on_open_change(move |open, window, cx| callback(open, window, cx))
164 })
165 .when_some(trigger_style, |this, style| this.refine_style(&style))
166 .into_any_element()
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173 use crate::{button::Button, h_flex};
174 use gpui::{Context, Render, point, px};
175
176 struct TriggerStyleHarness {
177 styled: bool,
178 }
179
180 impl Render for TriggerStyleHarness {
181 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
182 div().size_full().child(
185 h_flex()
186 .absolute()
187 .left(px(100.))
188 .top(px(100.))
189 .w(px(200.))
190 .child(
191 HoverCard::new("trigger-style-hover-card")
192 .appearance(false)
193 .open_delay(Duration::ZERO)
194 .anchor(Anchor::TopRight)
195 .when(self.styled, |this| {
196 this.trigger_style(StyleRefinement::default().w_full())
197 })
198 .trigger(Button::new("styled-trigger").size(px(40.)))
199 .child(
200 div()
201 .debug_selector(|| "styled-content".into())
202 .size(px(20.)),
203 ),
204 ),
205 )
206 }
207 }
208
209 #[gpui::test]
213 fn trigger_style_is_applied_to_the_trigger_container(cx: &mut gpui::TestAppContext) {
214 cx.update(crate::init);
215 let (view, cx) = cx.add_window_view(|_, _| TriggerStyleHarness { styled: false });
216 cx.update(|window, cx| window.draw(cx).clear(cx));
217 cx.simulate_mouse_move(point(px(110.), px(110.)), None, Default::default());
218 cx.run_until_parked();
219 cx.update(|window, cx| {
220 window.draw(cx).clear(cx);
221 window.draw(cx).clear(cx);
222 });
223 assert_eq!(
226 cx.debug_bounds("styled-content").unwrap().top_right().x,
227 px(140.)
228 );
229
230 cx.update(|window, cx| {
231 view.update(cx, |view, cx| {
232 view.styled = true;
233 cx.notify();
234 });
235 window.draw(cx).clear(cx);
236 window.draw(cx).clear(cx);
237 });
238 assert_eq!(
241 cx.debug_bounds("styled-content").unwrap().top_right().x,
242 px(300.)
243 );
244 }
245}