agg_gui/widgets/
tooltip.rs1use std::sync::Arc;
26
27use crate::color::Color;
28use crate::draw_ctx::DrawCtx;
29use crate::event::{Event, EventResult};
30use crate::geometry::{Point, Rect, Size};
31use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
32use crate::text::Font;
33use crate::widget::{Widget, paint_subtree};
34use crate::widgets::label::Label;
35
36const HOVER_DELAY_FRAMES: u32 = 30;
39
40pub struct Tooltip {
45 bounds: Rect,
46 children: Vec<Box<dyn Widget>>,
48 base: WidgetBase,
49
50 hover_frames: u32,
52 hovered: bool,
54 cursor: Point,
56
57 tip_label: Label,
59}
60
61impl Tooltip {
62 pub fn new(child: Box<dyn Widget>, text: impl Into<String>, font: Arc<Font>) -> Self {
64 Self {
65 bounds: Rect::default(),
66 children: vec![child],
67 base: WidgetBase::new(),
68 hover_frames: 0,
69 hovered: false,
70 cursor: Point::ORIGIN,
71 tip_label: Label::new(text, font).with_font_size(11.5),
72 }
73 }
74
75 pub fn with_margin(mut self, m: Insets) -> Self { self.base.margin = m; self }
76 pub fn with_h_anchor(mut self, h: HAnchor) -> Self { self.base.h_anchor = h; self }
77 pub fn with_v_anchor(mut self, v: VAnchor) -> Self { self.base.v_anchor = v; self }
78
79 fn show_tip(&self) -> bool {
80 self.hovered && self.hover_frames >= HOVER_DELAY_FRAMES
81 }
82}
83
84impl Widget for Tooltip {
85 fn type_name(&self) -> &'static str { "Tooltip" }
86 fn bounds(&self) -> Rect { self.bounds }
87 fn set_bounds(&mut self, b: Rect) { self.bounds = b; }
88 fn children(&self) -> &[Box<dyn Widget>] { &self.children }
89 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> { &mut self.children }
90
91 fn margin(&self) -> Insets { self.base.margin }
92 fn h_anchor(&self) -> HAnchor { self.base.h_anchor }
93 fn v_anchor(&self) -> VAnchor { self.base.v_anchor }
94
95 fn is_focusable(&self) -> bool {
96 self.children.first().map(|c| c.is_focusable()).unwrap_or(false)
97 }
98
99 fn layout(&mut self, available: Size) -> Size {
100 let s = if let Some(child) = self.children.first_mut() {
101 let cs = child.layout(available);
102 child.set_bounds(Rect::new(0.0, 0.0, cs.width, cs.height));
103 cs
104 } else {
105 available
106 };
107 self.bounds = Rect::new(0.0, 0.0, s.width, s.height);
108 s
109 }
110
111 fn paint(&mut self, ctx: &mut dyn DrawCtx) {
112 if self.hovered {
114 self.hover_frames = self.hover_frames.saturating_add(1);
115 }
116
117 if let Some(child) = self.children.first_mut() {
119 paint_subtree(child.as_mut(), ctx);
120 }
121
122 if !self.show_tip() { return; }
124
125 let v = ctx.visuals();
126 let pad_x = 8.0_f64;
127 let pad_y = 5.0_f64;
128
129 let max_tip_w = self.bounds.width.max(120.0).min(260.0);
131 let ls = self.tip_label.layout(Size::new(max_tip_w, 100.0));
132
133 let panel_w = ls.width + pad_x * 2.0;
134 let panel_h = ls.height + pad_y * 2.0;
135
136 let cursor_y = self.cursor.y;
138 let cursor_x = self.cursor.x;
139 let panel_x = (cursor_x - panel_w * 0.5).max(0.0).min(self.bounds.width - panel_w);
140 let panel_y = cursor_y + 10.0; ctx.set_fill_color(Color::rgba(0.0, 0.0, 0.0, 0.20));
144 ctx.begin_path();
145 ctx.rounded_rect(panel_x + 1.0, panel_y - 1.0, panel_w, panel_h, 4.0);
146 ctx.fill();
147
148 ctx.set_fill_color(v.window_fill);
150 ctx.begin_path();
151 ctx.rounded_rect(panel_x, panel_y, panel_w, panel_h, 4.0);
152 ctx.fill();
153
154 ctx.set_stroke_color(v.widget_stroke);
156 ctx.set_line_width(1.0);
157 ctx.begin_path();
158 ctx.rounded_rect(panel_x, panel_y, panel_w, panel_h, 4.0);
159 ctx.stroke();
160
161 self.tip_label.set_color(v.text_color);
163 self.tip_label.set_bounds(Rect::new(0.0, 0.0, ls.width, ls.height));
164 let lx = panel_x + pad_x;
165 let ly = panel_y + pad_y;
166 ctx.save();
167 ctx.translate(lx, ly);
168 paint_subtree(&mut self.tip_label, ctx);
169 ctx.restore();
170 }
171
172 fn on_event(&mut self, event: &Event) -> EventResult {
173 match event {
174 Event::MouseMove { pos } => {
175 let was = self.hovered;
176 self.hovered = self.hit_test(*pos);
177 self.cursor = *pos;
178 if !self.hovered {
179 self.hover_frames = 0;
180 }
181 let result = if let Some(child) = self.children.first_mut() {
183 child.on_event(event)
184 } else {
185 EventResult::Ignored
186 };
187 if self.hovered != was {
189 crate::animation::request_tick();
190 EventResult::Consumed
191 } else { result }
192 }
193 _ => {
194 if let Some(child) = self.children.first_mut() {
195 child.on_event(event)
196 } else {
197 EventResult::Ignored
198 }
199 }
200 }
201 }
202
203 fn hit_test(&self, local_pos: Point) -> bool {
204 local_pos.x >= 0.0 && local_pos.x <= self.bounds.width
205 && local_pos.y >= 0.0 && local_pos.y <= self.bounds.height
206 }
207}