diff --git a/egui/src/align.rs b/egui/src/align.rs index 4324e7e1..25cbe5a0 100644 --- a/egui/src/align.rs +++ b/egui/src/align.rs @@ -16,6 +16,21 @@ pub enum Align { Max, } +impl Align { + pub fn left() -> Self { + Self::Min + } + pub fn right() -> Self { + Self::Max + } + pub fn top() -> Self { + Self::Min + } + pub fn bottom() -> Self { + Self::Max + } +} + impl Default for Align { fn default() -> Align { Align::Min diff --git a/egui/src/containers/collapsing_header.rs b/egui/src/containers/collapsing_header.rs index 3531b792..011a2443 100644 --- a/egui/src/containers/collapsing_header.rs +++ b/egui/src/containers/collapsing_header.rs @@ -1,7 +1,6 @@ use std::hash::Hash; use crate::{ - layout::Direction, paint::{PaintCmd, TextStyle}, widgets::Label, *, @@ -168,7 +167,7 @@ struct Prepared { impl CollapsingHeader { fn begin(self, ui: &mut Ui) -> Prepared { assert!( - ui.layout().dir() == Direction::Vertical, + ui.layout().main_dir().is_vertical(), "Horizontal collapsing is unimplemented" ); let Self { diff --git a/egui/src/containers/combo_box.rs b/egui/src/containers/combo_box.rs index 8c894150..ec0478c2 100644 --- a/egui/src/containers/combo_box.rs +++ b/egui/src/containers/combo_box.rs @@ -60,10 +60,13 @@ pub fn combo_box( let frame = Frame::popup(ui.style()); let frame_margin = frame.margin; frame.show(ui, |ui| { - ui.with_layout(Layout::justified(Direction::Vertical), |ui| { - ui.set_min_width(button_response.rect.width() - 2.0 * frame_margin.x); - menu_contents(ui); - }); + ui.with_layout( + Layout::top_down(Align::left()).with_cross_justify(true), + |ui| { + ui.set_min_width(button_response.rect.width() - 2.0 * frame_margin.x); + menu_contents(ui); + }, + ); }) }); diff --git a/egui/src/demos/demo_window.rs b/egui/src/demos/demo_window.rs index 76e10a45..e2b2ddf6 100644 --- a/egui/src/demos/demo_window.rs +++ b/egui/src/demos/demo_window.rs @@ -44,7 +44,7 @@ impl DemoWindow { }); CollapsingHeader::new("Layout") - .default_open(false) + .default_open(true) .show(ui, |ui| self.layout.ui(ui)); CollapsingHeader::new("Tree") @@ -304,29 +304,25 @@ use crate::layout::*; #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] struct LayoutDemo { - dir: Direction, - align: Option, // None == justified - reversed: bool, + // Identical to contents of `egui::Layout` + main_dir: Direction, + cross_align: Align, + cross_justify: bool, } impl Default for LayoutDemo { fn default() -> Self { Self { - dir: Direction::Vertical, - align: Some(Align::Center), - reversed: false, + main_dir: Direction::TopDown, + cross_align: Align::Min, + cross_justify: false, } } } impl LayoutDemo { fn layout(&self) -> Layout { - let layout = Layout::from_dir_align(self.dir, self.align); - if self.reversed { - layout.reverse() - } else { - layout - } + Layout::from_parts(self.main_dir, self.cross_align, self.cross_justify) } pub fn ui(&mut self, ui: &mut Ui) { @@ -347,39 +343,24 @@ impl LayoutDemo { // TODO: enum iter - for &dir in &[Direction::Horizontal, Direction::Vertical] { - if ui - .add(RadioButton::new(self.dir == dir, format!("{:?}", dir))) - .clicked - { - self.dir = dir; - } + for &dir in &[ + Direction::LeftToRight, + Direction::RightToLeft, + Direction::TopDown, + Direction::BottomUp, + ] { + ui.radio_value(&mut self.main_dir, dir, format!("{:?}", dir)); } - ui.checkbox(&mut self.reversed, "Reversed"); - ui.separator(); ui.label("Align:"); - for &align in &[Align::Min, Align::Center, Align::Max] { - if ui - .add(RadioButton::new( - self.align == Some(align), - format!("{:?}", align), - )) - .clicked - { - self.align = Some(align); - } - } - if ui - .add(RadioButton::new(self.align == None, "Justified")) - .on_hover_text("Try to fill full width/height (e.g. buttons)") - .clicked - { - self.align = None; + ui.radio_value(&mut self.cross_align, align, format!("{:?}", align)); } + + ui.checkbox(&mut self.cross_justify, "Justified") + .on_hover_text("Try to fill full width/height (e.g. buttons)"); } } diff --git a/egui/src/demos/demo_windows.rs b/egui/src/demos/demo_windows.rs index 77840c95..03121777 100644 --- a/egui/src/demos/demo_windows.rs +++ b/egui/src/demos/demo_windows.rs @@ -345,7 +345,7 @@ fn show_menu_bar(ui: &mut Ui, windows: &mut OpenWindows, seconds_since_midnight: (time % 1.0 * 100.0).floor() ); - ui.with_layout(Layout::horizontal(Align::Center).reverse(), |ui| { + ui.with_layout(Layout::right_to_left(), |ui| { if ui .add(Button::new(time).text_style(TextStyle::Monospace)) .clicked diff --git a/egui/src/layout.rs b/egui/src/layout.rs index 61b283fc..f3f32c38 100644 --- a/egui/src/layout.rs +++ b/egui/src/layout.rs @@ -64,18 +64,30 @@ impl Region { // ---------------------------------------------------------------------------- -/// `Layout` direction (horizontal or vertical). +/// Main layout direction #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] pub enum Direction { - Horizontal, - Vertical, + LeftToRight, + RightToLeft, + TopDown, + BottomUp, } -impl Default for Direction { - fn default() -> Direction { - Direction::Vertical +impl Direction { + pub fn is_horizontal(self) -> bool { + match self { + Direction::LeftToRight | Direction::RightToLeft => true, + Direction::TopDown | Direction::BottomUp => false, + } + } + + pub fn is_vertical(self) -> bool { + match self { + Direction::LeftToRight | Direction::RightToLeft => false, + Direction::TopDown | Direction::BottomUp => true, + } } } @@ -85,111 +97,133 @@ impl Default for Direction { #[derive(Clone, Copy, Debug, PartialEq)] // #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct Layout { - /// Lay out things horizontally or vertically? Main axis. - dir: Direction, + /// Main axis direction + main_dir: Direction, /// How to align things on the cross axis. /// 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, + cross_align: Align, - /// Lay out things in reversed order, i.e. from the right or bottom-up. - reversed: bool, + /// Justify the cross axis? + /// For vertical layouts justify mean all widgets get maximum width. + /// For horizontal layouts justify mean all widgets get maximum height. + cross_justify: bool, } impl Default for Layout { fn default() -> Self { + // TODO: Get from `Style` instead. + // This is a very euro-centric default. Self { - dir: Direction::Vertical, - align: Some(Align::Min), - reversed: false, + main_dir: Direction::TopDown, + cross_align: Align::left(), + cross_justify: false, } } } impl Layout { /// None align means justified, e.g. fill full width/height. - pub fn from_dir_align(dir: Direction, align: Option) -> Self { + pub(crate) fn from_parts(main_dir: Direction, cross_align: Align, cross_justify: bool) -> Self { Self { - dir, - align, - reversed: false, + main_dir, + cross_align, + cross_justify, } } - pub fn vertical(align: Align) -> Self { + #[deprecated = "Use `top_down`"] + pub fn vertical(cross_align: Align) -> Self { + Self::top_down(cross_align) + } + + #[deprecated = "Use `left_to_right`"] + pub fn horizontal(cross_align: Align) -> Self { + Self::left_to_right().with_cross_align(cross_align) + } + + pub fn left_to_right() -> Self { Self { - dir: Direction::Vertical, - align: Some(align), - reversed: false, + main_dir: Direction::LeftToRight, + cross_align: Align::Center, + cross_justify: false, } } - pub fn horizontal(align: Align) -> Self { + pub fn right_to_left() -> Self { Self { - dir: Direction::Horizontal, - align: Some(align), - reversed: false, + main_dir: Direction::RightToLeft, + cross_align: Align::Center, + cross_justify: false, } } - /// Full-width layout. - /// Nice for menus etc where each button is full width. - pub fn justified(dir: Direction) -> Self { + pub fn top_down(cross_align: Align) -> Self { Self { - dir, - align: None, - reversed: false, + main_dir: Direction::TopDown, + cross_align, + cross_justify: false, } } - #[must_use] - pub fn reverse(self) -> Self { + pub fn bottom_up(cross_align: Align) -> Self { Self { - dir: self.dir, - align: self.align, - reversed: !self.reversed, + main_dir: Direction::BottomUp, + cross_align, + cross_justify: false, } } - #[must_use] - pub fn with_reversed(self, reversed: bool) -> Self { - if reversed { - self.reverse() - } else { - self + pub fn with_cross_align(self, cross_align: Align) -> Self { + Self { + cross_align, + ..self } } - pub fn dir(self) -> Direction { - self.dir + pub fn with_cross_justify(self, cross_justify: bool) -> Self { + Self { + cross_justify, + ..self + } } - pub fn align(self) -> Option { - self.align + // ------------------------------------------------------------------------ + + pub fn main_dir(self) -> Direction { + self.main_dir } - pub fn is_reversed(self) -> bool { - self.reversed + pub fn cross_align(self) -> Align { + self.cross_align } + pub fn cross_justify(self) -> bool { + self.cross_justify + } + + pub fn is_horizontal(self) -> bool { + self.main_dir().is_horizontal() + } + + pub fn is_vertical(self) -> bool { + self.main_dir().is_vertical() + } + + pub fn prefer_right_to_left(self) -> bool { + self.main_dir == Direction::RightToLeft + || self.main_dir.is_vertical() && self.cross_align == Align::Max + } + + // ------------------------------------------------------------------------ + fn initial_cursor(self, max_rect: Rect) -> Pos2 { - match self.dir { - Direction::Horizontal => { - if self.reversed { - max_rect.right_top() - } else { - max_rect.left_top() - } - } - Direction::Vertical => { - if self.reversed { - max_rect.left_bottom() - } else { - max_rect.left_top() - } - } + match self.main_dir { + Direction::LeftToRight => max_rect.left_top(), + Direction::RightToLeft => max_rect.right_top(), + Direction::TopDown => max_rect.left_top(), + Direction::BottomUp => max_rect.left_bottom(), } } @@ -215,52 +249,45 @@ impl Layout { /// for the next widget? fn available_from_cursor_max_rect(self, cursor: Pos2, max_rect: Rect) -> Rect { let mut rect = max_rect; - match self.dir { - Direction::Horizontal => { - rect.min.y = cursor.y; - if self.reversed { - rect.max.x = cursor.x; - } else { - rect.min.x = cursor.x; - } - } - Direction::Vertical => { + + match self.main_dir { + Direction::LeftToRight => { rect.min.x = cursor.x; - if self.reversed { - rect.max.y = cursor.y; - } else { - rect.min.y = cursor.y; - } + rect.min.y = cursor.y; + } + Direction::RightToLeft => { + rect.max.x = cursor.x; + rect.min.y = cursor.y; + } + Direction::TopDown => { + rect.min.x = cursor.x; + rect.min.y = cursor.y; + } + Direction::BottomUp => { + rect.min.x = cursor.x; + rect.max.y = cursor.y; } } + rect } /// Advance the cursor by this many points. pub fn advance_cursor(self, region: &mut Region, amount: f32) { - match self.dir() { - Direction::Horizontal => { - if self.is_reversed() { - region.cursor.x -= amount; - } else { - region.cursor.x += amount; - } - } - Direction::Vertical => { - if self.is_reversed() { - region.cursor.y -= amount; - } else { - region.cursor.y += amount; - } - } + match self.main_dir { + Direction::LeftToRight => region.cursor.x += amount, + Direction::RightToLeft => region.cursor.x -= amount, + Direction::TopDown => region.cursor.y += amount, + Direction::BottomUp => region.cursor.y -= amount, } } /// Advance the cursor by this spacing pub fn advance_cursor2(self, region: &mut Region, amount: Vec2) { - match self.dir() { - Direction::Horizontal => self.advance_cursor(region, amount.x), - Direction::Vertical => self.advance_cursor(region, amount.y), + if self.main_dir.is_horizontal() { + self.advance_cursor(region, amount.x) + } else { + self.advance_cursor(region, amount.y) } } @@ -282,49 +309,39 @@ impl Layout { let mut child_size = minimum_child_size; let mut child_move = Vec2::default(); - let mut cursor_change = Vec2::default(); - match self.dir { - Direction::Horizontal => { - if let Some(align) = self.align { - child_move.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_change.x += child_size.x; - } - Direction::Vertical => { - if let Some(align) = self.align { - child_move.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); + if self.main_dir.is_horizontal() { + if self.cross_justify { + // fill full height + child_size.y = child_size.y.max(available_size.y); + } else { + child_move.y += match self.cross_align { + Align::Min => 0.0, + Align::Center => 0.5 * (available_size.y - child_size.y), + Align::Max => available_size.y - child_size.y, + }; + } + } else { + if self.cross_justify { + // justified: fill full width + child_size.x = child_size.x.max(available_size.x); + } else { + child_move.x += match self.cross_align { + Align::Min => 0.0, + Align::Center => 0.5 * (available_size.x - child_size.x), + Align::Max => available_size.x - child_size.x, }; - cursor_change.y += child_size.y; } } - if self.is_reversed() { - let child_pos = region.cursor + child_move; - let child_pos = match self.dir { - Direction::Horizontal => child_pos + vec2(-child_size.x, 0.0), - Direction::Vertical => child_pos + vec2(0.0, -child_size.y), - }; - Rect::from_min_size(child_pos, child_size) - } else { - let child_pos = region.cursor + child_move; - Rect::from_min_size(child_pos, child_size) - } + let child_pos = match self.main_dir { + Direction::LeftToRight => region.cursor + child_move, + Direction::RightToLeft => region.cursor + child_move + vec2(-child_size.x, 0.0), + Direction::TopDown => region.cursor + child_move, + Direction::BottomUp => region.cursor + child_move + vec2(0.0, -child_size.y), + }; + + Rect::from_min_size(child_pos, child_size) } } @@ -343,24 +360,22 @@ impl Layout { let align; - match self.dir { - Direction::Horizontal => { - if self.reversed { - painter.debug_arrow(cursor, vec2(-1.0, 0.0), stroke); - align = (Align::Max, Align::Min); - } else { - painter.debug_arrow(cursor, vec2(1.0, 0.0), stroke); - align = (Align::Min, Align::Min); - } + match self.main_dir { + Direction::LeftToRight => { + painter.debug_arrow(cursor, vec2(1.0, 0.0), stroke); + align = (Align::Min, Align::Min); } - Direction::Vertical => { - if self.reversed { - painter.debug_arrow(cursor, vec2(0.0, -1.0), stroke); - align = (Align::Min, Align::Max); - } else { - painter.debug_arrow(cursor, vec2(0.0, 1.0), stroke); - align = (Align::Min, Align::Min); - } + Direction::RightToLeft => { + painter.debug_arrow(cursor, vec2(-1.0, 0.0), stroke); + align = (Align::Max, Align::Min); + } + Direction::TopDown => { + painter.debug_arrow(cursor, vec2(0.0, 1.0), stroke); + align = (Align::Min, Align::Min); + } + Direction::BottomUp => { + painter.debug_arrow(cursor, vec2(0.0, -1.0), stroke); + align = (Align::Min, Align::Max); } } diff --git a/egui/src/menu.rs b/egui/src/menu.rs index 11be4ddf..ac5a8646 100644 --- a/egui/src/menu.rs +++ b/egui/src/menu.rs @@ -111,7 +111,10 @@ fn menu_impl<'c>( style.visuals.widgets.inactive.bg_fill = TRANSPARENT; style.visuals.widgets.inactive.bg_stroke = Stroke::none(); ui.set_style(style); - ui.with_layout(Layout::justified(Direction::Vertical), add_contents); + ui.with_layout( + Layout::top_down(Align::left()).with_cross_justify(true), + add_contents, + ); }) }); diff --git a/egui/src/ui.rs b/egui/src/ui.rs index 1f1de60a..cdcd3891 100644 --- a/egui/src/ui.rs +++ b/egui/src/ui.rs @@ -223,7 +223,7 @@ impl Ui { /// Set the maximum width of the ui. /// You won't be able to shrink it below the current minimum size. pub fn set_max_width(&mut self, width: f32) { - if self.layout.dir() == Direction::Horizontal && self.layout.is_reversed() { + if self.layout.main_dir() == Direction::RightToLeft { debug_assert_eq!(self.min_rect().max.x, self.max_rect().max.x); self.region.max_rect.min.x = self.region.max_rect.max.x - width.at_least(self.min_rect().width()); @@ -237,7 +237,7 @@ impl Ui { /// Set the maximum height of the ui. /// You won't be able to shrink it below the current minimum size. pub fn set_max_height(&mut self, height: f32) { - if self.layout.dir() == Direction::Vertical && self.layout.is_reversed() { + if self.layout.main_dir() == Direction::BottomUp { debug_assert_eq!(self.min_rect().max.y, self.region.max_rect.max.y); self.region.max_rect.min.y = self.region.max_rect.max.y - height.at_least(self.min_rect().height()); @@ -260,7 +260,7 @@ impl Ui { /// Set the minimum width of the ui. /// This can't shrink the ui, only make it larger. pub fn set_min_width(&mut self, width: f32) { - if self.layout.dir() == Direction::Horizontal && self.layout.is_reversed() { + if self.layout.main_dir() == Direction::RightToLeft { debug_assert_eq!(self.region.min_rect.max.x, self.region.max_rect.max.x); let min_rect = &mut self.region.min_rect; min_rect.min.x = min_rect.min.x.min(min_rect.max.x - width); @@ -275,7 +275,7 @@ impl Ui { /// Set the minimum height of the ui. /// This can't shrink the ui, only make it larger. pub fn set_min_height(&mut self, height: f32) { - if self.layout.dir() == Direction::Vertical && self.layout.is_reversed() { + if self.layout.main_dir() == Direction::BottomUp { debug_assert_eq!(self.region.min_rect.max.y, self.region.max_rect.max.y); let min_rect = &mut self.region.min_rect; min_rect.min.y = min_rect.min.y.min(min_rect.max.y - height); @@ -733,8 +733,9 @@ impl Ui { add_contents: impl FnOnce(&mut Ui) -> R, ) -> (R, Response) { assert!( - self.layout().dir() == Direction::Vertical, - "You can only indent vertical layouts" + self.layout.is_vertical(), + "You can only indent vertical layouts, found {:?}", + self.layout ); let indent = vec2(self.style().spacing.indent, 0.0); let child_rect = @@ -805,22 +806,19 @@ impl Ui { self.style().spacing.interact_size.y, // Assume there will be something interactive on the horizontal layout ); - let right_to_left = - (self.layout.dir(), self.layout.align()) == (Direction::Vertical, Some(Align::Max)); + let layout = if self.layout.prefer_right_to_left() { + Layout::right_to_left() + } else { + Layout::left_to_right() + }; - self.allocate_ui_min(initial_size, |ui| { - ui.with_layout( - Layout::horizontal(Align::Center).with_reversed(right_to_left), - add_contents, - ) - .0 - }) + self.allocate_ui_min(initial_size, |ui| ui.with_layout(layout, add_contents).0) } /// Start a ui with vertical layout. /// Widgets will be left-justified. pub fn vertical(&mut self, add_contents: impl FnOnce(&mut Ui) -> R) -> (R, Response) { - self.with_layout(Layout::vertical(Align::Min), add_contents) + self.with_layout(Layout::top_down(Align::Min), add_contents) } pub fn with_layout( diff --git a/egui/src/widgets/mod.rs b/egui/src/widgets/mod.rs index a8772bdb..04bd44b2 100644 --- a/egui/src/widgets/mod.rs +++ b/egui/src/widgets/mod.rs @@ -6,7 +6,7 @@ #![allow(clippy::new_without_default)] -use crate::{layout::Direction, *}; +use crate::*; pub mod color_picker; mod drag_value; @@ -606,27 +606,24 @@ impl Widget for Separator { let available_space = ui.available_finite().size(); - let (points, rect) = match ui.layout().dir() { - Direction::Horizontal => { - let rect = ui.allocate_space(vec2(spacing, available_space.y)); - ( - [ - pos2(rect.center().x, rect.top()), - pos2(rect.center().x, rect.bottom()), - ], - rect, - ) - } - Direction::Vertical => { - let rect = ui.allocate_space(vec2(available_space.x, spacing)); - ( - [ - pos2(rect.left(), rect.center().y), - pos2(rect.right(), rect.center().y), - ], - rect, - ) - } + let (points, rect) = if ui.layout().main_dir().is_horizontal() { + let rect = ui.allocate_space(vec2(spacing, available_space.y)); + ( + [ + pos2(rect.center().x, rect.top()), + pos2(rect.center().x, rect.bottom()), + ], + rect, + ) + } else { + let rect = ui.allocate_space(vec2(available_space.x, spacing)); + ( + [ + pos2(rect.left(), rect.center().y), + pos2(rect.right(), rect.center().y), + ], + rect, + ) }; let stroke = ui.style().visuals.widgets.noninteractive.bg_stroke; ui.painter().line_segment(points, stroke);