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}