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}