pix_engine/gui/widgets/tooltip.rs
1//! Tooltip widget rendering methods.
2//!
3//! Provided [`PixState`] methods:
4//!
5//! - [`PixState::help_marker`]
6//! - [`PixState::tooltip`]
7//! - [`PixState::advanced_tooltip`]
8//!
9//! # Example
10//!
11//! ```
12//! # use pix_engine::prelude::*;
13//! # struct App { select_box: usize };
14//! # impl PixEngine for App {
15//! fn on_update(&mut self, s: &mut PixState) -> PixResult<()> {
16//! s.help_marker("Help marker icon w/ tooltip")?;
17//!
18//! s.text("Hover me")?;
19//! if s.hovered() {
20//! s.tooltip("Basic tooltip")?;
21//! }
22//!
23//! s.text("Hover me too!")?;
24//! if s.hovered() {
25//! s.advanced_tooltip(
26//! "Advanced tooltip",
27//! rect![s.mouse_pos(), 200, 100],
28//! |s: &mut PixState| {
29//! s.background(Color::CADET_BLUE);
30//! s.bullet("Advanced tooltip")?;
31//! Ok(())
32//! }
33//! )?;
34//! }
35//! Ok(())
36//! }
37//! # }
38//! ```
39
40use crate::{ops::clamp_dimensions, prelude::*};
41
42impl PixState {
43 /// Draw help marker text that, when hovered, displays a help box with text to the current
44 /// canvas.
45 ///
46 /// # Errors
47 ///
48 /// If the renderer fails to draw to the current render target, then an error is returned.
49 ///
50 /// # Example
51 ///
52 /// ```
53 /// # use pix_engine::prelude::*;
54 /// # struct App { select_box: usize };
55 /// # impl PixEngine for App {
56 /// fn on_update(&mut self, s: &mut PixState) -> PixResult<()> {
57 /// s.help_marker("Help marker icon w/ tooltip")?;
58 /// Ok(())
59 /// }
60 /// # }
61 /// ```
62 pub fn help_marker<S>(&mut self, text: S) -> PixResult<()>
63 where
64 S: AsRef<str>,
65 {
66 let text = text.as_ref();
67
68 let s = self;
69 let id = s.ui.get_id(&text);
70 let text = s.ui.get_label(text);
71 let pos = s.cursor_pos();
72 let spacing = s.theme.spacing;
73 let fpad = spacing.frame_pad;
74 let ipad = spacing.item_pad;
75
76 // Calculate hover area
77 let marker = "?";
78 let (marker_width, marker_height) = s.text_size(marker)?;
79 let hover = rect![
80 pos,
81 marker_width + 2 * ipad.x(),
82 marker_height + 2 * ipad.y()
83 ];
84
85 // Check hover/active/keyboard focus
86 let hovered = s.focused() && s.ui.try_hover(id, &hover);
87 let focused = s.focused() && s.ui.try_focus(id);
88 let disabled = s.ui.disabled;
89
90 s.push();
91 s.ui.push_cursor();
92
93 // Marker outline
94 s.rect_mode(RectMode::Corner);
95 let [_, bg, fg] = s.widget_colors(id, ColorType::Background);
96 s.disable(true);
97 s.stroke(None);
98 s.fill(bg);
99 s.square(hover)?;
100
101 // Marker
102 s.rect_mode(RectMode::Center);
103 s.set_cursor_pos([hover.center().x(), hover.center().y() - 3]);
104 s.stroke(None);
105 s.fill(fg);
106 s.text(marker)?;
107 if !disabled {
108 s.disable(false);
109 }
110
111 // Tooltip
112 if focused {
113 let (text_width, text_height) = s.text_size(text)?;
114 let text_width = text_width + 2 * fpad.x();
115 let text_height = text_height + 2 * fpad.y();
116 s.push_id(id);
117 s.advanced_tooltip(
118 text,
119 rect![hover.bottom_right() - 10, text_width, text_height],
120 |s: &mut PixState| {
121 let [stroke, bg, fg] = s.widget_colors(id, ColorType::Surface);
122 s.background(bg);
123
124 s.stroke(stroke);
125 s.fill(None);
126 s.rect([0, 0, text_width - 1, text_height - 1])?;
127
128 s.stroke(None);
129 s.fill(fg);
130 s.text(text)?;
131 Ok(())
132 },
133 )?;
134 s.pop_id();
135 } else if hovered {
136 s.tooltip(text)?;
137 }
138
139 s.ui.pop_cursor();
140 s.pop();
141
142 // Process input
143 s.ui.handle_focus(id);
144 s.advance_cursor([hover.width(), hover.height() - ipad.y()]);
145
146 Ok(())
147 }
148
149 /// Draw tooltip box at the mouse cursor with text to the current canvas.
150 ///
151 /// # Errors
152 ///
153 /// If the renderer fails to draw to the current render target, then an error is returned.
154 ///
155 /// # Example
156 ///
157 /// ```
158 /// # use pix_engine::prelude::*;
159 /// # struct App { select_box: usize };
160 /// # impl PixEngine for App {
161 /// fn on_update(&mut self, s: &mut PixState) -> PixResult<()> {
162 /// s.text("Hover me")?;
163 /// if s.hovered() {
164 /// s.tooltip("Basic tooltip")?;
165 /// }
166 /// Ok(())
167 /// }
168 /// # }
169 /// ```
170 pub fn tooltip<S>(&mut self, text: S) -> PixResult<()>
171 where
172 S: AsRef<str>,
173 {
174 let text = text.as_ref();
175
176 let s = self;
177 let id = s.ui.get_id(&text);
178 let text = s.ui.get_label(text);
179 let spacing = s.theme.spacing;
180 let pad = spacing.frame_pad;
181
182 let (text_width, text_height) = s.text_size(text)?;
183 let text_width = text_width + 2 * pad.x();
184 let text_height = text_height + 2 * pad.y();
185
186 // Render
187 s.push_id(id);
188 s.advanced_tooltip(
189 text,
190 rect![s.mouse_pos(), text_width, text_height],
191 |s: &mut PixState| {
192 let [stroke, bg, fg] = s.widget_colors(id, ColorType::Surface);
193 s.background(bg);
194
195 s.stroke(stroke);
196 s.fill(None);
197 s.rect([0, 0, text_width - 1, text_height - 1])?;
198
199 s.stroke(None);
200 s.fill(fg);
201 s.text(text)?;
202 Ok(())
203 },
204 )?;
205 s.pop_id();
206
207 Ok(())
208 }
209
210 /// Draw an advanced tooltip box at the mouse cursor to the current canvas. It accepts a
211 /// closure that is passed [`&mut PixState`][`PixState`] which you can use to draw all the
212 /// standard drawing primitives and change any drawing settings. Settings changed inside the
213 /// closure will not persist.
214 ///
215 /// # Errors
216 ///
217 /// If the renderer fails to draw to the current render target, then an error is returned.
218 ///
219 /// # Example
220 ///
221 /// ```
222 /// # use pix_engine::prelude::*;
223 /// # struct App { select_box: usize };
224 /// # impl PixEngine for App {
225 /// fn on_update(&mut self, s: &mut PixState) -> PixResult<()> {
226 /// s.text("Hover me")?;
227 /// if s.hovered() {
228 /// s.advanced_tooltip(
229 /// "Tooltip",
230 /// rect![s.mouse_pos(), 200, 100],
231 /// |s: &mut PixState| {
232 /// s.background(Color::CADET_BLUE);
233 /// s.bullet("Advanced tooltip")?;
234 /// Ok(())
235 /// },
236 /// )?;
237 /// }
238 /// Ok(())
239 /// }
240 /// # }
241 /// ```
242 pub fn advanced_tooltip<S, R, F>(&mut self, label: S, rect: R, f: F) -> PixResult<()>
243 where
244 S: AsRef<str>,
245 R: Into<Rect<i32>>,
246 F: FnOnce(&mut PixState) -> PixResult<()>,
247 {
248 let label = label.as_ref();
249
250 let s = self;
251 let id = s.ui.get_id(&label);
252 let pad = s.theme.spacing.frame_pad;
253
254 s.rect_mode(RectMode::Corner);
255
256 // Calculate rect
257 let mut rect = s.get_rect(rect).offset([15, 15]);
258
259 // Ensure rect stays inside window
260 let (win_width, win_height) = s.window_dimensions()?;
261 let (win_width, win_height) = clamp_dimensions(win_width, win_height);
262 if rect.right() > win_width {
263 let offset = (rect.right() - win_width) + pad.x();
264 rect = rect.offset([-offset, 0]);
265 }
266 if rect.bottom() > win_height {
267 let offset = (rect.bottom() - win_height) + pad.y();
268 rect = rect.offset([0, -offset]);
269 let mpos = s.mouse_pos();
270 if rect.contains(mpos) {
271 rect.set_bottom(mpos.y() - pad.y());
272 }
273 }
274
275 let texture_id = s.get_or_create_texture(id, None, rect)?;
276 s.ui.offset_mouse(rect.top_left());
277
278 s.set_texture_target(texture_id)?;
279 f(s)?;
280 s.clear_texture_target();
281
282 s.ui.clear_mouse_offset();
283
284 Ok(())
285 }
286}