armas_basic/components/
hover_card.rs1use crate::components::popover::{Popover, PopoverPosition, PopoverStyle};
7use egui::{Id, Ui};
8
9pub struct HoverCard {
25 id: Id,
26 open_delay: f32,
27 close_delay: f32,
28 position: PopoverPosition,
29 width: Option<f32>,
30}
31
32pub struct HoverCardResponse {
34 pub response: egui::Response,
36 pub is_open: bool,
38}
39
40impl HoverCard {
41 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 #[must_use]
54 pub const fn open_delay(mut self, delay: f32) -> Self {
55 self.open_delay = delay;
56 self
57 }
58
59 #[must_use]
61 pub const fn close_delay(mut self, delay: f32) -> Self {
62 self.close_delay = delay;
63 self
64 }
65
66 #[must_use]
68 pub const fn position(mut self, pos: PopoverPosition) -> Self {
69 self.position = pos;
70 self
71 }
72
73 #[must_use]
75 pub const fn width(mut self, w: f32) -> Self {
76 self.width = Some(w);
77 self
78 }
79
80 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 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 leave_start = None;
121
122 if !is_open {
123 if hover_start.is_none() {
125 hover_start = Some(now);
126 }
127
128 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 hover_start = None;
139
140 if is_open {
141 if leave_start.is_none() {
143 leave_start = Some(now);
144 }
145
146 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 if hover_start.is_some() || leave_start.is_some() {
158 ctx.request_repaint();
159 }
160
161 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}