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}