Skip to main content

gpui_component/
hover_card.rs

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/// A hover card element that displays content when hovering over a trigger element.
14///
15/// Similar to Popover but triggered by mouse hover instead of click, with configurable delays
16/// for showing and hiding the content.
17#[derive(IntoElement)]
18pub struct HoverCard {
19    id: ElementId,
20    style: StyleRefinement,
21    anchor: Anchor,
22    trigger: Option<Box<dyn FnOnce(&mut Window, &App) -> AnyElement + 'static>>,
23    content: Option<
24        Rc<
25            dyn Fn(&mut HoverCardState, &mut Window, &mut Context<HoverCardState>) -> AnyElement
26                + 'static,
27        >,
28    >,
29    children: Vec<AnyElement>,
30    open_delay: Duration,
31    close_delay: Duration,
32    appearance: bool,
33    on_open_change: Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>,
34}
35
36impl HoverCard {
37    /// Create a new HoverCard.
38    pub fn new(id: impl Into<ElementId>) -> Self {
39        Self {
40            id: id.into(),
41            style: StyleRefinement::default(),
42            anchor: Anchor::TopCenter,
43            trigger: None,
44            content: None,
45            children: vec![],
46            open_delay: Duration::from_secs_f64(0.6),
47            close_delay: Duration::from_secs_f64(0.3),
48            appearance: true,
49            on_open_change: None,
50        }
51    }
52
53    /// Set the anchor corner of the hover card, default is [`Anchor::TopCenter`].
54    pub fn anchor(mut self, anchor: impl Into<Anchor>) -> Self {
55        self.anchor = anchor.into();
56        self
57    }
58
59    /// Set the trigger element of the hover card.
60    pub fn trigger<T>(mut self, trigger: T) -> Self
61    where
62        T: IntoElement + 'static,
63    {
64        self.trigger = Some(Box::new(|_, _| trigger.into_any_element()));
65        self
66    }
67
68    /// Set the content builder of the hover card.
69    pub fn content<F, E>(mut self, content: F) -> Self
70    where
71        F: Fn(&mut HoverCardState, &mut Window, &mut Context<HoverCardState>) -> E + 'static,
72        E: IntoElement + 'static,
73    {
74        self.content = Some(Rc::new(move |state, window, cx| {
75            content(state, window, cx).into_any_element()
76        }));
77        self
78    }
79
80    /// Set the delay before showing the hover card, default is 600ms.
81    pub fn open_delay(mut self, duration: Duration) -> Self {
82        self.open_delay = duration;
83        self
84    }
85
86    /// Set the delay before hiding the hover card, default is 300ms.
87    pub fn close_delay(mut self, duration: Duration) -> Self {
88        self.close_delay = duration;
89        self
90    }
91
92    /// Set whether to apply default appearance styles, default is `true`.
93    pub fn appearance(mut self, appearance: bool) -> Self {
94        self.appearance = appearance;
95        self
96    }
97
98    /// Set a callback to be called when the open state changes.
99    pub fn on_open_change<F>(mut self, callback: F) -> Self
100    where
101        F: Fn(&bool, &mut Window, &mut App) + 'static,
102    {
103        self.on_open_change = Some(Rc::new(callback));
104        self
105    }
106}
107
108impl Styled for HoverCard {
109    fn style(&mut self) -> &mut StyleRefinement {
110        &mut self.style
111    }
112}
113
114impl ParentElement for HoverCard {
115    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
116        self.children.extend(elements);
117    }
118}
119
120impl RenderOnce for HoverCard {
121    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
122        let Some(trigger) = self.trigger else {
123            return div().id("empty").into_any_element();
124        };
125
126        let anchor = self.anchor;
127        let appearance = self.appearance;
128        let content = self.content;
129        let children = self.children;
130        let style = self.style;
131
132        BaseHoverCard::new(self.id)
133            .anchor(anchor)
134            .open_delay(self.open_delay)
135            .close_delay(self.close_delay)
136            .trigger((trigger)(window, cx))
137            .content(move |state, window, cx| {
138                Popover::render_popover_content(anchor, appearance, window, cx)
139                    .overflow_hidden()
140                    .when_some(content, |this, content| {
141                        this.child((content)(state, window, cx))
142                    })
143                    .children(children)
144                    .refine_style(&style)
145            })
146            .when_some(self.on_open_change, |this, callback| {
147                this.on_open_change(move |open, window, cx| callback(open, window, cx))
148            })
149            .into_any_element()
150    }
151}