1use crate::ext::ArmasContextExt;
7use egui::{pos2, vec2, Color32, FontId, Rect, Response, Shape, Stroke, Ui, Vec2};
8
9const CORNER_RADIUS: f32 = 6.0; const PADDING_X: f32 = 12.0; const PADDING_Y: f32 = 6.0; const ARROW_SIZE: f32 = 5.0; #[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum TooltipPosition {
19 Top,
21 Bottom,
23 Left,
25 Right,
27}
28
29pub struct Tooltip {
43 text: String,
44 position: Option<TooltipPosition>,
45 max_width: f32,
46 delay_ms: u64,
47 show_arrow: bool,
48}
49
50impl Tooltip {
51 pub fn new(text: impl Into<String>) -> Self {
53 Self {
54 text: text.into(),
55 position: None, max_width: 300.0,
57 delay_ms: 0, show_arrow: true,
59 }
60 }
61
62 #[must_use]
64 pub const fn position(mut self, position: TooltipPosition) -> Self {
65 self.position = Some(position);
66 self
67 }
68
69 #[must_use]
71 pub const fn max_width(mut self, width: f32) -> Self {
72 self.max_width = width;
73 self
74 }
75
76 #[must_use]
78 pub const fn delay(mut self, delay_ms: u64) -> Self {
79 self.delay_ms = delay_ms;
80 self
81 }
82
83 #[must_use]
85 pub const fn arrow(mut self, show: bool) -> Self {
86 self.show_arrow = show;
87 self
88 }
89
90 pub fn show(self, ui: &mut Ui, target_response: &Response) -> bool {
92 let theme = ui.ctx().armas_theme();
93 let is_hovered = target_response.hovered();
94
95 let hover_id = target_response.id.with("tooltip_hover");
97 let current_time = ui.ctx().input(|i| i.time);
98
99 let hover_start: Option<f64> = ui.ctx().data(|d| d.get_temp(hover_id));
100
101 if is_hovered {
102 if hover_start.is_none() {
104 ui.ctx().data_mut(|d| d.insert_temp(hover_id, current_time));
105 if self.delay_ms > 0 {
106 ui.ctx().request_repaint();
107 return false;
108 }
109 }
110
111 if let Some(start) = hover_start {
113 let elapsed_ms = ((current_time - start) * 1000.0) as u64;
114 if elapsed_ms < self.delay_ms {
115 ui.ctx().request_repaint();
116 return false;
117 }
118 }
119 } else {
120 ui.ctx().data_mut(|d| d.remove::<f64>(hover_id));
122 return false;
123 }
124
125 let font_id = FontId::proportional(theme.typography.sm);
127 let padding = vec2(PADDING_X, PADDING_Y);
128
129 let bg_color = theme.foreground();
131 let text_color = theme.background();
132
133 let text_galley = ui.painter().layout(
134 self.text.clone(),
135 font_id,
136 text_color,
137 self.max_width - padding.x * 2.0,
138 );
139
140 let text_size = text_galley.size();
141 let tooltip_size = text_size + padding * 2.0;
142 let arrow_offset = if self.show_arrow {
143 ARROW_SIZE + 2.0
144 } else {
145 4.0
146 };
147
148 let target_rect = target_response.rect;
150 let position = self.determine_position(ui, target_rect, tooltip_size, arrow_offset);
151 let tooltip_rect =
152 self.calculate_tooltip_rect(target_rect, tooltip_size, arrow_offset, position);
153
154 let layer_id = egui::LayerId::new(
156 egui::Order::Tooltip,
157 target_response.id.with("tooltip_layer"),
158 );
159 let painter = ui.ctx().layer_painter(layer_id);
160
161 painter.rect_filled(tooltip_rect, CORNER_RADIUS, bg_color);
163
164 if self.show_arrow {
166 self.draw_arrow(&painter, bg_color, target_rect, tooltip_rect, position);
167 }
168
169 painter.galley(tooltip_rect.min + padding, text_galley, text_color);
171
172 true
173 }
174
175 fn determine_position(
177 &self,
178 ui: &Ui,
179 target_rect: Rect,
180 tooltip_size: Vec2,
181 arrow_offset: f32,
182 ) -> TooltipPosition {
183 if let Some(pos) = self.position {
184 return pos;
185 }
186
187 let screen_rect = ui.clip_rect();
188 let spacing = arrow_offset;
189
190 let space_above = target_rect.top() - screen_rect.top();
192 let space_below = screen_rect.bottom() - target_rect.bottom();
193 let space_left = target_rect.left() - screen_rect.left();
194 let space_right = screen_rect.right() - target_rect.right();
195
196 let needed_vertical = tooltip_size.y + spacing;
197 let needed_horizontal = tooltip_size.x + spacing;
198
199 if space_above >= needed_vertical {
201 TooltipPosition::Top
202 } else if space_below >= needed_vertical {
203 TooltipPosition::Bottom
204 } else if space_right >= needed_horizontal {
205 TooltipPosition::Right
206 } else if space_left >= needed_horizontal {
207 TooltipPosition::Left
208 } else {
209 TooltipPosition::Top
210 }
211 }
212
213 fn calculate_tooltip_rect(
215 &self,
216 target_rect: Rect,
217 tooltip_size: Vec2,
218 arrow_offset: f32,
219 position: TooltipPosition,
220 ) -> Rect {
221 let target_center_x = target_rect.center().x;
222 let target_center_y = target_rect.center().y;
223
224 let min_pos = match position {
225 TooltipPosition::Top => pos2(
226 target_center_x - tooltip_size.x / 2.0,
227 target_rect.top() - tooltip_size.y - arrow_offset,
228 ),
229 TooltipPosition::Bottom => pos2(
230 target_center_x - tooltip_size.x / 2.0,
231 target_rect.bottom() + arrow_offset,
232 ),
233 TooltipPosition::Left => pos2(
234 target_rect.left() - tooltip_size.x - arrow_offset,
235 target_center_y - tooltip_size.y / 2.0,
236 ),
237 TooltipPosition::Right => pos2(
238 target_rect.right() + arrow_offset,
239 target_center_y - tooltip_size.y / 2.0,
240 ),
241 };
242
243 Rect::from_min_size(min_pos, tooltip_size)
244 }
245
246 fn draw_arrow(
248 &self,
249 painter: &egui::Painter,
250 bg_color: Color32,
251 _target_rect: Rect,
252 tooltip_rect: Rect,
253 position: TooltipPosition,
254 ) {
255 let size = ARROW_SIZE;
256
257 let (tip, base1, base2) = match position {
258 TooltipPosition::Top => {
259 let tip = pos2(tooltip_rect.center().x, tooltip_rect.bottom() + size);
261 let base1 = pos2(tip.x - size, tooltip_rect.bottom());
262 let base2 = pos2(tip.x + size, tooltip_rect.bottom());
263 (tip, base1, base2)
264 }
265 TooltipPosition::Bottom => {
266 let tip = pos2(tooltip_rect.center().x, tooltip_rect.top() - size);
268 let base1 = pos2(tip.x - size, tooltip_rect.top());
269 let base2 = pos2(tip.x + size, tooltip_rect.top());
270 (tip, base1, base2)
271 }
272 TooltipPosition::Left => {
273 let tip = pos2(tooltip_rect.right() + size, tooltip_rect.center().y);
275 let base1 = pos2(tooltip_rect.right(), tip.y - size);
276 let base2 = pos2(tooltip_rect.right(), tip.y + size);
277 (tip, base1, base2)
278 }
279 TooltipPosition::Right => {
280 let tip = pos2(tooltip_rect.left() - size, tooltip_rect.center().y);
282 let base1 = pos2(tooltip_rect.left(), tip.y - size);
283 let base2 = pos2(tooltip_rect.left(), tip.y + size);
284 (tip, base1, base2)
285 }
286 };
287
288 painter.add(Shape::convex_polygon(
290 vec![tip, base1, base2],
291 bg_color,
292 Stroke::NONE,
293 ));
294 }
295}
296
297pub fn tooltip(ui: &mut Ui, response: &Response, text: impl Into<String>) {
311 Tooltip::new(text).show(ui, response);
312}
313
314pub fn tooltip_with(
330 ui: &mut Ui,
331 response: &Response,
332 text: impl Into<String>,
333 configure: impl FnOnce(Tooltip) -> Tooltip,
334) {
335 configure(Tooltip::new(text)).show(ui, response);
336}
337
338#[doc(hidden)]
340pub type TooltipStyle = ();
341#[doc(hidden)]
342pub type TooltipColor = ();