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