diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d93815a..63f2b1b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). * Add support for secondary and middle mouse buttons. * Add `Label` methods for code, strong, strikethrough, underline and italics. * Add `ui.group(|ui| { … })` to visually group some widgets within a frame. +* Add `Ui` helpers for doing manual layout (`ui.put`, `ui.allocate_ui_at_rect` and more). * Add `ui.set_enabled(false)` to disable all widgets in a `Ui` (grayed out and non-interactive). * Add `TextEdit::hint_text` for showing a weak hint text when empty. * `egui::popup::popup_below_widget`: show a popup area below another widget. diff --git a/egui/src/grid.rs b/egui/src/grid.rs index af649625..785b1d88 100644 --- a/egui/src/grid.rs +++ b/egui/src/grid.rs @@ -136,7 +136,7 @@ impl GridLayout { Align2::LEFT_CENTER.align_size_within_rect(size, frame) } - pub(crate) fn justify_or_align(&self, frame: Rect, size: Vec2) -> Rect { + pub(crate) fn justify_and_align(&self, frame: Rect, size: Vec2) -> Rect { self.align_size_within_rect(size, frame) } diff --git a/egui/src/layout.rs b/egui/src/layout.rs index b03d14ca..e9c6cf7a 100644 --- a/egui/src/layout.rs +++ b/egui/src/layout.rs @@ -115,6 +115,12 @@ pub struct Layout { /// wrap to a new row when we reach the right side of the `max_rect`. main_wrap: bool, + /// How to align things on the main axis. + main_align: Align, + + /// Justify the main axis? + main_justify: bool, + /// 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? @@ -133,6 +139,8 @@ impl Default for Layout { Self { main_dir: Direction::TopDown, main_wrap: false, + main_align: Align::TOP, + main_justify: false, cross_align: Align::LEFT, cross_justify: false, } @@ -145,6 +153,8 @@ impl Layout { Self { main_dir: Direction::LeftToRight, main_wrap: false, + main_align: Align::Center, // looks best to e.g. center text within a button + main_justify: false, cross_align: Align::Center, cross_justify: false, } @@ -154,6 +164,8 @@ impl Layout { Self { main_dir: Direction::RightToLeft, main_wrap: false, + main_align: Align::Center, // looks best to e.g. center text within a button + main_justify: false, cross_align: Align::Center, cross_justify: false, } @@ -163,6 +175,8 @@ impl Layout { Self { main_dir: Direction::TopDown, main_wrap: false, + main_align: Align::Center, // looks best to e.g. center text within a button + main_justify: false, cross_align, cross_justify: false, } @@ -177,6 +191,8 @@ impl Layout { Self { main_dir: Direction::BottomUp, main_wrap: false, + main_align: Align::Center, // looks best to e.g. center text within a button + main_justify: false, cross_align, cross_justify: false, } @@ -186,11 +202,24 @@ impl Layout { Self { main_dir, main_wrap: false, + main_align: Align::Center, // looks best to e.g. center text within a button + main_justify: false, cross_align, cross_justify: false, } } + pub fn centered_and_justified(main_dir: Direction) -> Self { + Self { + main_dir, + main_wrap: false, + main_align: Align::Center, + main_justify: true, + cross_align: Align::Center, + cross_justify: true, + } + } + #[deprecated = "Use `top_down`"] pub fn vertical(cross_align: Align) -> Self { Self::top_down(cross_align) @@ -252,22 +281,38 @@ impl Layout { } fn horizontal_align(&self) -> Align { - match self.main_dir { - // Direction::LeftToRight => Align::LEFT, - // Direction::RightToLeft => Align::right(), - Direction::LeftToRight | Direction::RightToLeft => Align::Center, // looks better to e.g. center text within a button - - Direction::TopDown | Direction::BottomUp => self.cross_align, + if self.is_horizontal() { + self.main_align + } else { + self.cross_align } } fn vertical_align(&self) -> Align { - match self.main_dir { - // Direction::TopDown => Align::TOP, - // Direction::BottomUp => Align::BOTTOM, - Direction::TopDown | Direction::BottomUp => Align::Center, // looks better to e.g. center text within a button + if self.is_vertical() { + self.main_align + } else { + self.cross_align + } + } - Direction::LeftToRight | Direction::RightToLeft => self.cross_align, + fn align2(&self) -> Align2 { + Align2([self.horizontal_align(), self.vertical_align()]) + } + + fn horizontal_justify(&self) -> bool { + if self.is_horizontal() { + self.main_justify + } else { + self.cross_justify + } + } + + fn vertical_justify(&self) -> bool { + if self.is_vertical() { + self.main_justify + } else { + self.cross_justify } } } @@ -275,18 +320,7 @@ impl Layout { /// ## Doing layout impl Layout { pub fn align_size_within_rect(&self, size: Vec2, outer: Rect) -> Rect { - let x = match self.horizontal_align() { - Align::Min => outer.left(), - Align::Center => outer.center().x - size.x / 2.0, - Align::Max => outer.right() - size.x, - }; - let y = match self.vertical_align() { - Align::Min => outer.top(), - Align::Center => outer.center().y - size.y / 2.0, - Align::Max => outer.bottom() - size.y, - }; - - Rect::from_min_size(Pos2::new(x, y), size) + self.align2().align_size_within_rect(size, outer) } fn initial_cursor(&self, max_rect: Rect) -> Pos2 { @@ -369,7 +403,7 @@ impl Layout { /// Returns where to put the next widget that is of the given size. /// The returned `frame_rect` `Rect` will always be justified along the cross axis. /// This is what you then pass to `advance_after_rects`. - /// Use `justify_or_align` to get the inner `widget_rect`. + /// Use `justify_and_align` to get the inner `widget_rect`. #[allow(clippy::collapsible_if)] pub(crate) fn next_space( &self, @@ -422,12 +456,12 @@ impl Layout { } let available_size = self.available_size_before_wrap_finite(region); - if self.main_dir.is_horizontal() { - // Fill full height - child_size.y = child_size.y.max(available_size.y); - } else { - // Fill full width - child_size.x = child_size.x.max(available_size.x); + + if self.is_vertical() || self.horizontal_justify() { + child_size.x = child_size.x.at_least(available_size.x); // fill full width + } + if self.is_horizontal() || self.vertical_justify() { + child_size.y = child_size.y.at_least(available_size.y); // fill full height } let child_pos = match self.main_dir { @@ -440,30 +474,15 @@ impl Layout { Rect::from_min_size(child_pos, child_size) } - /// Apply justify or alignment after calling `next_space`. - pub(crate) fn justify_or_align(&self, rect: Rect, mut child_size: Vec2) -> Rect { - if self.cross_justify { - if self.main_dir.is_horizontal() { - child_size.y = rect.height(); // fill full height - } else { - child_size.x = rect.width(); // fill full width - } + /// Apply justify (fill width/height) and/or alignment after calling `next_space`. + pub(crate) fn justify_and_align(&self, rect: Rect, mut child_size: Vec2) -> Rect { + if self.horizontal_justify() { + child_size.x = child_size.x.at_least(rect.width()); // fill full width } - - match self.main_dir { - Direction::LeftToRight => { - Align2([Align::Min, self.cross_align]).align_size_within_rect(child_size, rect) - } - Direction::RightToLeft => { - Align2([Align::Max, self.cross_align]).align_size_within_rect(child_size, rect) - } - Direction::TopDown => { - Align2([self.cross_align, Align::Min]).align_size_within_rect(child_size, rect) - } - Direction::BottomUp => { - Align2([self.cross_align, Align::Max]).align_size_within_rect(child_size, rect) - } + if self.vertical_justify() { + child_size.y = child_size.y.at_least(rect.height()); // fill full height } + self.align_size_within_rect(child_size, rect) } /// Advance the cursor by this many points. diff --git a/egui/src/placer.rs b/egui/src/placer.rs index 5e5ae10e..9df78265 100644 --- a/egui/src/placer.rs +++ b/egui/src/placer.rs @@ -102,7 +102,7 @@ impl Placer { /// Returns where to put the next widget that is of the given size. /// The returned `frame_rect` will always be justified along the cross axis. /// This is what you then pass to `advance_after_rects`. - /// Use `justify_or_align` to get the inner `widget_rect`. + /// Use `justify_and_align` to get the inner `widget_rect`. pub(crate) fn next_space(&self, child_size: Vec2, item_spacing: Vec2) -> Rect { if let Some(grid) = &self.grid { grid.next_cell(self.region.cursor, child_size) @@ -113,11 +113,11 @@ impl Placer { } /// Apply justify or alignment after calling `next_space`. - pub(crate) fn justify_or_align(&self, rect: Rect, child_size: Vec2) -> Rect { + pub(crate) fn justify_and_align(&self, rect: Rect, child_size: Vec2) -> Rect { if let Some(grid) = &self.grid { - grid.justify_or_align(rect, child_size) + grid.justify_and_align(rect, child_size) } else { - self.layout.justify_or_align(rect, child_size) + self.layout.justify_and_align(rect, child_size) } } diff --git a/egui/src/ui.rs b/egui/src/ui.rs index 8e2198f9..3a5e37e3 100644 --- a/egui/src/ui.rs +++ b/egui/src/ui.rs @@ -17,7 +17,9 @@ use crate::{ /// ui.label("A shorter and more convenient way to add a label."); /// ui.horizontal(|ui| { /// ui.label("Add widgets"); -/// ui.button("on the same row!"); +/// if ui.button("on the same row!").clicked() { +/// /* … */ +/// } /// }); /// ``` pub struct Ui { @@ -274,7 +276,7 @@ impl Ui { // ------------------------------------------------------------------------ -/// ## Sizes etc +/// # Sizes etc impl Ui { /// Where and how large the `Ui` is already. /// All widgets that have been added ot this `Ui` fits within this rectangle. @@ -517,7 +519,10 @@ impl Ui { pub fn advance_cursor(&mut self, amount: f32) { self.placer.advance_cursor(amount); } +} +/// # Allocating space: where do I put my widgets? +impl Ui { /// Allocate space for a widget and check for interaction in the space. /// Returns a `Response` which contains a rectangle, id, and interaction info. /// @@ -629,7 +634,7 @@ impl Ui { fn allocate_space_impl(&mut self, desired_size: Vec2) -> Rect { let item_spacing = self.spacing().item_spacing; let frame_rect = self.placer.next_space(desired_size, item_spacing); - let widget_rect = self.placer.justify_or_align(frame_rect, desired_size); + let widget_rect = self.placer.justify_and_align(frame_rect, desired_size); self.placer .advance_after_rects(frame_rect, widget_rect, item_spacing); @@ -637,7 +642,8 @@ impl Ui { widget_rect } - /// Allocate a specific part of the ui. + /// Allocate a specific part of the `Ui‘. + /// Ignore the layout of the `Ui‘: just put my widget here! pub(crate) fn allocate_rect(&mut self, rect: Rect, sense: Sense) -> Response { let id = self.advance_cursor_after_rect(rect); self.interact(rect, id, sense) @@ -666,7 +672,9 @@ impl Ui { ) -> (R, Response) { let item_spacing = self.spacing().item_spacing; let outer_child_rect = self.placer.next_space(desired_size, item_spacing); - let inner_child_rect = self.placer.justify_or_align(outer_child_rect, desired_size); + let inner_child_rect = self + .placer + .justify_and_align(outer_child_rect, desired_size); let mut child_ui = self.child_ui(inner_child_rect, *self.layout()); let ret = add_contents(&mut child_ui); @@ -682,6 +690,29 @@ impl Ui { (ret, response) } + /// Allocated the given rectangle and then adds content to that rectangle. + /// If the contents overflow, more space will be allocated. + /// When finished, the amount of space actually used (`min_rect`) will be allocated. + /// So you can request a lot of space and then use less. + pub fn allocate_ui_at_rect( + &mut self, + max_rect: Rect, + add_contents: impl FnOnce(&mut Self) -> R, + ) -> (R, Response) { + let mut child_ui = self.child_ui(max_rect, *self.layout()); + let ret = add_contents(&mut child_ui); + let final_child_rect = child_ui.min_rect(); + + self.placer.advance_after_rects( + final_child_rect, + final_child_rect, + self.spacing().item_spacing, + ); + + let response = self.interact(final_child_rect, child_ui.id, Sense::hover()); + (ret, response) + } + /// Convenience function to get a region to paint on pub fn allocate_painter(&mut self, desired_size: Vec2, sense: Sense) -> (Response, Painter) { let response = self.allocate_response(desired_size, sense); @@ -729,6 +760,22 @@ impl Ui { widget.ui(self) } + /// Add a widget to this `Ui` with a given max size. + pub fn add_sized(&mut self, max_size: Vec2, widget: impl Widget) -> Response { + self.allocate_ui(max_size, |ui| { + ui.centered_and_justified(|ui| ui.add(widget)).0 + }) + .0 + } + + /// Add a widget to this `Ui` at a specific location (manual layout). + pub fn put(&mut self, max_rect: Rect, widget: impl Widget) -> Response { + self.allocate_ui_at_rect(max_rect, |ui| { + ui.centered_and_justified(|ui| ui.add(widget)).0 + }) + .0 + } + /// Shortcut for `add(Label::new(text))` pub fn label(&mut self, label: impl Into