pixels_graphics_lib/ui/layout/
mod.rs

1//! UI Layout
2//!
3//! There's three main approaches for positioning UI
4//! 1) Absolute:
5//!     Each view is positioned manually at specific coords
6//! 2) Assisted
7//!     Using the [RowLayout] and [ColumnLayout] macros views are positioned in rows and columns (with spacing and padding)
8//! 3) Relative
9//!     Using [LayoutContext] and [layout!](crate::ui::layout::relative::layout) to position and size views relative to the context and each other
10//!
11//! # Examples
12//!
13//! ### 1 Absolute
14//! ```rust
15//!# use pixels_graphics_lib::prelude::*;
16//!# use pixels_graphics_lib::ui::prelude::*;
17//!# let style = UiStyle::default();
18//! const PADDING: usize = 4;
19//! const BUTTON_HEIGHT: usize = 30;
20//! const BUTTON_WIDTH: usize = 70;
21//! const HEIGHT: usize = 100;
22//! const WIDTH: usize = 100;
23//! let background = Rect::new((0,0), (WIDTH, HEIGHT));
24//! let label = Text::new("Are you sure?", TextPos::px((100, 50)), (WHITE, PixelFont::Standard6x7, Positioning::Center));
25//! let positive = Button::new((PADDING, HEIGHT - PADDING - BUTTON_HEIGHT), "Yes", Some(BUTTON_WIDTH), &style.button);
26//! let negative = Button::new((WIDTH - PADDING - BUTTON_WIDTH, HEIGHT - PADDING - BUTTON_HEIGHT), "No", Some(BUTTON_WIDTH), &style.button);
27//! ```
28//!
29//! ### 2 Assisted
30//!
31//!```rust
32//!# use buffer_graphics_lib::text::PixelFont::Standard6x7;
33//!# use pixels_graphics_lib::column_layout;
34//!# use pixels_graphics_lib::ui::prelude::*;
35//!# use pixels_graphics_lib::prelude::*;
36//!# let style = UiStyle::default();
37//! let mut lbl_name = Label::new(Text::new("Name", TextPos::px(Coord::default()), (WHITE, Standard6x7)));
38//! let mut txt_name = TextField::new(Coord::default(), 30, Standard6x7, (None, None), "", &[TextFilter::Letters], &style.text_field);
39//! let mut cta = Button::new(Coord::default(), "Submit", None, &style.button);
40//! column_layout!(Rect::new((0,0),(200,200)), ColumnGravity::Left, padding: 4, views: lbl_name, txt_name, cta);
41//! ```
42//!
43//! ### 3 Relative
44//! ```rust
45//! use pixels_graphics_lib::layout;
46//!# use pixels_graphics_lib::prelude::*;
47//!# use pixels_graphics_lib::ui::layout::relative::LayoutContext;
48//!# use pixels_graphics_lib::ui::prelude::*;
49//!# let style = UiStyle::default();
50//! const BUTTON_WIDTH: usize = 70;
51//! const PADDING: usize = 6;
52//! const HEIGHT: usize = 100;
53//! const WIDTH: usize = 100;
54//! let background = Rect::new((0,0), (WIDTH, HEIGHT));
55//! let context = LayoutContext::new_with_padding(background.clone(), PADDING);
56//! let mut label = Label::new(Text::new("Are you sure?", TextPos::px((100, 50)), (WHITE, PixelFont::Standard6x7, Positioning::Center)));
57//! let mut positive = Button::new((0,0), "Yes", Some(BUTTON_WIDTH), &style.button);
58//! let mut negative = Button::new((0,0), "No", Some(BUTTON_WIDTH), &style.button);
59//!
60//! layout!(context, label, align_centerh);
61//! layout!(context, label, align_top);
62//!
63//! layout!(context, positive, align_left);
64//! layout!(context, positive, align_bottom);
65//!
66//! layout!(context, negative, align_left);
67//! layout!(context, negative, align_bottom);
68//! ```
69
70use crate::prelude::Rect;
71use crate::ui::PixelView;
72use std::fmt::Debug;
73
74pub mod column;
75pub mod relative;
76pub mod row;
77
78pub type ViewId = usize;
79
80pub trait LayoutView: PixelView + Debug {
81    fn set_bounds(&mut self, bounds: Rect);
82}
83
84#[macro_export]
85macro_rules! or_else {
86    ($value:expr, $other: expr) => {
87        $value
88    };
89    (, $other: expr) => {
90        $other
91    };
92}
93
94#[macro_export]
95macro_rules! bounds {
96    ($top_left:expr, $bottom_right:expr) => {
97        Rect::new(
98            $crate::prelude::coord!($top_left),
99            $crate::prelude::coord!($bottom_right),
100        )
101    };
102    ($top_left:expr, $width: expr, $height: expr) => {
103        Rect::new_with_size(
104            $crate::prelude::coord!($top_left),
105            $width as usize,
106            $height as usize,
107        )
108    };
109}
110
111/// Update the position multiple views to be in a column
112/// if `gravity` is
113///  - Left - then only the `top_left` of `bounds` is used
114///  - Center - then only the `center` of `bounds` is used
115///  - Right - then only the `bottom_right` of `bounds` is used
116///
117/// `padding` is used to offset views from the relevant position
118///
119/// `spacing` is space between views
120///
121/// **Usage**
122/// ```ignore
123///  let button1 = Button::new("Button", ...);
124///  let button2 = Button::new("Button", ...);
125///  let button3 = Button::new("Button", ...);
126///  column_layout!(Rect::new((16,16),(16,16)), ColumnGravity::Left, padding: 2, spacing: 8,  views: button3, button1, button2);
127///  // button3 top left will be (18, 18)
128///  // button1 top left will be (18, 18 + 8 + button3.height)
129///  // button2 top left will be (18, 18 + 8 + button3.height + 8 + button2.height)
130/// ```
131#[macro_export]
132macro_rules! column_layout {
133    ($bounds:expr, $gravity:expr, $(padding: $padding:expr,)? $(spacing: $margin:expr,)? views: $($views:expr),+ $(,)?) => {{
134        $crate::ui::layout::column::ColumnLayout::new($crate::or_else!($($padding)?, 0),$crate::or_else!($($margin)?, 0), $bounds, $gravity).layout(&mut [$(&mut $views,)*])
135    }};
136}
137
138/// Update the position multiple views to be in a row
139/// if `gravity` is
140///  - Top - then only the `top_left` of `bounds` is used
141///  - Center - then only the `center` of `bounds` is used
142///  - Bottom - then only the `bottom_right` of `bounds` is used
143///
144/// `padding` is used to offset views from the relevant position
145///
146/// `spacing` is space between views
147///
148/// **Usage**
149/// ```ignore
150///  let button1 = Button::new("Button", ...);
151///  let button2 = Button::new("Button", ...);
152///  let button3 = Button::new("Button", ...);
153///  row_layout!(Rect::new((16,16),(16,16)), RowGravity::Top, spacing: 8,  views: button3, button1, button2);
154///  // button3 top left will be (16,16)
155///  // button1 top left will be (16 + 8 + button3.width, 16)
156///  // button2 top left will be (16 + 8 + 8 + button3.width + button1.width, 16)
157/// ```
158#[macro_export]
159macro_rules! row_layout {
160    ($bounds:expr, $gravity:expr, $(padding: $padding:expr,)? $(spacing: $margin:expr,)? views: $($views:expr),+ $(,)?) => {{
161        $crate::ui::layout::row::RowLayout::new($crate::or_else!($($padding)?, 0), $crate::or_else!($($margin)?, 0), $bounds, $gravity).layout(&mut [$(&mut $views,)*])
162    }};
163}
164
165#[cfg(test)]
166mod test {
167    use crate::ui::button::Button;
168    use crate::ui::prelude::*;
169    use crate::ui::PixelView;
170
171    #[test]
172    fn syntax_check() {
173        let style = UiStyle::default();
174        let mut button = Button::new((0, 0), "test", None, &style.button);
175        let mut button2 = Button::new((0, 0), "test", None, &style.button);
176        column_layout!(
177            bounds!((100, 100), 10, 10),
178            ColumnGravity::Left,
179            views: button
180        );
181        assert_eq!(button.bounds().top_left(), coord!(100, 100));
182
183        column_layout!(
184            bounds!((100, 100), 10, 10),
185            ColumnGravity::Right,
186            padding: 4,
187            views: button
188        );
189        assert_eq!(button.bounds().top_left(), coord!(100, 104));
190
191        row_layout!(
192            bounds!((100, 100), 10, 10),
193            RowGravity::Center,
194            spacing: 4,
195            views: button, button2
196        );
197        assert_eq!(button.bounds().top_left(), coord!(100, 100));
198        assert_eq!(button2.bounds().top_left(), coord!(137, 100));
199    }
200
201    #[test]
202    fn bounds_check() {
203        let bounds = bounds!((0, 0), 100, 100);
204        assert_eq!(bounds, Rect::new_with_size(Coord::new(0, 0), 100, 100));
205
206        let bounds = bounds!((45, 47), (100, 100));
207        assert_eq!(bounds, Rect::new(Coord::new(45, 47), Coord::new(100, 100)));
208
209        let bounds = bounds!((45, 47), 100_isize, 100_i32);
210        assert_eq!(bounds, Rect::new(Coord::new(45, 47), Coord::new(145, 147)));
211    }
212}