1use std::cell::RefCell;
19use std::rc::Rc;
20use std::sync::Arc;
21
22use crate::color::Color;
23use crate::draw_ctx::DrawCtx;
24use crate::event::{Event, EventResult};
25use crate::geometry::{Point, Rect, Size};
26use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
27use crate::text::Font;
28use crate::widget::{current_mouse_world, Widget};
29
30const HOVER_DELAY_FRAMES: u32 = 18;
33const TOOLTIP_FONT_SIZE: f64 = 12.0;
34const TOOLTIP_PAD_X: f64 = 8.0;
35const TOOLTIP_PAD_Y: f64 = 6.0;
36const TOOLTIP_GAP: f64 = 4.0;
37const SCREEN_MARGIN: f64 = 4.0;
38
39#[derive(Clone)]
40enum TooltipLineKind {
41 Text,
42 Code,
43 Link,
44}
45
46#[derive(Clone)]
47struct TooltipLine {
48 text: String,
49 kind: TooltipLineKind,
50}
51
52struct TooltipRequest {
53 font: Arc<Font>,
54 lines: Vec<TooltipLine>,
55 anchor: Point,
56 at_pointer: bool,
57}
58
59thread_local! {
60 static TOOLTIP_QUEUE: RefCell<Vec<TooltipRequest>> = const { RefCell::new(Vec::new()) };
61}
62
63pub struct Tooltip {
65 bounds: Rect,
66 children: Vec<Box<dyn Widget>>,
68 base: WidgetBase,
69
70 hover_frames: u32,
72 hovered: bool,
74 cursor: Point,
76
77 font: Arc<Font>,
78 lines: Vec<TooltipLine>,
79 disabled_lines: Vec<TooltipLine>,
80 disabled_when: Option<Rc<dyn Fn() -> bool>>,
81 at_pointer: bool,
82}
83
84impl Tooltip {
85 pub fn new(child: Box<dyn Widget>, text: impl Into<String>, font: Arc<Font>) -> Self {
87 Self {
88 bounds: Rect::default(),
89 children: vec![child],
90 base: WidgetBase::new(),
91 hover_frames: 0,
92 hovered: false,
93 cursor: Point::ORIGIN,
94 font,
95 lines: text_to_lines(text),
96 disabled_lines: Vec::new(),
97 disabled_when: None,
98 at_pointer: false,
99 }
100 }
101
102 pub fn with_text(mut self, text: impl Into<String>) -> Self {
105 self.lines.extend(text_to_lines(text));
106 self
107 }
108
109 pub fn with_code_line(mut self, text: impl Into<String>) -> Self {
111 self.lines.push(TooltipLine {
112 text: text.into(),
113 kind: TooltipLineKind::Code,
114 });
115 self
116 }
117
118 pub fn with_link_line(mut self, text: impl Into<String>) -> Self {
122 self.lines.push(TooltipLine {
123 text: text.into(),
124 kind: TooltipLineKind::Link,
125 });
126 self
127 }
128
129 pub fn at_pointer(mut self) -> Self {
131 self.at_pointer = true;
132 self
133 }
134
135 pub fn with_disabled_text(
137 mut self,
138 text: impl Into<String>,
139 disabled_when: impl Fn() -> bool + 'static,
140 ) -> Self {
141 self.disabled_lines = text_to_lines(text);
142 self.disabled_when = Some(Rc::new(disabled_when));
143 self
144 }
145
146 pub fn with_margin(mut self, m: Insets) -> Self {
147 self.base.margin = m;
148 self
149 }
150 pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
151 self.base.h_anchor = h;
152 self
153 }
154 pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
155 self.base.v_anchor = v;
156 self
157 }
158
159 fn show_tip(&self) -> bool {
160 self.hovered && self.hover_frames >= HOVER_DELAY_FRAMES
161 }
162
163 fn active_lines(&self) -> Vec<TooltipLine> {
164 if self.disabled_when.as_ref().map(|f| f()).unwrap_or(false)
165 && !self.disabled_lines.is_empty()
166 {
167 self.disabled_lines.clone()
168 } else {
169 self.lines.clone()
170 }
171 }
172}
173
174impl Widget for Tooltip {
175 fn type_name(&self) -> &'static str {
176 "Tooltip"
177 }
178 fn bounds(&self) -> Rect {
179 self.bounds
180 }
181 fn set_bounds(&mut self, b: Rect) {
182 self.bounds = b;
183 }
184 fn children(&self) -> &[Box<dyn Widget>] {
185 &self.children
186 }
187 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
188 &mut self.children
189 }
190
191 fn margin(&self) -> Insets {
192 self.base.margin
193 }
194 fn widget_base(&self) -> Option<&WidgetBase> {
195 Some(&self.base)
196 }
197 fn widget_base_mut(&mut self) -> Option<&mut WidgetBase> {
198 Some(&mut self.base)
199 }
200 fn h_anchor(&self) -> HAnchor {
201 self.base.h_anchor
202 }
203 fn v_anchor(&self) -> VAnchor {
204 self.base.v_anchor
205 }
206
207 fn is_focusable(&self) -> bool {
208 self.children
209 .first()
210 .map(|c| c.is_focusable())
211 .unwrap_or(false)
212 }
213
214 fn layout(&mut self, available: Size) -> Size {
215 let s = if let Some(child) = self.children.first_mut() {
216 let cs = child.layout(available);
217 child.set_bounds(Rect::new(0.0, 0.0, cs.width, cs.height));
218 cs
219 } else {
220 available
221 };
222 self.bounds = Rect::new(0.0, 0.0, s.width, s.height);
223 s
224 }
225
226 fn paint(&mut self, _: &mut dyn DrawCtx) {}
227
228 fn paint_overlay(&mut self, ctx: &mut dyn DrawCtx) {
229 if self.hovered {
230 self.hover_frames = self.hover_frames.saturating_add(1);
231 if !self.show_tip() {
232 crate::animation::request_draw();
233 }
234 }
235
236 if !self.show_tip() {
237 return;
238 }
239
240 let mut anchor = if self.at_pointer {
241 current_mouse_world().unwrap_or(self.cursor)
242 } else {
243 let mut x = self.bounds.width * 0.5;
244 let mut y = self.bounds.height;
245 ctx.root_transform().transform(&mut x, &mut y);
246 Point::new(x, y)
247 };
248 if self.at_pointer {
249 anchor.x += 14.0;
250 anchor.y += 14.0;
251 }
252
253 submit_tooltip(TooltipRequest {
254 font: Arc::clone(&self.font),
255 lines: self.active_lines(),
256 anchor,
257 at_pointer: self.at_pointer,
258 });
259 }
260
261 fn on_event(&mut self, event: &Event) -> EventResult {
262 match event {
263 Event::MouseMove { pos } => {
264 let was = self.hovered;
265 self.hovered = self.hit_test(*pos);
266 self.cursor = *pos;
267 if !self.hovered {
268 self.hover_frames = 0;
269 }
270 if self.hovered != was {
271 crate::animation::request_draw();
272 }
273 EventResult::Ignored
274 }
275 Event::MouseWheel { .. } => {
276 self.hovered = false;
277 self.hover_frames = 0;
278 EventResult::Ignored
279 }
280 _ => EventResult::Ignored,
281 }
282 }
283
284 fn hit_test(&self, local_pos: Point) -> bool {
285 local_pos.x >= 0.0
286 && local_pos.x <= self.bounds.width
287 && local_pos.y >= 0.0
288 && local_pos.y <= self.bounds.height
289 }
290}
291
292fn text_to_lines(text: impl Into<String>) -> Vec<TooltipLine> {
293 text.into()
294 .lines()
295 .map(|line| TooltipLine {
296 text: line.to_owned(),
297 kind: TooltipLineKind::Text,
298 })
299 .collect()
300}
301
302fn submit_tooltip(request: TooltipRequest) {
303 TOOLTIP_QUEUE.with(|q| q.borrow_mut().push(request));
304}
305
306pub(crate) fn begin_tooltip_frame() {
307 TOOLTIP_QUEUE.with(|q| q.borrow_mut().clear());
308}
309
310pub(crate) fn paint_global_tooltips(ctx: &mut dyn DrawCtx, viewport: Size) {
311 let requests = TOOLTIP_QUEUE.with(|q| q.borrow_mut().drain(..).collect::<Vec<_>>());
312 for request in requests {
313 paint_request(ctx, viewport, request);
314 }
315}
316
317fn paint_request(ctx: &mut dyn DrawCtx, viewport: Size, request: TooltipRequest) {
318 if request.lines.is_empty() {
319 return;
320 }
321
322 let v = ctx.visuals();
323 ctx.set_font(Arc::clone(&request.font));
324 ctx.set_font_size(TOOLTIP_FONT_SIZE);
325
326 let line_h = TOOLTIP_FONT_SIZE * 1.45;
327 let mut max_w = 0.0_f64;
328 for line in &request.lines {
329 if let Some(m) = ctx.measure_text(&line.text) {
330 max_w = max_w.max(m.width);
331 }
332 }
333
334 let panel_w = (max_w + TOOLTIP_PAD_X * 2.0).max(64.0);
335 let panel_h = request.lines.len() as f64 * line_h + TOOLTIP_PAD_Y * 2.0;
336 let mut panel_x = if request.at_pointer {
337 request.anchor.x
338 } else {
339 request.anchor.x - panel_w * 0.5
340 };
341 let mut panel_y = request.anchor.y + TOOLTIP_GAP;
342
343 if panel_x + panel_w > viewport.width - SCREEN_MARGIN {
344 panel_x = viewport.width - panel_w - SCREEN_MARGIN;
345 }
346 if panel_y + panel_h > viewport.height - SCREEN_MARGIN {
347 panel_y = request.anchor.y - panel_h - TOOLTIP_GAP * 3.0;
348 }
349 panel_x = panel_x.clamp(
350 SCREEN_MARGIN,
351 (viewport.width - panel_w - SCREEN_MARGIN).max(SCREEN_MARGIN),
352 );
353 panel_y = panel_y.clamp(
354 SCREEN_MARGIN,
355 (viewport.height - panel_h - SCREEN_MARGIN).max(SCREEN_MARGIN),
356 );
357
358 ctx.set_fill_color(Color::rgba(0.0, 0.0, 0.0, 0.20));
359 ctx.begin_path();
360 ctx.rounded_rect(panel_x + 1.0, panel_y - 1.0, panel_w, panel_h, 5.0);
361 ctx.fill();
362
363 ctx.set_fill_color(v.window_fill);
364 ctx.begin_path();
365 ctx.rounded_rect(panel_x, panel_y, panel_w, panel_h, 5.0);
366 ctx.fill();
367
368 ctx.set_stroke_color(v.widget_stroke);
369 ctx.set_line_width(1.0);
370 ctx.begin_path();
371 ctx.rounded_rect(panel_x, panel_y, panel_w, panel_h, 5.0);
372 ctx.stroke();
373
374 for (i, line) in request.lines.iter().enumerate() {
375 let y = panel_y + panel_h - TOOLTIP_PAD_Y - (i as f64 + 1.0) * line_h + 2.0;
376 match line.kind {
377 TooltipLineKind::Text => {
378 ctx.set_fill_color(v.text_color);
379 ctx.fill_text(&line.text, panel_x + TOOLTIP_PAD_X, y);
380 }
381 TooltipLineKind::Code => {
382 if let Some(m) = ctx.measure_text(&line.text) {
383 ctx.set_fill_color(v.track_bg);
384 ctx.begin_path();
385 ctx.rounded_rect(
386 panel_x + TOOLTIP_PAD_X - 3.0,
387 y - 3.0,
388 m.width + 6.0,
389 line_h,
390 3.0,
391 );
392 ctx.fill();
393 }
394 ctx.set_fill_color(v.text_color);
395 ctx.fill_text(&line.text, panel_x + TOOLTIP_PAD_X, y);
396 }
397 TooltipLineKind::Link => {
398 ctx.set_fill_color(v.text_link);
399 ctx.fill_text(&line.text, panel_x + TOOLTIP_PAD_X, y);
400 if let Some(m) = ctx.measure_text(&line.text) {
401 ctx.set_stroke_color(v.text_link);
402 ctx.set_line_width(1.0);
403 ctx.begin_path();
404 ctx.move_to(panel_x + TOOLTIP_PAD_X, y - 2.0);
405 ctx.line_to(panel_x + TOOLTIP_PAD_X + m.width, y - 2.0);
406 ctx.stroke();
407 }
408 }
409 }
410 }
411}