diff --git a/egui/src/containers/panel.rs b/egui/src/containers/panel.rs index 51830a53..4ae1c983 100644 --- a/egui/src/containers/panel.rs +++ b/egui/src/containers/panel.rs @@ -151,7 +151,7 @@ impl SidePanel { width_range, } = self; - let available_rect = ui.max_rect(); + let available_rect = ui.available_rect_before_wrap(); let mut panel_rect = available_rect; { let mut width = default_width; @@ -187,7 +187,8 @@ impl SidePanel { is_resizing = ui.memory().interaction.drag_id == Some(resize_id); if is_resizing { let width = (pointer.x - side.side_x(panel_rect)).abs(); - let width = clamp_to_range(width, width_range).at_most(available_rect.width()); + let width = + clamp_to_range(width, width_range.clone()).at_most(available_rect.width()); side.set_rect_width(&mut panel_rect, width); } @@ -201,15 +202,31 @@ impl SidePanel { } } - let mut panel_ui = ui.child_ui(panel_rect, Layout::top_down(Align::Min)); + let mut panel_ui = ui.child_ui_with_id_source(panel_rect, Layout::top_down(Align::Min), id); panel_ui.expand_to_include_rect(panel_rect); let frame = frame.unwrap_or_else(|| Frame::side_top_panel(ui.style())); let inner_response = frame.show(&mut panel_ui, |ui| { ui.set_min_height(ui.max_rect_finite().height()); // Make sure the frame fills the full height + ui.set_width_range(width_range); add_contents(ui) }); let rect = inner_response.response.rect; + + { + let mut cursor = ui.cursor(); + match side { + Side::Left => { + cursor.min.x = rect.max.x + ui.spacing().item_spacing.x; + } + Side::Right => { + cursor.max.x = rect.min.x - ui.spacing().item_spacing.x; + } + } + ui.set_cursor(cursor); + } + ui.expand_to_include_rect(rect); + ui.memory().id_data.insert(id, PanelState { rect }); if resize_hover || is_resizing { @@ -230,6 +247,7 @@ impl SidePanel { inner_response } + pub fn show( self, ctx: &CtxRef, @@ -390,7 +408,7 @@ impl TopBottomPanel { height_range, } = self; - let available_rect = ui.max_rect(); + let available_rect = ui.available_rect_before_wrap(); let mut panel_rect = available_rect; { let state = ui.memory().id_data.get::(&id).copied(); @@ -428,8 +446,8 @@ impl TopBottomPanel { is_resizing = ui.memory().interaction.drag_id == Some(resize_id); if is_resizing { let height = (pointer.y - side.side_y(panel_rect)).abs(); - let height = - clamp_to_range(height, height_range).at_most(available_rect.height()); + let height = clamp_to_range(height, height_range.clone()) + .at_most(available_rect.height()); side.set_rect_height(&mut panel_rect, height); } @@ -443,15 +461,31 @@ impl TopBottomPanel { } } - let mut panel_ui = ui.child_ui(panel_rect, Layout::top_down(Align::Min)); + let mut panel_ui = ui.child_ui_with_id_source(panel_rect, Layout::top_down(Align::Min), id); panel_ui.expand_to_include_rect(panel_rect); let frame = frame.unwrap_or_else(|| Frame::side_top_panel(ui.style())); let inner_response = frame.show(&mut panel_ui, |ui| { ui.set_min_width(ui.max_rect_finite().width()); // Make the frame fill full width + ui.set_height_range(height_range); add_contents(ui) }); let rect = inner_response.response.rect; + + { + let mut cursor = ui.cursor(); + match side { + TopBottomSide::Top => { + cursor.min.y = rect.max.y + ui.spacing().item_spacing.y; + } + TopBottomSide::Bottom => { + cursor.max.y = rect.min.y - ui.spacing().item_spacing.y; + } + } + ui.set_cursor(cursor); + } + ui.expand_to_include_rect(rect); + ui.memory().id_data.insert(id, PanelState { rect }); if resize_hover || is_resizing { @@ -563,6 +597,7 @@ impl CentralPanel { add_contents(ui) }) } + pub fn show( self, ctx: &CtxRef, diff --git a/egui/src/layout.rs b/egui/src/layout.rs index 0238e770..dc59f34b 100644 --- a/egui/src/layout.rs +++ b/egui/src/layout.rs @@ -37,7 +37,7 @@ pub(crate) struct Region { /// /// So one can think of `cursor` as a constraint on the available region. /// - /// If something has already been added, this will point ot `style.spacing.item_spacing` beyond the latest child. + /// If something has already been added, this will point to `style.spacing.item_spacing` beyond the latest child. /// The cursor can thus be `style.spacing.item_spacing` pixels outside of the min_rect. pub(crate) cursor: Rect, } @@ -409,7 +409,7 @@ impl Layout { // NOTE: in normal top-down layout the cursor has moved below the current max_rect, // but the available shouldn't be negative. - // ALSO: with wrapping layouts, cursor jumps to new row before expanding max_rect + // ALSO: with wrapping layouts, cursor jumps to new row before expanding max_rect. let mut avail = max_rect; @@ -417,45 +417,47 @@ impl Layout { Direction::LeftToRight => { avail.min.x = cursor.min.x; avail.max.x = avail.max.x.max(cursor.min.x); - if self.main_wrap { - avail.min.y = cursor.min.y; - avail.max.y = cursor.max.y; - } avail.max.x = avail.max.x.max(avail.min.x); avail.max.y = avail.max.y.max(avail.min.y); } Direction::RightToLeft => { avail.max.x = cursor.max.x; avail.min.x = avail.min.x.min(cursor.max.x); - if self.main_wrap { - avail.min.y = cursor.min.y; - avail.max.y = cursor.max.y; - } avail.min.x = avail.min.x.min(avail.max.x); avail.max.y = avail.max.y.max(avail.min.y); } Direction::TopDown => { avail.min.y = cursor.min.y; avail.max.y = avail.max.y.max(cursor.min.y); - if self.main_wrap { - avail.min.x = cursor.min.x; - avail.max.x = cursor.max.x; - } avail.max.x = avail.max.x.max(avail.min.x); avail.max.y = avail.max.y.max(avail.min.y); } Direction::BottomUp => { avail.max.y = cursor.max.y; avail.min.y = avail.min.y.min(cursor.max.y); - if self.main_wrap { - avail.min.x = cursor.min.x; - avail.max.x = cursor.max.x; - } avail.max.x = avail.max.x.max(avail.min.x); avail.min.y = avail.min.y.min(avail.max.y); } } + // We can use the cursor to restrict the available region. + // For instance, we use this to restrict the available space of a parent Ui + // after adding a panel to it. + // We also use it for wrapping layouts. + avail = avail.intersect(cursor); + + // Make sure it isn't negative: + if avail.max.x < avail.min.x { + let x = 0.5 * (avail.min.x + avail.max.x); + avail.min.x = x; + avail.max.x = x; + } + if avail.max.y < avail.min.y { + let y = 0.5 * (avail.min.y + avail.max.y); + avail.min.y = y; + avail.max.y = y; + } + avail } @@ -600,7 +602,9 @@ impl Layout { ) -> Rect { let frame = self.next_frame_ignore_wrap(region, size); let rect = self.align_size_within_rect(size, frame); - crate::egui_assert!((rect.size() - size).length() < 1.0); + crate::egui_assert!(!rect.any_nan()); + crate::egui_assert!((rect.width() - size.x).abs() < 1.0 || size.x == f32::INFINITY); + crate::egui_assert!((rect.height() - size.y).abs() < 1.0 || size.y == f32::INFINITY); rect } diff --git a/egui/src/placer.rs b/egui/src/placer.rs index 9953ebe1..c490a9f2 100644 --- a/egui/src/placer.rs +++ b/egui/src/placer.rs @@ -72,6 +72,11 @@ impl Placer { pub(crate) fn cursor(&self) -> Rect { self.region.cursor } + + #[inline(always)] + pub(crate) fn set_cursor(&mut self, cursor: Rect) { + self.region.cursor = cursor + } } impl Placer { diff --git a/egui/src/ui.rs b/egui/src/ui.rs index 7306c48b..25da2b52 100644 --- a/egui/src/ui.rs +++ b/egui/src/ui.rs @@ -78,12 +78,22 @@ impl Ui { /// Create a new `Ui` at a specific region. pub fn child_ui(&mut self, max_rect: Rect, layout: Layout) -> Self { + self.child_ui_with_id_source(max_rect, layout, "child") + } + + /// Create a new `Ui` at a specific region with a specific id. + pub fn child_ui_with_id_source( + &mut self, + max_rect: Rect, + layout: Layout, + id_source: impl Hash, + ) -> Self { crate::egui_assert!(!max_rect.any_nan()); let next_auto_id_source = Id::new(self.next_auto_id_source).with("child").value(); self.next_auto_id_source = self.next_auto_id_source.wrapping_add(1); Ui { - id: self.id.with("child"), + id: self.id.with(id_source), next_auto_id_source, painter: self.painter.clone(), style: self.style.clone(), @@ -436,6 +446,12 @@ impl Ui { self.set_max_width(*width.end()); } + /// `ui.set_height_range(min..=max);` is equivalent to `ui.set_min_height(min); ui.set_max_height(max);`. + pub fn set_height_range(&mut self, height: std::ops::RangeInclusive) { + self.set_min_height(*height.start()); + self.set_max_height(*height.end()); + } + /// Set both the minimum and maximum width. pub fn set_width(&mut self, width: f32) { self.set_min_width(width); @@ -734,6 +750,10 @@ impl Ui { self.placer.cursor() } + pub(crate) fn set_cursor(&mut self, cursor: Rect) { + self.placer.set_cursor(cursor) + } + /// Where do we expect a zero-sized widget to be placed? pub(crate) fn next_widget_position(&self) -> Pos2 { self.placer.next_widget_position() diff --git a/egui_demo_lib/src/apps/demo/window_with_panels.rs b/egui_demo_lib/src/apps/demo/window_with_panels.rs index 538d1d9e..35c3ee89 100644 --- a/egui_demo_lib/src/apps/demo/window_with_panels.rs +++ b/egui_demo_lib/src/apps/demo/window_with_panels.rs @@ -1,5 +1,3 @@ -use egui::{menu, Align, CentralPanel, Layout, ScrollArea, SidePanel, TopBottomPanel}; - #[derive(Clone, PartialEq, Default)] #[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] pub struct WindowWithPanels {} @@ -8,6 +6,7 @@ impl super::Demo for WindowWithPanels { fn name(&self) -> &'static str { "🗖 Window With Panels" } + fn show(&mut self, ctx: &egui::CtxRef, open: &mut bool) { use super::View; let window = egui::Window::new("Window with Panels") @@ -23,21 +22,12 @@ impl super::Demo for WindowWithPanels { impl super::View for WindowWithPanels { fn ui(&mut self, ui: &mut egui::Ui) { - let left_panel_min_width = 100.; - let left_panel_max_width = left_panel_min_width * 4.; - let bottom_height = 25.; - - ui.expand_to_include_rect(ui.max_rect()); // Expand frame to include it all - - let mut top_rect = ui.available_rect_before_wrap_finite(); - top_rect.min.y += ui.spacing().item_spacing.y; - let mut top_ui = ui.child_ui(top_rect, Layout::top_down(Align::Max)); - - let top_response = TopBottomPanel::top("window_menu") + egui::TopBottomPanel::top("top_panel") .resizable(false) - .show_inside(&mut top_ui, |ui| { - menu::bar(ui, |ui| { - menu::menu(ui, "Menu", |ui| { + .min_height(0.0) + .show_inside(ui, |ui| { + egui::menu::bar(ui, |ui| { + egui::menu::menu(ui, "Menu", |ui| { if ui.button("Option 1").clicked() {} if ui.button("Option 2").clicked() {} if ui.button("Option 3").clicked() {} @@ -45,43 +35,49 @@ impl super::View for WindowWithPanels { }); }); - let mut left_rect = ui.available_rect_before_wrap_finite(); - left_rect.min.y = top_response.response.rect.max.y + ui.spacing().item_spacing.y; - let mut left_ui = ui.child_ui(left_rect, Layout::top_down(Align::Max)); + egui::TopBottomPanel::bottom("bottom_panel_A") + .resizable(false) + .min_height(0.0) + .show_inside(ui, |ui| { + ui.label("Bottom Panel A"); + }); - let left_response = SidePanel::left("Folders") + egui::SidePanel::left("left_panel") .resizable(true) - .min_width(left_panel_min_width) - .max_width(left_panel_max_width) - .show_inside(&mut left_ui, |ui| { - ScrollArea::auto_sized().show(ui, |ui| { + .width_range(60.0..=200.0) + .show_inside(ui, |ui| { + egui::ScrollArea::auto_sized().show(ui, |ui| { ui.vertical(|ui| { ui.label("Left Panel"); - }) - }) + ui.small(crate::LOREM_IPSUM_LONG); + }); + }); }); - let mut right_rect = ui.available_rect_before_wrap_finite(); - right_rect.min.x = left_response.response.rect.max.x; - right_rect.min.y = top_response.response.rect.max.y + ui.spacing().item_spacing.y; - let mut right_ui = ui.child_ui(right_rect, Layout::top_down(Align::Max)); + egui::SidePanel::right("right_panel") + .resizable(true) + .width_range(60.0..=200.0) + .show_inside(ui, |ui| { + egui::ScrollArea::auto_sized().show(ui, |ui| { + ui.vertical(|ui| { + ui.label("Right Panel"); + ui.small(crate::LOREM_IPSUM_LONG); + }); + }); + }); - CentralPanel::default().show_inside(&mut right_ui, |ui| { - let mut rect = ui.min_rect(); - let mut bottom_rect = rect; - bottom_rect.min.y = ui.max_rect_finite().max.y - bottom_height; - rect.max.y = bottom_rect.min.y - ui.spacing().indent; - let mut child_ui = ui.child_ui(rect, Layout::top_down(Align::Min)); - let mut bottom_ui = ui.child_ui(bottom_rect, Layout::bottom_up(Align::Max)); - ScrollArea::auto_sized().show(&mut child_ui, |ui| { + egui::TopBottomPanel::bottom("bottom_panel_B") + .resizable(false) + .min_height(0.0) + .show_inside(ui, |ui| { + ui.label("Bottom Panel B"); + }); + + egui::CentralPanel::default().show_inside(ui, |ui| { + egui::ScrollArea::auto_sized().show(ui, |ui| { ui.vertical(|ui| { ui.label("Central Panel"); - }) - }); - bottom_ui.vertical(|ui| { - ui.separator(); - ui.horizontal(|ui| { - ui.label("Bottom Content"); + ui.small(crate::LOREM_IPSUM_LONG); }); }); }); diff --git a/emath/src/align.rs b/emath/src/align.rs index 35885a2f..a6039b16 100644 --- a/emath/src/align.rs +++ b/emath/src/align.rs @@ -63,6 +63,36 @@ impl Align { Self::Max => 1.0, } } + + /// ``` rust + /// assert_eq!(emath::Align::Min.align_size_within_range(2.0, 10.0..=20.0), 10.0..=12.0); + /// assert_eq!(emath::Align::Center.align_size_within_range(2.0, 10.0..=20.0), 14.0..=16.0); + /// assert_eq!(emath::Align::Max.align_size_within_range(2.0, 10.0..=20.0), 18.0..=20.0); + /// assert_eq!(emath::Align::Min.align_size_within_range(f32::INFINITY, 10.0..=20.0), 10.0..=f32::INFINITY); + /// assert_eq!(emath::Align::Center.align_size_within_range(f32::INFINITY, 10.0..=20.0), f32::NEG_INFINITY..=f32::INFINITY); + /// assert_eq!(emath::Align::Max.align_size_within_range(f32::INFINITY, 10.0..=20.0), f32::NEG_INFINITY..=20.0); + /// ``` + #[inline] + pub fn align_size_within_range( + self, + size: f32, + range: RangeInclusive, + ) -> RangeInclusive { + let min = *range.start(); + let max = *range.end(); + match self { + Self::Min => min..=min + size, + Self::Center => { + if size == f32::INFINITY { + f32::NEG_INFINITY..=f32::INFINITY + } else { + let left = (min + max) / 2.0 - size / 2.0; + left..=left + size + } + } + Self::Max => max - size..=max, + } + } } impl Default for Align { @@ -126,18 +156,9 @@ impl Align2 { /// e.g. center a size within a given frame pub fn align_size_within_rect(self, size: Vec2, frame: Rect) -> Rect { - let x = match self.x() { - Align::Min => frame.left(), - Align::Center => frame.center().x - size.x / 2.0, - Align::Max => frame.right() - size.x, - }; - let y = match self.y() { - Align::Min => frame.top(), - Align::Center => frame.center().y - size.y / 2.0, - Align::Max => frame.bottom() - size.y, - }; - - Rect::from_min_size(Pos2::new(x, y), size) + let x_range = self.x().align_size_within_range(size.x, frame.x_range()); + let y_range = self.y().align_size_within_range(size.y, frame.y_range()); + Rect::from_x_y_ranges(x_range, y_range) } pub fn pos_in_rect(self, frame: &Rect) -> Pos2 {