pix_engine/gui/
widgets.rs

1//! UI widget rendering methods.
2//!
3//! Provided [`PixState`] methods:
4//!
5//! - [`PixState::button`]
6//! - [`PixState::checkbox`]
7//! - [`PixState::radio`]
8//!
9//! # Example
10//!
11//! ```
12//! # use pix_engine::prelude::*;
13//! # struct App { checkbox: bool, radio: usize };
14//! # impl PixEngine for App {
15//! fn on_update(&mut self, s: &mut PixState) -> PixResult<()> {
16//!     if s.button("Button")? {
17//!         // was clicked
18//!     }
19//!     s.checkbox("Checkbox", &mut self.checkbox)?;
20//!     s.radio("Radio 1", &mut self.radio, 0)?;
21//!     s.radio("Radio 2", &mut self.radio, 1)?;
22//!     s.radio("Radio 3", &mut self.radio, 2)?;
23//!     Ok(())
24//! }
25//! # }
26//! ```
27
28use crate::{gui::Direction, ops::clamp_size, prelude::*};
29
30pub mod field;
31pub mod select;
32pub mod slider;
33pub mod text;
34pub mod tooltip;
35
36impl PixState {
37    /// Draw a button to the current canvas that returns `true` when clicked.
38    ///
39    /// # Errors
40    ///
41    /// If the renderer fails to draw to the current render target, then an error is returned.
42    ///
43    /// # Example
44    ///
45    /// ```
46    /// # use pix_engine::prelude::*;
47    /// # struct App;
48    /// # impl PixEngine for App {
49    /// fn on_update(&mut self, s: &mut PixState) -> PixResult<()> {
50    ///     if s.button("Button")? {
51    ///         // was clicked
52    ///     }
53    ///     Ok(())
54    /// }
55    /// # }
56    /// ```
57    pub fn button<L>(&mut self, label: L) -> PixResult<bool>
58    where
59        L: AsRef<str>,
60    {
61        let label = label.as_ref();
62
63        let s = self;
64        let id = s.ui.get_id(&label);
65        let label = s.ui.get_label(label);
66        let pos = s.cursor_pos();
67        let fpad = s.theme.spacing.frame_pad;
68
69        // Calculate button size
70        let (label_width, label_height) = s.text_size(label)?;
71        let width = s.ui.next_width.take().unwrap_or(label_width);
72        let button = rect![pos, width, label_height].offset_size(2 * fpad);
73
74        // Check hover/active/keyboard focus
75        let hovered = s.focused() && s.ui.try_hover(id, &button);
76        if s.focused() {
77            s.ui.try_focus(id);
78        }
79        let disabled = s.ui.disabled;
80        let active = s.ui.is_active(id);
81
82        s.push();
83        s.ui.push_cursor();
84
85        // Render
86        s.rect_mode(RectMode::Corner);
87        if hovered {
88            s.frame_cursor(&Cursor::hand())?;
89        }
90        let [stroke, bg, fg] = s.widget_colors(id, ColorType::Primary);
91        s.stroke(stroke);
92        s.fill(bg);
93        if active {
94            s.rect(button.offset([1, 1]))?;
95        } else {
96            s.rect(button)?;
97        }
98
99        // Button text
100        s.rect_mode(RectMode::Center);
101        s.clip(button)?;
102        s.set_cursor_pos(button.center());
103        s.stroke(None);
104        s.fill(fg);
105        s.text(label)?;
106        s.clip(None)?;
107
108        s.ui.pop_cursor();
109        s.pop();
110
111        // Process input
112        s.ui.handle_focus(id);
113        s.advance_cursor(button.size());
114        Ok(!disabled && s.ui.was_clicked(id))
115    }
116
117    /// Draw a text link to the current canvas that returns `true` when clicked.
118    ///
119    /// # Errors
120    ///
121    /// If the renderer fails to draw to the current render target, then an error is returned.
122    ///
123    /// # Example
124    ///
125    /// ```
126    /// # use pix_engine::prelude::*;
127    /// # struct App;
128    /// # impl PixEngine for App {
129    /// fn on_update(&mut self, s: &mut PixState) -> PixResult<()> {
130    ///     if s.link("Link")? {
131    ///         // was clicked
132    ///     }
133    ///     Ok(())
134    /// }
135    /// # }
136    /// ```
137    pub fn link<S>(&mut self, text: S) -> PixResult<bool>
138    where
139        S: AsRef<str>,
140    {
141        let text = text.as_ref();
142
143        let s = self;
144        let id = s.ui.get_id(&text);
145        let text = s.ui.get_label(text);
146        let pos = s.cursor_pos();
147        let pad = s.theme.spacing.item_pad;
148
149        // Calculate button size
150        let (width, height) = s.text_size(text)?;
151        let bounding_box = rect![pos, width, height].grow(pad / 2);
152
153        // Check hover/active/keyboard focus
154        let hovered = s.focused() && s.ui.try_hover(id, &bounding_box);
155        let focused = s.focused() && s.ui.try_focus(id);
156        let disabled = s.ui.disabled;
157        let active = s.ui.is_active(id);
158
159        s.push();
160
161        // Render
162        if hovered {
163            s.frame_cursor(&Cursor::hand())?;
164        }
165        let [stroke, bg, fg] = s.widget_colors(id, ColorType::Primary);
166        if focused {
167            s.stroke(stroke);
168            s.fill(None);
169            s.rect(bounding_box)?;
170        }
171
172        // Button text
173        s.stroke(None);
174        if active {
175            s.fill(fg.blended(bg, 0.04));
176        } else {
177            s.fill(bg);
178        }
179        s.text(text)?;
180
181        s.pop();
182
183        // Process input
184        s.ui.handle_focus(id);
185        Ok(!disabled && s.ui.was_clicked(id))
186    }
187
188    /// Draw a checkbox to the current canvas.
189    ///
190    /// # Errors
191    ///
192    /// If the renderer fails to draw to the current render target, then an error is returned.
193    ///
194    /// # Example
195    ///
196    /// ```
197    /// # use pix_engine::prelude::*;
198    /// # struct App { checkbox: bool };
199    /// # impl PixEngine for App {
200    /// fn on_update(&mut self, s: &mut PixState) -> PixResult<()> {
201    ///     s.checkbox("Checkbox", &mut self.checkbox)?;
202    ///     Ok(())
203    /// }
204    /// # }
205    /// ```
206    pub fn checkbox<S>(&mut self, label: S, checked: &mut bool) -> PixResult<bool>
207    where
208        S: AsRef<str>,
209    {
210        let label = label.as_ref();
211
212        let s = self;
213        let id = s.ui.get_id(&label);
214        let label = s.ui.get_label(label);
215        let pos = s.cursor_pos();
216        let (_, checkbox_size) = s.text_size(label)?;
217
218        // Calculate checkbox rect
219        let checkbox = square![pos, checkbox_size];
220
221        // Check hover/active/keyboard focus
222        let hovered = s.focused() && s.ui.try_hover(id, &checkbox);
223        if s.focused() {
224            s.ui.try_focus(id);
225        }
226        let disabled = s.ui.disabled;
227
228        s.push();
229
230        // Checkbox
231        s.rect_mode(RectMode::Corner);
232        if hovered {
233            s.frame_cursor(&Cursor::hand())?;
234        }
235        let [stroke, bg, fg] = if *checked {
236            s.widget_colors(id, ColorType::Primary)
237        } else {
238            s.widget_colors(id, ColorType::Background)
239        };
240        s.stroke(stroke);
241        s.fill(bg);
242        s.rect(checkbox)?;
243
244        if *checked {
245            s.stroke(fg);
246            s.stroke_weight(2);
247            let half = checkbox_size / 2;
248            let third = checkbox_size / 3;
249            let x = checkbox.left() + half - 1;
250            let y = checkbox.bottom() - third + 1;
251            let start = point![x - third + 2, y - third + 2];
252            let mid = point![x, y];
253            let end = point![x + third + 1, y - half + 2];
254            s.line([start, mid])?;
255            s.line([mid, end])?;
256        }
257        s.advance_cursor(checkbox.size());
258        s.pop();
259
260        // Label
261        s.same_line(None);
262        s.text(label)?;
263
264        // Process input
265        s.ui.handle_focus(id);
266        if disabled {
267            Ok(false)
268        } else {
269            let clicked = s.ui.was_clicked(id);
270            if clicked {
271                *checked = !(*checked);
272            }
273            Ok(clicked)
274        }
275    }
276
277    /// Draw a set of radio buttons to the current canvas.
278    ///
279    /// # Errors
280    ///
281    /// If the renderer fails to draw to the current render target, then an error is returned.
282    ///
283    /// # Example
284    ///
285    /// ```
286    /// # use pix_engine::prelude::*;
287    /// # struct App { radio: usize };
288    /// # impl PixEngine for App {
289    /// fn on_update(&mut self, s: &mut PixState) -> PixResult<()> {
290    ///     s.radio("Radio 1", &mut self.radio, 0)?;
291    ///     s.radio("Radio 2", &mut self.radio, 1)?;
292    ///     s.radio("Radio 3", &mut self.radio, 2)?;
293    ///     Ok(())
294    /// }
295    /// # }
296    /// ```
297    pub fn radio<S>(&mut self, label: S, selected: &mut usize, index: usize) -> PixResult<bool>
298    where
299        S: AsRef<str>,
300    {
301        let label = label.as_ref();
302
303        let s = self;
304        let id = s.ui.get_id(&label);
305        let label = s.ui.get_label(label);
306        let pos = s.cursor_pos();
307        let (_, label_height) = s.text_size(label)?;
308        let radio_size = label_height / 2;
309
310        // Calculate radio rect
311        let radio = circle![pos + radio_size, radio_size];
312
313        // Check hover/active/keyboard focus
314        let hovered = s.focused() && s.ui.try_hover(id, &radio);
315        if s.focused() {
316            s.ui.try_focus(id);
317        }
318        let disabled = s.ui.disabled;
319
320        s.push();
321
322        // Radio
323        s.rect_mode(RectMode::Corner);
324        s.ellipse_mode(EllipseMode::Center);
325        if hovered {
326            s.frame_cursor(&Cursor::hand())?;
327        }
328        let is_selected = *selected == index;
329        let [stroke, bg, _] = if is_selected {
330            s.widget_colors(id, ColorType::Primary)
331        } else {
332            s.widget_colors(id, ColorType::Background)
333        };
334        if is_selected {
335            s.stroke(bg);
336            s.fill(None);
337        } else {
338            s.stroke(stroke);
339            s.fill(bg);
340        }
341        s.circle(radio)?;
342
343        if is_selected {
344            s.stroke(bg);
345            s.fill(bg);
346            s.circle([radio.x(), radio.y(), radio.radius() - 3])?;
347        }
348        s.advance_cursor(radio.bounding_rect().size());
349        s.pop();
350
351        // Label
352        s.same_line(None);
353        s.text(label)?;
354
355        // Process input
356        s.ui.handle_focus(id);
357        if disabled {
358            Ok(false)
359        } else {
360            let clicked = s.ui.was_clicked(id);
361            if clicked {
362                *selected = index;
363            }
364            Ok(clicked)
365        }
366    }
367
368    /// Render an arrow aligned with the current font size.
369    ///
370    /// # Errors
371    ///
372    /// If the renderer fails to draw to the current render target, then an error is returned.
373    pub fn arrow<P, S>(&mut self, pos: P, direction: Direction, scale: S) -> PixResult<()>
374    where
375        P: Into<Point<i32>>,
376        S: Into<f64>,
377    {
378        let pos: Point<f64> = pos.into().as_();
379        let scale = scale.into();
380
381        let s = self;
382        let font_size = clamp_size(s.theme.font_size);
383
384        let height = f64::from(font_size);
385        let mut ratio = height * 0.4 * scale;
386        let center = pos + point![height * 0.5, height * 0.5 * scale];
387
388        if let Direction::Up | Direction::Left = direction {
389            ratio = -ratio;
390        }
391        let (p1, p2, p3) = match direction {
392            Direction::Up | Direction::Down => (
393                point![0.0, 0.75],
394                point![-0.866, -0.75],
395                point![0.866, -0.75],
396            ),
397            Direction::Left | Direction::Right => (
398                point![0.75, 0.0],
399                point![-0.75, 0.866],
400                point![-0.75, -0.866],
401            ),
402        };
403
404        s.triangle([
405            (center + p1 * ratio).round().as_(),
406            (center + p2 * ratio).round().as_(),
407            (center + p3 * ratio).round().as_(),
408        ])?;
409
410        Ok(())
411    }
412}