From d4204f03c0320f966548b928f0650dfe2d703cff Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 13 May 2020 21:36:15 +0200 Subject: [PATCH] Refactor: break out Layout to own struct/file --- emigui/src/containers/collapsing_header.rs | 2 +- emigui/src/containers/menu.rs | 3 +- emigui/src/examples/app.rs | 27 +++-- emigui/src/layout.rs | 128 +++++++++++++++++++-- emigui/src/lib.rs | 2 +- emigui/src/ui.rs | 96 ++++------------ emigui/src/widgets.rs | 2 +- emigui/src/widgets/slider.rs | 3 +- example_glium/src/main.rs | 2 +- example_wasm/src/lib.rs | 3 +- 10 files changed, 173 insertions(+), 95 deletions(-) diff --git a/emigui/src/containers/collapsing_header.rs b/emigui/src/containers/collapsing_header.rs index 7b2e40d3..9a2ad29d 100644 --- a/emigui/src/containers/collapsing_header.rs +++ b/emigui/src/containers/collapsing_header.rs @@ -46,7 +46,7 @@ impl CollapsingHeader { impl CollapsingHeader { pub fn show(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui)) -> GuiResponse { assert!( - ui.direction() == Direction::Vertical, + ui.layout().dir() == Direction::Vertical, "Horizontal collapsing is unimplemented" ); let Self { diff --git a/emigui/src/containers/menu.rs b/emigui/src/containers/menu.rs index e6a69147..0b5c0373 100644 --- a/emigui/src/containers/menu.rs +++ b/emigui/src/containers/menu.rs @@ -79,8 +79,7 @@ pub fn menu(ui: &mut Ui, title: impl Into, add_contents: impl FnOnce(&mu style.interact.hovered.corner_radius = 0.0; style.interact.inactive.corner_radius = 0.0; ui.set_style(style); - ui.set_align(Align::Justified); - + ui.set_layout(Layout::justified(Direction::Vertical)); add_contents(ui) }) }) diff --git a/emigui/src/examples/app.rs b/emigui/src/examples/app.rs index e06e7a53..3558af6d 100644 --- a/emigui/src/examples/app.rs +++ b/emigui/src/examples/app.rs @@ -445,22 +445,27 @@ use crate::layout::*; #[serde(default)] struct LayoutExample { dir: Direction, - align: Align, + align: Option, // None == jusitifed } impl Default for LayoutExample { fn default() -> Self { Self { dir: Direction::Vertical, - align: Align::Min, + align: Some(Align::Center), } } } impl LayoutExample { pub fn ui(&mut self, ui: &mut Ui) { - ui.set_direction(self.dir); - ui.set_align(self.align); + Resize::default() + .default_size(vec2(200.0, 200.0)) + .show(ui, |ui| self.contents_ui(ui)); + } + + pub fn contents_ui(&mut self, ui: &mut Ui) { + ui.set_layout(Layout::from_dir_align(self.dir, self.align)); ui.add(label!("Available space: {:?}", ui.available().size())); if ui.add(Button::new("Reset")).clicked { @@ -481,19 +486,27 @@ impl LayoutExample { } ui.add(Separator::new()); + ui.add(label!("Align:")); - for &align in &[Align::Min, Align::Center, Align::Max, Align::Justified] { + for &align in &[Align::Min, Align::Center, Align::Max] { if ui .add(RadioButton::new( - self.align == align, + self.align == Some(align), format!("{:?}", align), )) .clicked { - self.align = align; + self.align = Some(align); } } + if ui + .add(RadioButton::new(self.align == None, "Justified")) + .tooltip_text("Try to fill full width/heigth (e.g. buttons)") + .clicked + { + self.align = None; + } } } diff --git a/emigui/src/layout.rs b/emigui/src/layout.rs index 7ba26bf2..fb6fa09b 100644 --- a/emigui/src/layout.rs +++ b/emigui/src/layout.rs @@ -1,6 +1,6 @@ use serde_derive::{Deserialize, Serialize}; -use crate::math::*; +use crate::{math::*, style::Style}; // ---------------------------------------------------------------------------- @@ -29,10 +29,6 @@ pub enum Align { /// Right/Bottom /// Note: requires a bounded/known available_width. Max, - - /// Full width/height. - /// Use this when you want - Justified, } impl Default for Align { @@ -41,17 +37,135 @@ impl Default for Align { } } +/// Used e.g. to anchor a piece of text to a part of the rectangle. /// Give a position within the rect, specified by the aligns pub fn align_rect(rect: Rect, align: (Align, Align)) -> Rect { let x = match align.0 { - Align::Min | Align::Justified => rect.left(), + Align::Min => rect.left(), Align::Center => rect.left() - 0.5 * rect.width(), Align::Max => rect.left() - rect.width(), }; let y = match align.1 { - Align::Min | Align::Justified => rect.top(), + Align::Min => rect.top(), Align::Center => rect.top() - 0.5 * rect.height(), Align::Max => rect.top() - rect.height(), }; Rect::from_min_size(pos2(x, y), rect.size()) } + +// ---------------------------------------------------------------------------- + +#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)] +pub struct Layout { + /// Lay out things horizontally or vertically? + dir: Direction, + + /// For vertical layouts: put things to left, center or right? + /// For horizontal layouts: put things to top, center or bottom? + /// None means justified, which means full width (vertical layout) or height (horizontal layouts). + align: Option, +} + +impl Default for Layout { + fn default() -> Self { + Self { + dir: Direction::Vertical, + align: Some(Align::Min), + } + } +} + +impl Layout { + /// None align means justified, e.g. fill full width/height. + pub fn from_dir_align(dir: Direction, align: Option) -> Self { + Self { dir, align } + } + + pub fn vertical(align: Align) -> Self { + Self { + dir: Direction::Vertical, + align: Some(align), + } + } + + pub fn horizontal(align: Align) -> Self { + Self { + dir: Direction::Horizontal, + align: Some(align), + } + } + + /// Full-width layout. + /// Nice for menues etc where each button is full width. + pub fn justified(dir: Direction) -> Self { + Self { dir, align: None } + } + + pub fn dir(&self) -> Direction { + self.dir + } + + /// Reserve this much space and move the cursor. + /// Returns where to put the widget. + /// + /// # How sizes are negotiated + /// Each widget should have a *minimum desired size* and a *desired size*. + /// When asking for space, ask AT LEAST for you minimum, and don't ask for more than you need. + /// If you want to fill the space, ask about `available().size()` and use that. + /// + /// You may get MORE space than you asked for, for instance + /// for `Justified` aligned layouts, like in menus. + /// + /// You may get LESS space than you asked for if the current layout won't fit what you asked for. + pub fn allocate_space( + &self, + cursor: &mut Pos2, + style: &Style, + available_size: Vec2, + mut child_size: Vec2, + ) -> Rect { + let available_size = available_size.max(child_size); + + let mut child_pos = *cursor; + if self.dir == Direction::Horizontal { + if let Some(align) = self.align { + if align != Align::Min { + debug_assert!(available_size.y.is_finite()); + debug_assert!(child_size.y.is_finite()); + } + + child_pos.y += match align { + Align::Min => 0.0, + Align::Center => 0.5 * (available_size.y - child_size.y), + Align::Max => available_size.y - child_size.y, + }; + } else { + // justified: fill full height + child_size.y = child_size.y.max(available_size.y); + } + + cursor.x += child_size.x; + cursor.x += style.item_spacing.x; // Where to put next thing, if there is a next thing + } else { + if let Some(align) = self.align { + if align != Align::Min { + debug_assert!(available_size.y.is_finite()); + debug_assert!(child_size.y.is_finite()); + } + + child_pos.x += match align { + Align::Min => 0.0, + Align::Center => 0.5 * (available_size.x - child_size.x), + Align::Max => available_size.x - child_size.x, + }; + } else { + // justified: fill full width + child_size.x = child_size.x.max(available_size.x); + }; + cursor.y += child_size.y; + cursor.y += style.item_spacing.y; // Where to put next thing, if there is a next thing + } + + Rect::from_min_size(child_pos, child_size) + } +} diff --git a/emigui/src/lib.rs b/emigui/src/lib.rs index 195f18ef..baa957ad 100644 --- a/emigui/src/lib.rs +++ b/emigui/src/lib.rs @@ -49,7 +49,7 @@ pub use { id::Id, input::*, layers::*, - layout::Align, + layout::*, math::*, memory::Memory, mesher::{Mesh, PaintBatches, Vertex}, diff --git a/emigui/src/ui.rs b/emigui/src/ui.rs index 313665ee..55086f90 100644 --- a/emigui/src/ui.rs +++ b/emigui/src/ui.rs @@ -41,18 +41,14 @@ pub struct Ui { /// Overide default style in this ui style: Style, - // Layout stuff follows. TODO: move to own type and abstract. - /// Doesn't change. - dir: Direction, - - align: Align, + layout: Layout, /// Where the next widget will be put. /// Progresses along self.dir. /// Initially set to rect.min /// If something has already been added, this will point ot style.item_spacing beyond the latest child. /// The cursor can thus be style.item_spacing pixels outside of the child_bounds. - cursor: Pos2, + cursor: Pos2, // TODO: move into Layout? } impl Ui { @@ -69,9 +65,8 @@ impl Ui { desired_rect: rect, child_bounds: Rect::from_min_size(rect.min, Vec2::zero()), // TODO: Rect::nothing() ? style, + layout: Default::default(), cursor: rect.min, - dir: Direction::Vertical, - align: Align::Min, } } @@ -82,15 +77,14 @@ impl Ui { let clip_rect = self.clip_rect(); // Keep it unless the child explciitly desires differently Ui { ctx: self.ctx.clone(), - layer: self.layer, - style: self.style, id: self.id, + layer: self.layer, clip_rect, desired_rect: child_rect, - cursor: child_rect.min, child_bounds: Rect::from_min_size(child_rect.min, Vec2::zero()), // TODO: Rect::nothing() ? - dir: self.dir, - align: self.align, + style: self.style, + layout: self.layout, + cursor: child_rect.min, } } @@ -248,19 +242,13 @@ impl Ui { Rect::from_min_max(self.cursor, self.finite_bottom_right()) } - // TODO: remove - pub fn direction(&self) -> Direction { - self.dir + pub fn layout(&self) -> &Layout { + &self.layout } // TODO: remove - pub fn set_direction(&mut self, dir: Direction) { - self.dir = dir; - } - - // TODO: remove - pub fn set_align(&mut self, align: Align) { - self.align = align; + pub fn set_layout(&mut self, layout: Layout) { + self.layout = layout; } // ------------------------------------------------------------------------ @@ -332,6 +320,7 @@ impl Ui { /// Reserve this much space and move the cursor. /// Returns where to put the widget. + /// /// # How sizes are negotiated /// Each widget should have a *minimum desired size* and a *desired size*. /// When asking for space, ask AT LEAST for you minimum, and don't ask for more than you need. @@ -384,40 +373,13 @@ impl Ui { /// Reserve this much space and move the cursor. /// Returns where to put the widget. - fn reserve_space_impl(&mut self, mut child_size: Vec2) -> Rect { + fn reserve_space_impl(&mut self, child_size: Vec2) -> Rect { let available_size = self.available_finite().size(); - let available_size = available_size.max(child_size); - - let mut child_pos = self.cursor; - if self.dir == Direction::Horizontal { - child_pos.y += match self.align { - Align::Min | Align::Justified => 0.0, - Align::Center => 0.5 * (available_size.y - child_size.y), - Align::Max => available_size.y - child_size.y, - }; - if self.align == Align::Justified && available_size.y.is_finite() { - // Fill full height - child_size.y = child_size.y.max(available_size.y); - } - self.child_bounds.extend_with(self.cursor + child_size); - self.cursor.x += child_size.x; - self.cursor.x += self.style.item_spacing.x; // Where to put next thing, if there is a next thing - } else { - child_pos.x += match self.align { - Align::Min | Align::Justified => 0.0, - Align::Center => 0.5 * (available_size.x - child_size.x), - Align::Max => available_size.x - child_size.x, - }; - if self.align == Align::Justified && available_size.x.is_finite() { - // Fill full width - child_size.x = child_size.x.max(available_size.x); - } - self.child_bounds.extend_with(self.cursor + child_size); - self.cursor.y += child_size.y; - self.cursor.y += self.style.item_spacing.y; // Where to put next thing, if there is a next thing - } - - Rect::from_min_size(child_pos, child_size) + let child_rect = + self.layout + .allocate_space(&mut self.cursor, &self.style, available_size, child_size); + self.child_bounds = self.child_bounds.union(child_rect); + child_rect } // ------------------------------------------------ @@ -564,14 +526,13 @@ impl Ui { add_contents: impl FnOnce(&mut Ui), ) -> InteractInfo { assert!( - self.dir == Direction::Vertical, + self.layout().dir() == Direction::Vertical, "You can only indent vertical layouts" ); let indent = vec2(self.style.indent, 0.0); let child_rect = Rect::from_min_max(self.cursor + indent, self.bottom_right()); let mut child_ui = Ui { id: self.id.with(id_source), - align: Align::Min, ..self.child_ui(child_rect) }; add_contents(&mut child_ui); @@ -603,15 +564,11 @@ impl Ui { } /// A column ui with a given width. - pub fn column(&mut self, column_position: Align, mut width: f32) -> Ui { + pub fn column(&mut self, column_position: Align, width: f32) -> Ui { let x = match column_position { Align::Min => 0.0, Align::Center => self.available().width() / 2.0 - width / 2.0, Align::Max => self.available().width() - width, - Align::Justified => { - width = self.available().width(); - 0.0 - } }; self.child_ui(Rect::from_min_size( self.cursor + vec2(x, 0.0), @@ -621,24 +578,22 @@ impl Ui { /// Start a ui with horizontal layout pub fn horizontal(&mut self, add_contents: impl FnOnce(&mut Ui)) -> InteractInfo { - self.inner_layout(Direction::Horizontal, Align::Min, add_contents) + self.inner_layout(Layout::horizontal(Align::Min), add_contents) } /// Start a ui with vertical layout pub fn vertical(&mut self, add_contents: impl FnOnce(&mut Ui)) -> InteractInfo { - self.inner_layout(Direction::Vertical, Align::Min, add_contents) + self.inner_layout(Layout::vertical(Align::Min), add_contents) } pub fn inner_layout( &mut self, - dir: Direction, - align: Align, + layout: Layout, add_contents: impl FnOnce(&mut Self), ) -> InteractInfo { let child_rect = Rect::from_min_max(self.cursor, self.bottom_right()); let mut child_ui = Self { - dir, - align, + layout, ..self.child_ui(child_rect) }; add_contents(&mut child_ui); @@ -646,7 +601,7 @@ impl Ui { self.reserve_space(size, None) } - /// Temporarily split split a vertical layout into several columns. + /// Temporarily split split an Ui into several columns. /// /// ``` ignore /// ui.columns(2, |columns| { @@ -671,7 +626,6 @@ impl Ui { Self { id: self.make_child_id(&("column", col_idx)), - dir: Direction::Vertical, ..self.child_ui(child_rect) } }) diff --git a/emigui/src/widgets.rs b/emigui/src/widgets.rs index 4509c827..3a9a981c 100644 --- a/emigui/src/widgets.rs +++ b/emigui/src/widgets.rs @@ -421,7 +421,7 @@ impl Widget for Separator { let available_space = ui.available_finite().size(); let extra = self.extra; - let (points, interact) = match ui.direction() { + let (points, interact) = match ui.layout().dir() { Direction::Horizontal => { let interact = ui.reserve_space(vec2(self.min_spacing, available_space.y), None); ( diff --git a/emigui/src/widgets/slider.rs b/emigui/src/widgets/slider.rs index 78a6c2e5..657bdf08 100644 --- a/emigui/src/widgets/slider.rs +++ b/emigui/src/widgets/slider.rs @@ -128,8 +128,7 @@ impl<'a> Widget for Slider<'a> { // Place the text in line with the slider on the left: columns[1].set_desired_height(slider_response.rect.height()); - columns[1].horizontal(|ui| { - ui.set_align(Align::Center); + columns[1].inner_layout(Layout::horizontal(Align::Center), |ui| { ui.add(Label::new(full_text).multiline(false)); }); diff --git a/example_glium/src/main.rs b/example_glium/src/main.rs index ab1e1b1d..ccaff00d 100644 --- a/example_glium/src/main.rs +++ b/example_glium/src/main.rs @@ -111,7 +111,7 @@ fn main() { let mut ui = ctx.fullscreen_ui(); example_app.ui(&mut ui); let mut ui = ui.centered_column(ui.available().width().min(480.0)); - ui.set_align(Align::Min); + ui.set_layout(Layout::vertical(Align::Min)); ui.add(label!("Emigui running inside of Glium").text_style(emigui::TextStyle::Heading)); if ui.add(Button::new("Quit")).clicked { running = false; diff --git a/example_wasm/src/lib.rs b/example_wasm/src/lib.rs index 6ca3d6d4..2437bea7 100644 --- a/example_wasm/src/lib.rs +++ b/example_wasm/src/lib.rs @@ -43,7 +43,7 @@ impl State { let mut ui = self.ctx.fullscreen_ui(); self.example_app.ui(&mut ui); let mut ui = ui.centered_column(ui.available().width().min(480.0)); - ui.set_align(Align::Min); + ui.set_layout(Layout::vertical(Align::Min)); ui.add(label!("Emigui!").text_style(TextStyle::Heading)); ui.add_label("Emigui is an immediate mode GUI written in Rust, compiled to WebAssembly, rendered with WebGL."); ui.add_label( @@ -57,7 +57,6 @@ impl State { }); ui.add(Separator::new()); - ui.set_align(Align::Min); ui.add_label("WebGl painter info:"); ui.indent("webgl region id", |ui| { ui.add_label(self.webgl_painter.debug_info());