Skip to main content

armas_basic/components/
hover_card.rs

1//! Hover Card Component (shadcn/ui style)
2//!
3//! A card that appears on hover with configurable open/close delays.
4//! Placeholder — implementation follows.
5
6use crate::components::popover::{Popover, PopoverPosition, PopoverStyle};
7use egui::{Id, Ui};
8
9/// Hover Card — appears on hover over a trigger element.
10///
11/// # Example
12///
13/// ```rust,no_run
14/// # use egui::Ui;
15/// # fn example(ctx: &egui::Context, trigger: &egui::Response) {
16/// use armas_basic::components::HoverCard;
17///
18/// let mut card = HoverCard::new("user_card");
19/// card.show(ctx, trigger, |ui| {
20///     ui.label("User details here");
21/// });
22/// # }
23/// ```
24pub struct HoverCard {
25    id: Id,
26    open_delay: f32,
27    close_delay: f32,
28    position: PopoverPosition,
29    width: Option<f32>,
30}
31
32/// Response from a hover card.
33pub struct HoverCardResponse {
34    /// The UI response.
35    pub response: egui::Response,
36    /// Whether the card is currently open.
37    pub is_open: bool,
38}
39
40impl HoverCard {
41    /// Create a new hover card.
42    pub fn new(id: impl Into<Id>) -> Self {
43        Self {
44            id: id.into(),
45            open_delay: 0.7,
46            close_delay: 0.3,
47            position: PopoverPosition::Bottom,
48            width: None,
49        }
50    }
51
52    /// Set the delay before the card opens on hover (seconds).
53    #[must_use]
54    pub const fn open_delay(mut self, delay: f32) -> Self {
55        self.open_delay = delay;
56        self
57    }
58
59    /// Set the delay before the card closes after hover leaves (seconds).
60    #[must_use]
61    pub const fn close_delay(mut self, delay: f32) -> Self {
62        self.close_delay = delay;
63        self
64    }
65
66    /// Set the card position relative to the trigger.
67    #[must_use]
68    pub const fn position(mut self, pos: PopoverPosition) -> Self {
69        self.position = pos;
70        self
71    }
72
73    /// Set the card width.
74    #[must_use]
75    pub const fn width(mut self, w: f32) -> Self {
76        self.width = Some(w);
77        self
78    }
79
80    /// Show the hover card. Opens when `trigger` is hovered.
81    pub fn show(
82        &mut self,
83        ctx: &egui::Context,
84        trigger: &egui::Response,
85        content: impl FnOnce(&mut Ui),
86    ) -> HoverCardResponse {
87        let theme = crate::ext::ArmasContextExt::armas_theme(ctx);
88
89        let state_id = self.id.with("hover_card_state");
90        let hover_start_id = self.id.with("hover_start");
91        let leave_start_id = self.id.with("leave_start");
92
93        let mut is_open = ctx.data_mut(|d| d.get_temp::<bool>(state_id).unwrap_or(false));
94        let mut hover_start: Option<f64> =
95            ctx.data_mut(|d| d.get_temp(hover_start_id).unwrap_or(None));
96        let mut leave_start: Option<f64> =
97            ctx.data_mut(|d| d.get_temp(leave_start_id).unwrap_or(None));
98
99        let now = ctx.input(|i| i.time);
100        let trigger_hovered = trigger.hovered();
101
102        // Show popover (needed to check if card itself is hovered)
103        let mut popover = Popover::new(self.id.with("popover"))
104            .position(self.position)
105            .style(PopoverStyle::Default);
106        if let Some(w) = self.width {
107            popover = popover.width(w);
108        }
109        popover.set_open(is_open);
110
111        let popover_response = popover.show(ctx, &theme, trigger.rect, |ui| {
112            content(ui);
113        });
114
115        let card_hovered = popover_response.response.hovered();
116        let any_hovered = trigger_hovered || card_hovered;
117
118        if any_hovered {
119            // Something is hovered — cancel any pending close
120            leave_start = None;
121
122            if !is_open {
123                // Start open timer if not already started
124                if hover_start.is_none() {
125                    hover_start = Some(now);
126                }
127
128                // Check if open delay elapsed
129                if let Some(start) = hover_start {
130                    if now - start >= f64::from(self.open_delay) {
131                        is_open = true;
132                        hover_start = None;
133                    }
134                }
135            }
136        } else {
137            // Nothing hovered — cancel any pending open
138            hover_start = None;
139
140            if is_open {
141                // Start close timer if not already started
142                if leave_start.is_none() {
143                    leave_start = Some(now);
144                }
145
146                // Check if close delay elapsed
147                if let Some(start) = leave_start {
148                    if now - start >= f64::from(self.close_delay) {
149                        is_open = false;
150                        leave_start = None;
151                    }
152                }
153            }
154        }
155
156        // Request repaint while timers are active
157        if hover_start.is_some() || leave_start.is_some() {
158            ctx.request_repaint();
159        }
160
161        // Save state
162        ctx.data_mut(|d| {
163            d.insert_temp(state_id, is_open);
164            d.insert_temp(hover_start_id, hover_start);
165            d.insert_temp(leave_start_id, leave_start);
166        });
167
168        HoverCardResponse {
169            response: popover_response.response,
170            is_open,
171        }
172    }
173}