diff --git a/egui/src/containers/scroll_area.rs b/egui/src/containers/scroll_area.rs index ded2c6e0..8609365a 100644 --- a/egui/src/containers/scroll_area.rs +++ b/egui/src/containers/scroll_area.rs @@ -103,7 +103,7 @@ impl ScrollArea { }; let outer_size = vec2( - ui.available().width(), + ui.available_width(), ui.available().height().at_most(max_height), ); diff --git a/egui/src/demos/dancing_strings.rs b/egui/src/demos/dancing_strings.rs index f41a31ff..f699c2c3 100644 --- a/egui/src/demos/dancing_strings.rs +++ b/egui/src/demos/dancing_strings.rs @@ -32,7 +32,7 @@ impl View for DancingStrings { ui.ctx().request_repaint(); let time = ui.input().time; - let desired_size = ui.available().width() * vec2(1.0, 0.35); + let desired_size = ui.available_width() * vec2(1.0, 0.35); let rect = ui.allocate_space(desired_size); let mut cmds = vec![]; diff --git a/egui/src/demos/demo_window.rs b/egui/src/demos/demo_window.rs index 9b2c1ea4..e45a4c1e 100644 --- a/egui/src/demos/demo_window.rs +++ b/egui/src/demos/demo_window.rs @@ -323,7 +323,7 @@ impl Default for LayoutDemo { cross_align: Align::Min, cross_justify: false, wrap_column_width: 150.0, - wrap_row_height: 40.0, + wrap_row_height: 20.0, } } } @@ -338,7 +338,7 @@ impl LayoutDemo { pub fn ui(&mut self, ui: &mut Ui) { self.content_ui(ui); Resize::default() - .default_size([200.0, 100.0]) + .default_size([300.0, 200.0]) .show(ui, |ui| { if self.main_wrap { if self.main_dir.is_horizontal() { @@ -360,10 +360,22 @@ impl LayoutDemo { } pub fn content_ui(&mut self, ui: &mut Ui) { - // ui.label(format!("Available space: {:?}", ui.available().size())); - if ui.button("Default").clicked { - *self = Default::default(); - } + ui.horizontal(|ui| { + if ui.button("Top-down").clicked { + *self = Default::default(); + } + if ui.button("Top-down, centered and justified").clicked { + *self = Default::default(); + self.cross_align = Align::Center; + self.cross_justify = true; + } + if ui.button("Horizontal wrapped").clicked { + *self = Default::default(); + self.main_dir = Direction::LeftToRight; + self.cross_align = Align::Center; + self.main_wrap = true; + } + }); ui.horizontal(|ui| { ui.label("Main Direction:"); @@ -400,9 +412,20 @@ impl LayoutDemo { } pub fn demo_ui(&mut self, ui: &mut Ui) { - ui.heading("Effect:"); - for i in 0..7 { - let _ = ui.button(format!("Button {}", i)); + ui.monospace("Example widgets:"); + for i in 0..9 { + match i % 3 { + 0 => { + ui.label(format!("{} label", i)); + } + 1 => { + let mut dummy = false; + ui.checkbox(&mut dummy, format!("{} checkbox", i)); + } + _ => { + let _ = ui.button(format!("{} button", i)); + } + } } } } diff --git a/egui/src/introspection.rs b/egui/src/introspection.rs index 7e303afb..7a7a8735 100644 --- a/egui/src/introspection.rs +++ b/egui/src/introspection.rs @@ -16,8 +16,8 @@ impl Texture { return; } let mut size = vec2(self.width as f32, self.height as f32); - if size.x > ui.available().width() { - size *= ui.available().width() / size.x; + if size.x > ui.available_width() { + size *= ui.available_width() / size.x; } let rect = ui.allocate_space(size); let mut triangles = Triangles::default(); diff --git a/egui/src/layout.rs b/egui/src/layout.rs index aea38565..fbe60770 100644 --- a/egui/src/layout.rs +++ b/egui/src/layout.rs @@ -282,10 +282,31 @@ impl Layout { } } - pub fn available(&self, region: &Region) -> Rect { + // TODO: clarify if it is before or after wrap + pub(crate) fn available(&self, region: &Region) -> Rect { self.available_from_cursor_max_rect(region.cursor, region.max_rect) } + /// Amount of space available for a widget. + /// Wor wrapping layouts, this is the maximum (after wrap) + pub fn available_size(&self, r: &Region) -> Vec2 { + if self.main_wrap { + if self.main_dir.is_horizontal() { + vec2(r.max_rect.width(), r.max_rect.bottom() - r.cursor.y) + } else { + vec2(r.max_rect.right() - r.cursor.x, r.max_rect.height()) + } + } else { + self.available_from_cursor_max_rect(r.cursor, r.max_rect) + .size() + } + } + + fn available_size_before_wrap(&self, region: &Region) -> Rect { + self.available_from_cursor_max_rect(region.cursor, region.max_rect) + } + + // TODO pub fn available_finite(&self, region: &Region) -> Rect { self.available_from_cursor_max_rect(region.cursor, region.max_rect_finite()) } @@ -317,97 +338,106 @@ impl Layout { rect } - /// 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 layouts, like in menus. - /// - /// You may get LESS space than you asked for if the current layout won't fit what you asked for. + /// Returns where to put the next widget that is of the given size. + /// The returned "outer" `Rect` will always be justified along the cross axis. + /// This is what you then pass to `advance_after_outer_rect`. + /// Use `justify_or_align` to get the inner `Rect`. #[allow(clippy::collapsible_if)] - pub fn next_space(self, region: &Region, minimum_child_size: Vec2) -> Rect { + pub fn next_space(self, region: &Region, mut child_size: Vec2, item_spacing: Vec2) -> Rect { let mut cursor = region.cursor; if self.main_wrap { - let available_size = self.available_finite(region).size(); - // TODO: spacing? + let available_size = self.available_size_before_wrap(region).size(); match self.main_dir { Direction::LeftToRight => { - if available_size.x < minimum_child_size.x && region.max_rect.left() < cursor.x - { + if available_size.x < child_size.x && region.max_rect.left() < cursor.x { // New row - cursor = pos2(region.max_rect.left(), region.max_rect.bottom()); + cursor = pos2( + region.max_rect.left(), + region.max_rect.bottom() + item_spacing.y, + ); } } Direction::RightToLeft => { - if available_size.x < minimum_child_size.x && cursor.x < region.max_rect.right() - { + if available_size.x < child_size.x && cursor.x < region.max_rect.right() { // New row - cursor = pos2(region.max_rect.right(), region.max_rect.bottom()); + cursor = pos2( + region.max_rect.right(), + region.max_rect.bottom() + item_spacing.y, + ); } } Direction::TopDown => { - if available_size.y < minimum_child_size.y && region.max_rect.top() < cursor.y { + if available_size.y < child_size.y && region.max_rect.top() < cursor.y { // New column - cursor = pos2(region.max_rect.right(), region.max_rect.top()); + cursor = pos2( + region.max_rect.right() + item_spacing.x, + region.max_rect.top(), + ); } } Direction::BottomUp => { - if available_size.y < minimum_child_size.y - && cursor.y < region.max_rect.bottom() - { + if available_size.y < child_size.y && cursor.y < region.max_rect.bottom() { // New column - cursor = pos2(region.max_rect.right(), region.max_rect.bottom()); + cursor = pos2( + region.max_rect.right() + item_spacing.x, + region.max_rect.bottom(), + ); } } } } let available_size = self.available_finite(region).size(); - let available_size = available_size.at_least(minimum_child_size); - - let mut child_size = minimum_child_size; - let mut child_move = Vec2::default(); - 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, - }; - } + // Fill full height + child_size.y = child_size.y.max(available_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, - }; - } + // Fill full width + child_size.x = child_size.x.max(available_size.x); } let child_pos = match self.main_dir { - Direction::LeftToRight => cursor + child_move, - Direction::RightToLeft => cursor + child_move + vec2(-child_size.x, 0.0), - Direction::TopDown => cursor + child_move, - Direction::BottomUp => cursor + child_move + vec2(0.0, -child_size.y), + Direction::LeftToRight => cursor, + Direction::RightToLeft => cursor + vec2(-child_size.x, 0.0), + Direction::TopDown => cursor, + Direction::BottomUp => cursor + vec2(0.0, -child_size.y), }; Rect::from_min_size(child_pos, child_size) } + /// Apply justify or alignment after calling `next_space`. + pub fn justify_or_align(self, mut rect: Rect, child_size: Vec2) -> Rect { + if self.main_dir.is_horizontal() { + debug_assert!((rect.width() - child_size.x).abs() < 0.1); + if self.cross_justify { + rect // fill full height + } else { + rect.min.y += match self.cross_align { + Align::Min => 0.0, + Align::Center => 0.5 * (rect.size().y - child_size.y), + Align::Max => rect.size().y - child_size.y, + }; + rect.max.y = rect.min.y + child_size.y; + rect + } + } else { + debug_assert!((rect.height() - child_size.y).abs() < 0.1); + if self.cross_justify { + rect // justified: fill full width + } else { + rect.min.x += match self.cross_align { + Align::Min => 0.0, + Align::Center => 0.5 * (rect.size().x - child_size.x), + Align::Max => rect.size().x - child_size.x, + }; + rect.max.x = rect.min.x + child_size.x; + rect + } + } + } + /// Advance the cursor by this many points. pub fn advance_cursor(self, region: &mut Region, amount: f32) { match self.main_dir { @@ -428,7 +458,7 @@ impl Layout { } /// Advance cursor after a widget was added to a specific rectangle. - pub fn advance_after_rect(self, region: &mut Region, rect: Rect, item_spacing: Vec2) { + pub fn advance_after_outer_rect(self, region: &mut Region, rect: Rect, item_spacing: Vec2) { region.cursor = match self.main_dir { Direction::LeftToRight => pos2(rect.right() + item_spacing.x, rect.top()), Direction::RightToLeft => pos2(rect.left() - item_spacing.x, rect.top()), diff --git a/egui/src/menu.rs b/egui/src/menu.rs index ac5a8646..46bd1486 100644 --- a/egui/src/menu.rs +++ b/egui/src/menu.rs @@ -54,7 +54,7 @@ pub fn bar(ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> (R, Respo // Take full width and fixed height: let height = ui.style().spacing.interact_size.y; - ui.set_min_size(vec2(ui.available().width(), height)); + ui.set_min_size(vec2(ui.available_width(), height)); add_contents(ui) }) diff --git a/egui/src/ui.rs b/egui/src/ui.rs index 4991e854..aea8c08c 100644 --- a/egui/src/ui.rs +++ b/egui/src/ui.rs @@ -313,9 +313,17 @@ impl Ui { /// The available space at the moment, given the current cursor. /// This how much more space we can take up without overflowing our parent. /// Shrinks as widgets allocate space and the cursor moves. - /// A small rectangle should be interpreted as "as little as possible". - /// An infinite rectangle should be interpreted as "as much as you want". - /// In most layouts the next widget will be put in the top left corner of this `Rect`. + /// A small size should be interpreted as "as little as possible". + /// An infinite size should be interpreted as "as much as you want". + pub fn available_size(&self) -> Vec2 { + self.layout.available_size(&self.region) + } + + pub fn available_width(&self) -> f32 { + self.available_size().x + } + + // TODO: clarify if this is before or after wrap pub fn available(&self) -> Rect { self.layout.available(&self.region) } @@ -438,14 +446,17 @@ impl Ui { /// Reserve this much space and move the cursor. /// Returns where to put the widget. fn allocate_space_impl(&mut self, desired_size: Vec2) -> Rect { - let child_rect = self.layout.next_space(&self.region, desired_size); - let item_spacing = self.style().spacing.item_spacing; + let outer_child_rect = self + .layout + .next_space(&self.region, desired_size, item_spacing); + let inner_child_rect = self.layout.justify_or_align(outer_child_rect, desired_size); + self.layout - .advance_after_rect(&mut self.region, child_rect, item_spacing); + .advance_after_outer_rect(&mut self.region, outer_child_rect, item_spacing); self.next_auto_id = self.next_auto_id.wrapping_add(1); - child_rect + inner_child_rect } /// Allocated the given space and then adds content to that space. @@ -457,16 +468,23 @@ impl Ui { desired_size: Vec2, add_contents: impl FnOnce(&mut Ui) -> R, ) -> (R, Response) { - let child_rect = self.layout.next_space(&self.region, desired_size); - let mut child_ui = self.child_ui(child_rect, self.layout); - let ret = add_contents(&mut child_ui); - let child_rect = child_ui.region.max_rect; - let item_spacing = self.style().spacing.item_spacing; - self.layout - .advance_after_rect(&mut self.region, child_rect, item_spacing); + let outer_child_rect = self + .layout + .next_space(&self.region, desired_size, item_spacing); + let inner_child_rect = self.layout.justify_or_align(outer_child_rect, desired_size); - let response = self.interact_hover(child_rect); + let mut child_ui = self.child_ui(inner_child_rect, self.layout); + let ret = add_contents(&mut child_ui); + let final_child_rect = child_ui.region.max_rect; + + self.layout.advance_after_outer_rect( + &mut self.region, + outer_child_rect.union(final_child_rect), + item_spacing, + ); + + let response = self.interact_hover(final_child_rect); (ret, response) } @@ -476,19 +494,26 @@ impl Ui { /// So you can request a lot of space and then use less. pub fn allocate_ui_min( &mut self, - initial_size: Vec2, + desired_size: Vec2, add_contents: impl FnOnce(&mut Self) -> R, ) -> (R, Response) { - let child_rect = self.layout.next_space(&self.region, initial_size); - let mut child_ui = self.child_ui(child_rect, self.layout); - let ret = add_contents(&mut child_ui); - let child_rect = child_ui.region.min_rect; - let item_spacing = self.style().spacing.item_spacing; - self.layout - .advance_after_rect(&mut self.region, child_rect, item_spacing); + let outer_child_rect = self + .layout + .next_space(&self.region, desired_size, item_spacing); + let inner_child_rect = self.layout.justify_or_align(outer_child_rect, desired_size); - let response = self.interact_hover(child_rect); + let mut child_ui = self.child_ui(inner_child_rect, self.layout); + let ret = add_contents(&mut child_ui); + let final_child_rect = child_ui.region.min_rect; + + self.layout.advance_after_outer_rect( + &mut self.region, + outer_child_rect.union(final_child_rect), + item_spacing, + ); + + let response = self.interact_hover(final_child_rect); (ret, response) } } @@ -771,8 +796,8 @@ impl Ui { pub fn column(&mut self, column_position: Align, width: f32) -> Self { 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::Center => self.available_width() / 2.0 - width / 2.0, + Align::Max => self.available_width() - width, }; self.child_ui( Rect::from_min_size( @@ -823,8 +848,10 @@ impl Ui { ) -> (R, Response) { let mut child_ui = self.child_ui(self.available(), layout); let ret = add_contents(&mut child_ui); - let size = child_ui.min_size(); - let rect = self.allocate_space(size); + let rect = child_ui.min_rect(); + let item_spacing = self.style().spacing.item_spacing; + self.layout + .advance_after_outer_rect(&mut self.region, rect, item_spacing); (ret, self.interact_hover(rect)) } @@ -844,7 +871,7 @@ impl Ui { // TODO: ensure there is space let spacing = self.style().spacing.item_spacing.x; let total_spacing = spacing * (num_columns as f32 - 1.0); - let column_width = (self.available().width() - total_spacing) / (num_columns as f32); + let column_width = (self.available_width() - total_spacing) / (num_columns as f32); let mut columns: Vec = (0..num_columns) .map(|col_idx| { @@ -871,7 +898,7 @@ impl Ui { max_height = size.y.max(max_height); } - let size = vec2(self.available().width().max(sum_width), max_height); + let size = vec2(self.available_width().max(sum_width), max_height); self.allocate_space(size); result } diff --git a/egui/src/widgets/mod.rs b/egui/src/widgets/mod.rs index eba37003..492f6230 100644 --- a/egui/src/widgets/mod.rs +++ b/egui/src/widgets/mod.rs @@ -79,7 +79,7 @@ impl Label { } pub fn layout(&self, ui: &Ui) -> Galley { - let max_width = ui.available().width(); + let max_width = ui.available_width(); // Prevent word-wrapping after a single letter, and other silly shit: // TODO: general "don't force labels and similar to wrap so early" // TODO: max_width = max_width.at_least(ui.spacing.first_wrap_width); @@ -198,7 +198,7 @@ impl Widget for Hyperlink { let color = color::LIGHT_BLUE; let text_style = text_style.unwrap_or_else(|| ui.style().body_text_style); let font = &ui.fonts()[text_style]; - let galley = font.layout_multiline(text, ui.available().width()); + let galley = font.layout_multiline(text, ui.available_width()); let rect = ui.allocate_space(galley.size); let id = ui.make_position_id(); @@ -307,7 +307,7 @@ impl Widget for Button { let button_padding = ui.style().spacing.button_padding; let font = &ui.fonts()[text_style]; - let galley = font.layout_multiline(text, ui.available().width()); + let galley = font.layout_multiline(text, ui.available_width()); let mut desired_size = galley.size + 2.0 * button_padding; desired_size = desired_size.at_least(ui.style().spacing.interact_size); let rect = ui.allocate_space(desired_size); @@ -379,7 +379,7 @@ impl<'a> Widget for Checkbox<'a> { let total_extra = button_padding + vec2(icon_width + icon_spacing, 0.0) + button_padding; let galley = font.layout_single_line(text); - // let galley = font.layout_multiline(text, ui.available().width() - total_extra.x); + // let galley = font.layout_multiline(text, ui.available_width() - total_extra.x); let mut desired_size = total_extra + galley.size; desired_size = desired_size.at_least(spacing.interact_size); @@ -467,7 +467,7 @@ impl Widget for RadioButton { let button_padding = ui.style().spacing.button_padding; let total_extra = button_padding + vec2(icon_width + icon_spacing, 0.0) + button_padding; - let galley = font.layout_multiline(text, ui.available().width() - total_extra.x); + let galley = font.layout_multiline(text, ui.available_width() - total_extra.x); let mut desired_size = total_extra + galley.size; desired_size = desired_size.at_least(ui.style().spacing.interact_size); @@ -543,7 +543,7 @@ impl Widget for SelectableLabel { let button_padding = ui.style().spacing.button_padding; let total_extra = button_padding + button_padding; - let galley = font.layout_multiline(text, ui.available().width() - total_extra.x); + let galley = font.layout_multiline(text, ui.available_width() - total_extra.x); let mut desired_size = total_extra + galley.size; desired_size = desired_size.at_least(ui.style().spacing.interact_size); diff --git a/egui/src/widgets/text_edit.rs b/egui/src/widgets/text_edit.rs index b295b257..621892ae 100644 --- a/egui/src/widgets/text_edit.rs +++ b/egui/src/widgets/text_edit.rs @@ -228,7 +228,7 @@ impl<'t> Widget for TextEdit<'t> { let text_style = text_style.unwrap_or_else(|| ui.style().body_text_style); let font = &ui.fonts()[text_style]; let line_spacing = font.row_height(); - let available_width = ui.available().width(); + let available_width = ui.available_width(); let mut galley = if multiline { font.layout_multiline(text.clone(), available_width) } else {