pix_engine/gui/layout.rs
1//! UI spacing & layout rendering methods.
2//!
3//! Provided [`PixState`] methods:
4//!
5//! - [`PixState::same_line`]
6//! - [`PixState::next_width`]
7//! - [`PixState::tab_bar`]
8//! - [`PixState::spacing`]
9//! - [`PixState::indent`]
10//! - [`PixState::separator`]
11//!
12//! # Example
13//!
14//! ```
15//! # use pix_engine::prelude::*;
16//! # struct App { checkbox: bool, text_field: String, selected: &'static str };
17//! # impl PixEngine for App {
18//! fn on_update(&mut self, s: &mut PixState) -> PixResult<()> {
19//! s.text("Text")?;
20//! s.same_line(None);
21//! s.text("Same line")?;
22//! s.same_line([20, 0]);
23//! s.text("Same line with a +20 horizontal pixel offset")?;
24//!
25//! s.separator();
26//!
27//! s.spacing()?;
28//! s.indent()?;
29//! s.text("Indented!")?;
30//!
31//! s.next_width(200);
32//! if s.button("Button")? {
33//! // was clicked
34//! }
35//!
36//! s.tab_bar(
37//! "Tab bar",
38//! &["Tab 1", "Tab 2"],
39//! &mut self.selected,
40//! |tab: &&str, s: &mut PixState| {
41//! match tab {
42//! &"Tab 1" => {
43//! s.text("Tab 1 Content")?;
44//! },
45//! &"Tab 2" => {
46//! s.text("Tab 2 Content")?;
47//! },
48//! _ => (),
49//! }
50//! Ok(())
51//! }
52//! )?;
53//! Ok(())
54//! }
55//! # }
56//! ```
57
58use crate::{ops::clamp_size, prelude::*};
59
60impl PixState {
61 /// Reset current UI rendering position back to the previous line with item padding, and
62 /// continue with horizontal layout.
63 ///
64 /// You can optionally change the item padding, or set a different horizontal or vertical
65 /// position by passing in an `offset`.
66 ///
67 /// # Example
68 ///
69 /// ```
70 /// # use pix_engine::prelude::*;
71 /// # struct App { checkbox: bool, text_field: String };
72 /// # impl PixEngine for App {
73 /// fn on_update(&mut self, s: &mut PixState) -> PixResult<()> {
74 /// s.text("Text")?;
75 /// s.same_line(None);
76 /// s.text("Same line")?;
77 /// Ok(())
78 /// }
79 /// # }
80 /// ```
81 #[inline]
82 pub fn same_line<O>(&mut self, offset: O)
83 where
84 O: Into<Option<[i32; 2]>>,
85 {
86 let pos = self.ui.pcursor();
87 let offset = offset.into().unwrap_or([0; 2]);
88 let item_pad = self.theme.spacing.item_pad;
89 self.ui
90 .set_cursor([pos.x() + item_pad.x() + offset[0], pos.y() + offset[1]]);
91 self.ui.line_height = self.ui.pline_height;
92 }
93
94 /// Change the default width of the next rendered element for elements that typically take up
95 /// the remaining width of the window/frame they are rendered in.
96 ///
97 /// # Example
98 ///
99 /// ```
100 /// # use pix_engine::prelude::*;
101 /// # struct App { checkbox: bool, text_field: String };
102 /// # impl PixEngine for App {
103 /// fn on_update(&mut self, s: &mut PixState) -> PixResult<()> {
104 /// s.next_width(200);
105 /// if s.button("Button")? {
106 /// // was clicked
107 /// }
108 /// Ok(())
109 /// }
110 /// # }
111 /// ```
112 #[inline]
113 pub fn next_width(&mut self, width: u32) {
114 self.ui.next_width = Some(clamp_size(width));
115 }
116
117 /// Draw a tabbed view to the current canvas. It accepts a list of tabs to be rendered, which
118 /// one is selected and a closure that is passed the current tab and [`&mut
119 /// PixState`][`PixState`] which you can use to draw all the standard drawing primitives and
120 /// change any drawing settings. Settings changed inside the closure will not persist. Returns
121 /// `true` if a tab selection was changed.
122 ///
123 /// # Errors
124 ///
125 /// If the renderer fails to draw to the current render target, then an error is returned.
126 ///
127 /// # Example
128 ///
129 /// ```
130 /// # use pix_engine::prelude::*;
131 /// # struct App { selected: &'static str };
132 /// # impl PixEngine for App {
133 /// fn on_update(&mut self, s: &mut PixState) -> PixResult<()> {
134 /// s.tab_bar(
135 /// "Tab bar",
136 /// &["Tab 1", "Tab 2"],
137 /// &mut self.selected,
138 /// |tab: &&str, s: &mut PixState| {
139 /// match tab {
140 /// &"Tab 1" => {
141 /// s.text("Tab 1")?;
142 /// s.separator();
143 /// s.text("Some Content")?;
144 /// }
145 /// &"Tab 2" => {
146 /// s.next_width(200);
147 /// if s.button("Click me")? {
148 /// // was clicked
149 /// }
150 /// }
151 /// _ => (),
152 /// }
153 /// Ok(())
154 /// }
155 /// )?;
156 /// Ok(())
157 /// }
158 /// # }
159 /// ```
160 pub fn tab_bar<S, I, F>(
161 &mut self,
162 label: S,
163 tabs: &[I],
164 selected: &mut I,
165 f: F,
166 ) -> PixResult<bool>
167 where
168 S: AsRef<str>,
169 I: AsRef<str> + Copy,
170 F: FnOnce(&I, &mut PixState) -> PixResult<()>,
171 {
172 let label = label.as_ref();
173
174 let s = self;
175 let tab_id = s.ui.get_id(&label);
176 let font_size = s.theme.font_size;
177 let fpad = s.theme.spacing.frame_pad;
178 let ipad = s.theme.spacing.item_pad;
179
180 let mut changed = false;
181 for (i, tab) in tabs.iter().enumerate() {
182 if i > 0 {
183 s.same_line([-ipad.x() + 2, 0]);
184 } else {
185 let pos = s.cursor_pos();
186 s.set_cursor_pos([pos.x() + fpad.x(), pos.y()]);
187 }
188 let tab_label = tab.as_ref();
189 let id = s.ui.get_id(&tab_label);
190 let tab_label = s.ui.get_label(tab_label);
191 let pos = s.cursor_pos();
192 let colors = s.theme.colors;
193
194 // Calculate tab size
195 let (width, height) = s.text_size(tab_label)?;
196 let tab_rect = rect![pos, width, height].offset_size(4 * ipad);
197
198 // Check hover/active/keyboard focus
199 let hovered = s.focused() && s.ui.try_hover(id, &tab_rect);
200 let focused = s.focused() && s.ui.try_focus(id);
201 let disabled = s.ui.disabled;
202 let active = s.ui.is_active(id);
203
204 s.push();
205 s.ui.push_cursor();
206
207 // Render
208 s.rect_mode(RectMode::Corner);
209 let clip = tab_rect.offset_size([1, 0]);
210 s.clip(clip)?;
211 if hovered {
212 s.frame_cursor(&Cursor::hand())?;
213 }
214 let [stroke, fg, bg] = s.widget_colors(id, ColorType::SecondaryVariant);
215 if active || focused {
216 s.stroke(stroke);
217 } else {
218 s.stroke(None);
219 }
220 if hovered {
221 s.fill(fg.blended(colors.background, 0.04));
222 } else {
223 s.fill(colors.background);
224 }
225 if active {
226 s.clip(tab_rect.offset_size([2, 0]))?;
227 s.rect(tab_rect.offset([1, 1]))?;
228 } else {
229 s.rect(tab_rect)?;
230 }
231
232 // Tab text
233 s.rect_mode(RectMode::Center);
234 s.set_cursor_pos(tab_rect.center());
235 s.stroke(None);
236 let is_active_tab = tab_label == selected.as_ref();
237 if is_active_tab {
238 s.fill(colors.secondary_variant);
239 } else if hovered | focused {
240 s.fill(fg);
241 } else {
242 s.fill(colors.secondary_variant.blended(bg, 0.60));
243 }
244 s.text(tab_label)?;
245 s.clip(None)?;
246
247 s.ui.pop_cursor();
248 s.pop();
249
250 // Process input
251 s.ui.handle_focus(id);
252 s.advance_cursor(tab_rect.size());
253 if !disabled && s.ui.was_clicked(id) {
254 changed = true;
255 *selected = *tab;
256 }
257 }
258
259 let pos = s.cursor_pos();
260 s.set_cursor_pos([pos.x(), pos.y() - ipad.y() - font_size as i32 / 2]);
261 s.separator()?;
262 s.spacing()?;
263
264 s.push_id(tab_id);
265 f(selected, s)?;
266 s.pop_id();
267
268 Ok(changed)
269 }
270}
271
272impl PixState {
273 /// Draw a newline worth of spacing to the current canvas.
274 ///
275 /// # Errors
276 ///
277 /// If the renderer fails to draw to the current render target, then an error is returned.
278 ///
279 /// # Example
280 ///
281 /// ```
282 /// # use pix_engine::prelude::*;
283 /// # struct App { checkbox: bool, text_field: String };
284 /// # impl PixEngine for App {
285 /// fn on_update(&mut self, s: &mut PixState) -> PixResult<()> {
286 /// s.text("Some text")?;
287 /// s.spacing()?;
288 /// s.text("Some other text")?;
289 /// Ok(())
290 /// }
291 /// # }
292 /// ```
293 pub fn spacing(&mut self) -> PixResult<()> {
294 let s = self;
295 let width = s.ui_width()?;
296 let (_, height) = s.text_size(" ")?;
297 s.advance_cursor([width, height]);
298 Ok(())
299 }
300
301 /// Draw an indent worth of spacing to the current canvas.
302 ///
303 /// # Errors
304 ///
305 /// If the renderer fails to draw to the current render target, then an error is returned.
306 ///
307 /// # Example
308 ///
309 /// ```
310 /// # use pix_engine::prelude::*;
311 /// # struct App { checkbox: bool, text_field: String };
312 /// # impl PixEngine for App {
313 /// fn on_update(&mut self, s: &mut PixState) -> PixResult<()> {
314 /// s.indent()?;
315 /// s.text("Indented!")?;
316 /// Ok(())
317 /// }
318 /// # }
319 /// ```
320 pub fn indent(&mut self) -> PixResult<()> {
321 let s = self;
322 let (width, height) = s.text_size(" ")?;
323 s.advance_cursor([width, height]);
324 s.same_line(None);
325 Ok(())
326 }
327
328 /// Draw a horizontal or vertical separator to the current canvas.
329 ///
330 /// # Errors
331 ///
332 /// If the renderer fails to draw to the current render target, then an error is returned.
333 ///
334 /// # Example
335 ///
336 /// ```
337 /// # use pix_engine::prelude::*;
338 /// # struct App { checkbox: bool, text_field: String };
339 /// # impl PixEngine for App {
340 /// fn on_update(&mut self, s: &mut PixState) -> PixResult<()> {
341 /// s.text("Some text")?;
342 /// s.separator()?;
343 /// s.text("Some other text")?;
344 /// Ok(())
345 /// }
346 /// # }
347 /// ```
348 pub fn separator(&mut self) -> PixResult<()> {
349 // TODO: Add s.layout(Direction) method
350 let s = self;
351 let pos = s.cursor_pos();
352 let colors = s.theme.colors;
353 let pad = s.theme.spacing.frame_pad;
354 let height = clamp_size(s.theme.font_size);
355 let y = pos.y() + height / 2;
356
357 s.push();
358
359 s.stroke(colors.disabled());
360 let width = s.ui_width()?;
361 s.line(line_![pad.x(), y, width, y])?;
362
363 s.pop();
364 s.advance_cursor([width, height]);
365
366 Ok(())
367 }
368}