pix_engine/
gui.rs

1//! Graphical User Interface methods.
2//!
3//! Uses [immediate mode](https://en.wikipedia.org/wiki/Immediate_mode_GUI). See the `gui` example
4//! in the `examples/` folder for a full demo.
5//!
6//! # Note
7//!
8//! Many widgets rely on unique labels or IDs that are consistent across frames for internal state
9//! management. This ID is generally built using the hash of the label combined with any parents
10//! they may be under, for example items rendered under a tab are combined with the tabs label
11//! hash.
12//!
13//! e.g.
14//!
15//! ```
16//! # use pix_engine::prelude::*;
17//! # fn draw(s: &mut PixState) -> PixResult<()> {
18//! if s.button("Click")? {     // Label = "Click", ID = hash of "Click"
19//!     // Handle click action
20//! }
21//! s.advanced_tooltip(
22//!     "Advanced Tooltip",
23//!     rect![s.mouse_pos(), 300, 100],
24//!     |s: &mut PixState| {
25//!         // Label = "Click", ID = hash of "Click" + "Advanced Tooltip"
26//!         if s.button("Click")? {
27//!             // Handle click action
28//!         }
29//!         Ok(())
30//!     },
31//! )?;
32//! # Ok(())
33//! # }
34//! ```
35//!
36//! There is an ID stack system in place in addition to a `##` string pattern that can be used to
37//! uniquely identify elements that may require an empty label or conflict with another element.
38//!
39//! If you find that your widget is not properly interacting with user events or maintaining state
40//! correctly, try one of the following methods to ensure the label is unique.
41//!
42//! You can append a unique identifier after your label with `##`. Anything after this pattern
43//! won't be visible to the user:
44//!
45//! ```
46//! # use pix_engine::prelude::*;
47//! # fn draw(s: &mut PixState) -> PixResult<()> {
48//! if s.button("Click##action1")? {     // Label = "Click", ID = hash of "Click##action1"
49//!     // Handle action 1
50//! }
51//! if s.button("Click##action2")? {     // Label = "Click", ID = hash of "Click##action2"
52//!     // Handle action 2
53//! }
54//! # Ok(())
55//! # }
56//! ```
57//!
58//! You can use [`PixState::push_id`] and [`PixState::pop_id`] either by itself, or as part of a loop:
59//!
60//! ```
61//! # use pix_engine::prelude::*;
62//! # fn draw(s: &mut PixState) -> PixResult<()> {
63//! for i in 0..5 {
64//!   s.push_id(i);             // Push i to the ID stack
65//!   if s.button("Click")? {   // Label = "Click",  ID = hash of "Click" + i
66//!     // Handle click action
67//!   }
68//!   s.pop_id();
69//! }
70//! # Ok(())
71//! # }
72//! ```
73//!
74//! # Example
75//!
76//! ```
77//! # use pix_engine::prelude::*;
78//! # struct App { checkbox: bool, text_field: String };
79//! # impl PixEngine for App {
80//! fn on_update(&mut self, s: &mut PixState) -> PixResult<()> {
81//!     s.text("Some text")?;
82//!     s.separator()?; // Adds a horizontal line separator
83//!     s.spacing(); // Adds a line of spacing
84//!
85//!     if s.button("Button")? {
86//!         // Button was clicked!
87//!     }
88//!
89//!     s.checkbox("Checkbox", &mut self.checkbox)?;
90//!
91//!     s.next_width(200);
92//!     s.text_field("Text Field", &mut self.text_field)?;
93//!     Ok(())
94//! }
95//! # }
96//! ```
97
98use self::state::ElementId;
99use crate::{
100    ops::{clamp_dimensions, clamp_size},
101    prelude::*,
102    renderer::Rendering,
103};
104
105pub mod layout;
106pub mod system;
107pub mod theme;
108pub mod widgets;
109
110pub(crate) mod keys;
111pub(crate) mod mouse;
112pub(crate) mod scroll;
113pub(crate) mod state;
114
115/// Platform-specific control modifier key. `CTRL` on most platforms.
116#[cfg(not(target_os = "macos"))]
117pub const MOD_CTRL: KeyMod = KeyMod::CTRL;
118/// Platform-specific control modifier key. `Command` on macOS.
119#[cfg(target_os = "macos")]
120pub const MOD_CTRL: KeyMod = KeyMod::GUI;
121
122/// Coordinate Direction.
123#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
124pub enum Direction {
125    /// Up.
126    Up,
127    /// Down.
128    Down,
129    /// Left.
130    Left,
131    /// Right.
132    Right,
133}
134
135impl PixState {
136    /// Return usable UI width given the current UI cursor position and padding clamped to i32.
137    ///
138    /// # Errors
139    ///
140    /// If the current window target has been closed or is invalid, then an error is returned.
141    #[inline]
142    pub fn ui_width(&self) -> PixResult<i32> {
143        let pos = self.cursor_pos();
144        let fpad = self.theme.spacing.frame_pad;
145        Ok(clamp_size(self.width()?) - pos.x() - fpad.x())
146    }
147
148    /// Return usable UI height given the current UI cursor position and padding clamped to i32.
149    ///
150    /// # Errors
151    ///
152    /// If the current window target has been closed or is invalid, then an error is returned.
153    #[inline]
154    pub fn ui_height(&self) -> PixResult<i32> {
155        let pos = self.cursor_pos();
156        let fpad = self.theme.spacing.frame_pad;
157        Ok(clamp_size(self.height()?) - pos.y() - fpad.y())
158    }
159}
160
161impl PixState {
162    /// Set and return default colors based on widget state for the given surface type.
163    #[inline]
164    pub(crate) fn widget_colors(&mut self, id: ElementId, surface_color: ColorType) -> [Color; 3] {
165        // "On" overlay opacity:
166        // - High emphasis: 87%
167        // - Med emphasis: 60%
168        // - Disabled: 38%
169        // - Error: 100%
170        // Stroke & Fill opacity:
171        // - Focused: 12%
172        // - Hovered: 4%
173        // - Active: 8%
174
175        let s = self;
176        let focused = s.ui.is_focused(id);
177        let active = s.ui.is_active(id);
178        let hovered = s.ui.is_hovered(id);
179        let disabled = s.ui.disabled;
180        let c = s.theme.colors;
181
182        let (bg, overlay) = match surface_color {
183            ColorType::Background => (c.background, c.on_background),
184            ColorType::Surface => (c.surface, c.on_surface),
185            ColorType::Primary => (c.primary, c.on_primary),
186            ColorType::PrimaryVariant => (c.primary_variant, c.on_primary),
187            ColorType::Secondary => (c.secondary, c.on_secondary),
188            ColorType::SecondaryVariant => (c.secondary_variant, c.on_secondary),
189            ColorType::Error => (c.error, c.on_error),
190            _ => panic!("invalid surface color"),
191        };
192        let branded = matches!(
193            surface_color,
194            ColorType::Primary
195                | ColorType::PrimaryVariant
196                | ColorType::Secondary
197                | ColorType::SecondaryVariant,
198        );
199
200        let stroke_overlay = if branded {
201            bg.blended(Color::WHITE, 0.60)
202        } else {
203            overlay
204        };
205        let stroke = if focused {
206            stroke_overlay
207        } else if disabled {
208            stroke_overlay.blended(bg, 0.18)
209        } else {
210            stroke_overlay.blended(bg, 0.38)
211        };
212
213        let bg_overlay = if branded { Color::WHITE } else { overlay };
214        let bg = if focused {
215            bg_overlay.blended(bg, 0.12)
216        } else if active {
217            bg_overlay.blended(bg, 0.08)
218        } else if hovered {
219            if branded {
220                bg_overlay.blended(bg, 0.12)
221            } else {
222                bg_overlay.blended(bg, 0.04)
223            }
224        } else if branded && disabled {
225            overlay.blended(bg, 0.38)
226        } else {
227            bg
228        };
229
230        let fg = if disabled {
231            overlay.blended(bg, 0.38)
232        } else {
233            overlay.blended(bg, 0.87)
234        };
235
236        [stroke, bg, fg]
237    }
238
239    /// Return the size of text, clamped to i32.
240    #[inline]
241    pub(crate) fn text_size(&self, text: &str) -> PixResult<(i32, i32)> {
242        let s = &self.settings;
243        let wrap_width = s.wrap_width;
244        let ipad = self.theme.spacing.item_pad;
245        let pos = self.cursor_pos();
246        let wrap_width = if wrap_width.is_none() && text.contains('\n') {
247            text.lines()
248                .map(|line| {
249                    let (line_width, _) = self.renderer.size_of(line, None).unwrap_or_default();
250                    line_width
251                })
252                .max()
253                .map(|width| width + (pos.x() + ipad.x()) as u32)
254        } else {
255            wrap_width
256        };
257        let (w, h) = self.renderer.size_of(text, wrap_width)?;
258        // EXPL: Add same padding that `text_transformed` uses.
259        Ok(clamp_dimensions(w + 3, h + 3))
260    }
261}