From 5a196f66044c368469d27780c8eb5fd598b7eed4 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 26 Aug 2024 08:51:18 +0200 Subject: [PATCH] Create a `UiBuilder` for building `Ui`s (#4969) * Part of https://github.com/emilk/egui/issues/4634 The goals is to create fewer, more powerful entry points. ### Added * `egui::UiBuilder` * `Ui::allocate_new_ui` * `Ui::new_child` ### Breaking changes * `Ui::new` now takes a `UiBuilder` * Deprecated * `ui.add_visible_ui` * `ui.allocate_ui_at_rect` * `ui.child_ui` * `ui.child_ui_with_id_source` * `ui.push_stack_info` --- crates/egui/src/containers/area.rs | 27 +- crates/egui/src/containers/combo_box.rs | 2 +- crates/egui/src/containers/frame.rs | 8 +- crates/egui/src/containers/panel.rs | 61 ++-- crates/egui/src/containers/resize.rs | 8 +- crates/egui/src/containers/scroll_area.rs | 10 +- crates/egui/src/grid.rs | 13 +- crates/egui/src/lib.rs | 4 +- crates/egui/src/ui.rs | 269 ++++++++++++------ crates/egui/src/ui_builder.rs | 119 ++++++++ .../egui_demo_lib/src/demo/widget_gallery.rs | 13 +- crates/egui_extras/src/layout.rs | 13 +- examples/custom_window_frame/src/main.rs | 13 +- tests/test_viewports/src/main.rs | 4 +- 14 files changed, 392 insertions(+), 172 deletions(-) create mode 100644 crates/egui/src/ui_builder.rs diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index 062261af..196c3de2 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -532,16 +532,19 @@ impl Prepared { pub(crate) fn content_ui(&self, ctx: &Context) -> Ui { let max_rect = self.state.rect(); - let clip_rect = self.constrain_rect; // Don't paint outside our bounds + let mut ui_builder = UiBuilder::new() + .ui_stack_info(UiStackInfo::new(self.kind)) + .max_rect(max_rect); - let mut ui = Ui::new( - ctx.clone(), - self.layer_id, - self.layer_id.id, - max_rect, - clip_rect, - UiStackInfo::new(self.kind), - ); + if !self.enabled { + ui_builder = ui_builder.disabled(); + } + if self.sizing_pass { + ui_builder = ui_builder.sizing_pass(); + } + + let mut ui = Ui::new(ctx.clone(), self.layer_id, self.layer_id.id, ui_builder); + ui.set_clip_rect(self.constrain_rect); // Don't paint outside our bounds if self.fade_in { if let Some(last_became_visible_at) = self.state.last_became_visible_at { @@ -556,12 +559,6 @@ impl Prepared { } } - if !self.enabled { - ui.disable(); - } - if self.sizing_pass { - ui.set_sizing_pass(); - } ui } diff --git a/crates/egui/src/containers/combo_box.rs b/crates/egui/src/containers/combo_box.rs index cb29f277..5c6af803 100644 --- a/crates/egui/src/containers/combo_box.rs +++ b/crates/egui/src/containers/combo_box.rs @@ -425,7 +425,7 @@ fn button_frame( outer_rect.set_height(outer_rect.height().at_least(interact_size.y)); let inner_rect = outer_rect.shrink2(margin); - let mut content_ui = ui.child_ui(inner_rect, *ui.layout(), None); + let mut content_ui = ui.new_child(UiBuilder::new().max_rect(inner_rect)); add_contents(&mut content_ui); let mut outer_rect = content_ui.min_rect().expand2(margin); diff --git a/crates/egui/src/containers/frame.rs b/crates/egui/src/containers/frame.rs index 07cd679a..f5959840 100644 --- a/crates/egui/src/containers/frame.rs +++ b/crates/egui/src/containers/frame.rs @@ -250,10 +250,10 @@ impl Frame { inner_rect.max.x = inner_rect.max.x.max(inner_rect.min.x); inner_rect.max.y = inner_rect.max.y.max(inner_rect.min.y); - let content_ui = ui.child_ui( - inner_rect, - *ui.layout(), - Some(UiStackInfo::new(UiKind::Frame).with_frame(self)), + let content_ui = ui.new_child( + UiBuilder::new() + .ui_stack_info(UiStackInfo::new(UiKind::Frame).with_frame(self)) + .max_rect(inner_rect), ); // content_ui.set_clip_rect(outer_rect_bounds.shrink(self.stroke.width * 0.5)); // Can't do this since we don't know final size yet diff --git a/crates/egui/src/containers/panel.rs b/crates/egui/src/containers/panel.rs index 3b16c9c1..9aafa0cf 100644 --- a/crates/egui/src/containers/panel.rs +++ b/crates/egui/src/containers/panel.rs @@ -261,14 +261,15 @@ impl SidePanel { } } - let mut panel_ui = ui.child_ui_with_id_source( - panel_rect, - Layout::top_down(Align::Min), - id, - Some(UiStackInfo::new(match side { - Side::Left => UiKind::LeftPanel, - Side::Right => UiKind::RightPanel, - })), + let mut panel_ui = ui.new_child( + UiBuilder::new() + .id_source(id) + .ui_stack_info(UiStackInfo::new(match side { + Side::Left => UiKind::LeftPanel, + Side::Right => UiKind::RightPanel, + })) + .max_rect(panel_rect) + .layout(Layout::top_down(Align::Min)), ); panel_ui.expand_to_include_rect(panel_rect); panel_ui.set_clip_rect(panel_rect); // If we overflow, don't do so visibly (#4475) @@ -367,15 +368,13 @@ impl SidePanel { let layer_id = LayerId::background(); let side = self.side; let available_rect = ctx.available_rect(); - let clip_rect = ctx.screen_rect(); let mut panel_ui = Ui::new( ctx.clone(), layer_id, self.id, - available_rect, - clip_rect, - UiStackInfo::default(), + UiBuilder::new().max_rect(available_rect), ); + panel_ui.set_clip_rect(ctx.screen_rect()); let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents); let rect = inner_response.response.rect; @@ -750,14 +749,15 @@ impl TopBottomPanel { } } - let mut panel_ui = ui.child_ui_with_id_source( - panel_rect, - Layout::top_down(Align::Min), - id, - Some(UiStackInfo::new(match side { - TopBottomSide::Top => UiKind::TopPanel, - TopBottomSide::Bottom => UiKind::BottomPanel, - })), + let mut panel_ui = ui.new_child( + UiBuilder::new() + .id_source(id) + .ui_stack_info(UiStackInfo::new(match side { + TopBottomSide::Top => UiKind::TopPanel, + TopBottomSide::Bottom => UiKind::BottomPanel, + })) + .max_rect(panel_rect) + .layout(Layout::top_down(Align::Min)), ); panel_ui.expand_to_include_rect(panel_rect); panel_ui.set_clip_rect(panel_rect); // If we overflow, don't do so visibly (#4475) @@ -857,15 +857,13 @@ impl TopBottomPanel { let available_rect = ctx.available_rect(); let side = self.side; - let clip_rect = ctx.screen_rect(); let mut panel_ui = Ui::new( ctx.clone(), layer_id, self.id, - available_rect, - clip_rect, - UiStackInfo::default(), // set by show_inside_dyn + UiBuilder::new().max_rect(available_rect), ); + panel_ui.set_clip_rect(ctx.screen_rect()); let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents); let rect = inner_response.response.rect; @@ -1091,10 +1089,11 @@ impl CentralPanel { let Self { frame } = self; let panel_rect = ui.available_rect_before_wrap(); - let mut panel_ui = ui.child_ui( - panel_rect, - Layout::top_down(Align::Min), - Some(UiStackInfo::new(UiKind::CentralPanel)), + let mut panel_ui = ui.new_child( + UiBuilder::new() + .ui_stack_info(UiStackInfo::new(UiKind::CentralPanel)) + .max_rect(panel_rect) + .layout(Layout::top_down(Align::Min)), ); panel_ui.set_clip_rect(panel_rect); // If we overflow, don't do so visibly (#4475) @@ -1124,15 +1123,13 @@ impl CentralPanel { let layer_id = LayerId::background(); let id = Id::new((ctx.viewport_id(), "central_panel")); - let clip_rect = ctx.screen_rect(); let mut panel_ui = Ui::new( ctx.clone(), layer_id, id, - available_rect, - clip_rect, - UiStackInfo::default(), // set by show_inside_dyn + UiBuilder::new().max_rect(available_rect), ); + panel_ui.set_clip_rect(ctx.screen_rect()); let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents); diff --git a/crates/egui/src/containers/resize.rs b/crates/egui/src/containers/resize.rs index 4980c9c6..54a00af0 100644 --- a/crates/egui/src/containers/resize.rs +++ b/crates/egui/src/containers/resize.rs @@ -270,10 +270,10 @@ impl Resize { content_clip_rect = content_clip_rect.intersect(ui.clip_rect()); // Respect parent region - let mut content_ui = ui.child_ui( - inner_rect, - *ui.layout(), - Some(UiStackInfo::new(UiKind::Resize)), + let mut content_ui = ui.new_child( + UiBuilder::new() + .ui_stack_info(UiStackInfo::new(UiKind::Resize)) + .max_rect(inner_rect), ); content_ui.set_clip_rect(content_clip_rect); diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index 3f0e3a2d..20fd4e4f 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -560,10 +560,10 @@ impl ScrollArea { } let content_max_rect = Rect::from_min_size(inner_rect.min - state.offset, content_max_size); - let mut content_ui = ui.child_ui( - content_max_rect, - *ui.layout(), - Some(UiStackInfo::new(UiKind::ScrollArea)), + let mut content_ui = ui.new_child( + UiBuilder::new() + .ui_stack_info(UiStackInfo::new(UiKind::ScrollArea)) + .max_rect(content_max_rect), ); { @@ -732,7 +732,7 @@ impl ScrollArea { let rect = Rect::from_x_y_ranges(ui.max_rect().x_range(), y_min..=y_max); - ui.allocate_ui_at_rect(rect, |viewport_ui| { + ui.allocate_new_ui(UiBuilder::new().max_rect(rect), |viewport_ui| { viewport_ui.skip_ahead_auto_ids(min_row); // Make sure we get consistent IDs. add_contents(viewport_ui, min_row..max_row) }) diff --git a/crates/egui/src/grid.rs b/crates/egui/src/grid.rs index 2466c41e..e46f2e33 100644 --- a/crates/egui/src/grid.rs +++ b/crates/egui/src/grid.rs @@ -425,11 +425,14 @@ impl Grid { // then we should pick a default layout that matches that alignment, // which we do here: let max_rect = ui.cursor().intersect(ui.max_rect()); - ui.allocate_ui_at_rect(max_rect, |ui| { - if prev_state.is_none() { - // Hide the ui this frame, and make things as narrow as possible. - ui.set_sizing_pass(); - } + + let mut ui_builder = UiBuilder::new().max_rect(max_rect); + if prev_state.is_none() { + // Hide the ui this frame, and make things as narrow as possible. + ui_builder = ui_builder.sizing_pass(); + } + + ui.allocate_new_ui(ui_builder, |ui| { ui.horizontal(|ui| { let is_color = color_picker.is_some(); let mut grid = GridLayout { diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 92436215..9dc8dea6 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -400,6 +400,7 @@ mod sense; pub mod style; pub mod text_selection; mod ui; +mod ui_builder; mod ui_stack; pub mod util; pub mod viewport; @@ -442,7 +443,7 @@ pub mod text { }; } -pub use { +pub use self::{ containers::*, context::{Context, RepaintCause, RequestRepaintInfo}, data::{ @@ -467,6 +468,7 @@ pub use { style::{FontSelection, Style, TextStyle, Visuals}, text::{Galley, TextFormat}, ui::Ui, + ui_builder::UiBuilder, ui_stack::*, viewport::*, widget_rect::{WidgetRect, WidgetRects}, diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index b50dadbb..7e180a8f 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -75,20 +75,34 @@ impl Ui { // ------------------------------------------------------------------------ // Creation: - /// Create a new [`Ui`]. + /// Create a new top-level [`Ui`]. /// /// Normally you would not use this directly, but instead use /// [`SidePanel`], [`TopBottomPanel`], [`CentralPanel`], [`Window`] or [`Area`]. - pub fn new( - ctx: Context, - layer_id: LayerId, - id: Id, - max_rect: Rect, - clip_rect: Rect, - ui_stack_info: UiStackInfo, - ) -> Self { - let style = ctx.style(); - let layout = Layout::default(); + pub fn new(ctx: Context, layer_id: LayerId, id: Id, ui_builder: UiBuilder) -> Self { + let UiBuilder { + id_source, + ui_stack_info, + max_rect, + layout, + disabled, + invisible, + sizing_pass, + style, + } = ui_builder; + + debug_assert!( + id_source.is_none(), + "Top-level Ui:s should not have an id_source" + ); + + let max_rect = max_rect.unwrap_or_else(|| ctx.screen_rect()); + let clip_rect = max_rect; + let layout = layout.unwrap_or_default(); + let invisible = invisible || sizing_pass; + let disabled = disabled || invisible || sizing_pass; + let style = style.unwrap_or_else(|| ctx.style()); + let placer = Placer::new(max_rect, layout); let ui_stack = UiStack { id, @@ -98,7 +112,7 @@ impl Ui { min_rect: placer.min_rect(), max_rect: placer.max_rect(), }; - let ui = Ui { + let mut ui = Ui { id, next_auto_id_source: id.with("auto").value(), painter: Painter::new(ctx, layer_id, clip_rect), @@ -121,6 +135,16 @@ impl Ui { enabled: ui.enabled, }); + if disabled { + ui.disable(); + } + if invisible { + ui.set_invisible(); + } + if sizing_pass { + ui.set_sizing_pass(); + } + ui } @@ -130,25 +154,67 @@ impl Ui { /// [`Self::scope`] if needed. /// /// When in doubt, use `None` for the `UiStackInfo` argument. + #[deprecated = "Use ui.new_child() instead"] pub fn child_ui( &mut self, max_rect: Rect, layout: Layout, ui_stack_info: Option, ) -> Self { - self.child_ui_with_id_source(max_rect, layout, "child", ui_stack_info) + self.new_child( + UiBuilder::new() + .max_rect(max_rect) + .layout(layout) + .ui_stack_info(ui_stack_info.unwrap_or_default()), + ) } /// Create a new [`Ui`] at a specific region with a specific id. /// /// When in doubt, use `None` for the `UiStackInfo` argument. + #[deprecated = "Use ui.new_child() instead"] pub fn child_ui_with_id_source( &mut self, max_rect: Rect, - mut layout: Layout, + layout: Layout, id_source: impl Hash, ui_stack_info: Option, ) -> Self { + self.new_child( + UiBuilder::new() + .id_source(id_source) + .max_rect(max_rect) + .layout(layout) + .ui_stack_info(ui_stack_info.unwrap_or_default()), + ) + } + + /// Create a child `Ui` with the properties of the given builder. + pub fn new_child(&mut self, ui_builder: UiBuilder) -> Self { + let UiBuilder { + id_source, + ui_stack_info, + max_rect, + layout, + disabled, + invisible, + sizing_pass, + style, + } = ui_builder; + + let mut painter = self.painter.clone(); + + let id_source = id_source.unwrap_or_else(|| Id::from("child")); + let max_rect = max_rect.unwrap_or_else(|| self.available_rect_before_wrap()); + let mut layout = layout.unwrap_or(*self.layout()); + let invisible = invisible || sizing_pass; + let enabled = self.enabled && !disabled && !invisible && !sizing_pass; + if invisible { + painter.set_invisible(); + } + let sizing_pass = self.sizing_pass || sizing_pass; + let style = style.unwrap_or_else(|| self.style.clone()); + if self.sizing_pass { // During the sizing pass we want widgets to use up as little space as possible, // so that we measure the only the space we _need_. @@ -167,7 +233,7 @@ impl Ui { let ui_stack = UiStack { id: new_id, layout_direction: layout.main_dir, - info: ui_stack_info.unwrap_or_default(), + info: ui_stack_info, parent: Some(self.stack.clone()), min_rect: placer.min_rect(), max_rect: placer.max_rect(), @@ -175,11 +241,11 @@ impl Ui { let child_ui = Ui { id: new_id, next_auto_id_source, - painter: self.painter.clone(), - style: self.style.clone(), + painter, + style, placer, - enabled: self.enabled, - sizing_pass: self.sizing_pass, + enabled, + sizing_pass, menu_state: self.menu_state.clone(), stack: Arc::new(ui_stack), }; @@ -1130,16 +1196,10 @@ impl Ui { let item_spacing = self.spacing().item_spacing; let frame_rect = self.placer.next_space(desired_size, item_spacing); let child_rect = self.placer.justify_and_align(frame_rect, desired_size); - - let mut child_ui = self.child_ui(child_rect, layout, None); - 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, item_spacing); - - let response = self.interact(final_child_rect, child_ui.id, Sense::hover()); - InnerResponse::new(ret, response) + self.allocate_new_ui( + UiBuilder::new().max_rect(child_rect).layout(layout), + add_contents, + ) } /// Allocated the given rectangle and then adds content to that rectangle. @@ -1147,24 +1207,40 @@ impl Ui { /// 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. + #[deprecated = "Use `allocate_new_ui` instead"] pub fn allocate_ui_at_rect( &mut self, max_rect: Rect, add_contents: impl FnOnce(&mut Self) -> R, ) -> InnerResponse { - debug_assert!(max_rect.is_finite()); - let mut child_ui = self.child_ui(max_rect, *self.layout(), None); - let ret = add_contents(&mut child_ui); - let final_child_rect = child_ui.min_rect(); + self.allocate_new_ui(UiBuilder::new().max_rect(max_rect), add_contents) + } - self.placer.advance_after_rects( - final_child_rect, - final_child_rect, - self.spacing().item_spacing, - ); + /// Allocated space (`UiBuilder::max_rect`) and then add content to it. + /// + /// If the contents overflow, more space will be allocated. + /// When finished, the amount of space actually used (`min_rect`) will be allocated in the parent. + /// So you can request a lot of space and then use less. + pub fn allocate_new_ui( + &mut self, + ui_builder: UiBuilder, + add_contents: impl FnOnce(&mut Self) -> R, + ) -> InnerResponse { + self.allocate_new_ui_dyn(ui_builder, Box::new(add_contents)) + } - let response = self.interact(final_child_rect, child_ui.id, Sense::hover()); - InnerResponse::new(ret, response) + fn allocate_new_ui_dyn<'c, R>( + &mut self, + ui_builder: UiBuilder, + add_contents: Box R + 'c>, + ) -> InnerResponse { + let mut child_ui = self.new_child(ui_builder); + let inner = add_contents(&mut child_ui); + let rect = child_ui.min_rect(); + let item_spacing = self.spacing().item_spacing; + self.placer.advance_after_rects(rect, rect, item_spacing); + let response = self.interact(rect, child_ui.id, Sense::hover()); + InnerResponse::new(inner, response) } /// Convenience function to get a region to paint on. @@ -1194,7 +1270,10 @@ impl Ui { let painter = self.painter().with_clip_rect(clip_rect); (response, painter) } +} +/// # Scrolling +impl Ui { /// Adjust the scroll position of any parent [`ScrollArea`] so that the given [`Rect`] becomes visible. /// /// If `align` is [`Align::TOP`] it means "put the top of the rect at the top of the scroll area", etc. @@ -1367,9 +1446,12 @@ impl Ui { /// /// See also [`Self::add`] and [`Self::add_sized`]. 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)).inner - }) + self.allocate_new_ui( + UiBuilder::new() + .max_rect(max_rect) + .layout(Layout::centered_and_justified(Direction::TopDown)), + |ui| ui.add(widget), + ) .inner } @@ -1481,17 +1563,17 @@ impl Ui { /// }); /// # }); /// ``` + #[deprecated = "Use 'ui.scope_builder' instead"] pub fn add_visible_ui( &mut self, visible: bool, add_contents: impl FnOnce(&mut Ui) -> R, ) -> InnerResponse { - self.scope(|ui| { - if !visible { - ui.set_invisible(); - } - add_contents(ui) - }) + let mut ui_builder = UiBuilder::new(); + if !visible { + ui_builder = ui_builder.invisible(); + } + self.scope_builder(ui_builder, add_contents) } /// Add extra space before the next widget. @@ -1986,21 +2068,24 @@ impl Ui { id_source: impl Hash, add_contents: impl FnOnce(&mut Ui) -> R, ) -> InnerResponse { - self.scope_dyn(Box::new(add_contents), Id::new(id_source), None) + self.scope_dyn( + UiBuilder::new().id_source(id_source), + Box::new(add_contents), + ) } /// Push another level onto the [`UiStack`]. /// /// You can use this, for instance, to tag a group of widgets. + #[deprecated = "Use 'ui.scope_builder' instead"] pub fn push_stack_info( &mut self, ui_stack_info: UiStackInfo, add_contents: impl FnOnce(&mut Ui) -> R, ) -> InnerResponse { self.scope_dyn( + UiBuilder::new().ui_stack_info(ui_stack_info), Box::new(add_contents), - Id::new("child"), - Some(ui_stack_info), ) } @@ -2017,19 +2102,26 @@ impl Ui { /// # }); /// ``` pub fn scope(&mut self, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse { - self.scope_dyn(Box::new(add_contents), Id::new("child"), None) + self.scope_dyn(UiBuilder::new(), Box::new(add_contents)) } - fn scope_dyn<'c, R>( + /// Create a child, add content to it, and then allocate only what was used in the parent `Ui`. + pub fn scope_builder( &mut self, - add_contents: Box R + 'c>, - id_source: Id, - ui_stack_info: Option, + ui_builder: UiBuilder, + add_contents: impl FnOnce(&mut Ui) -> R, + ) -> InnerResponse { + self.scope_dyn(ui_builder, Box::new(add_contents)) + } + + /// Create a child, add content to it, and then allocate only what was used in the parent `Ui`. + pub fn scope_dyn<'c, R>( + &mut self, + ui_builder: UiBuilder, + add_contents: Box R + 'c>, ) -> InnerResponse { - let child_rect = self.available_rect_before_wrap(); let next_auto_id_source = self.next_auto_id_source; - let mut child_ui = - self.child_ui_with_id_source(child_rect, *self.layout(), id_source, ui_stack_info); + let mut child_ui = self.new_child(ui_builder); self.next_auto_id_source = next_auto_id_source; // HACK: we want `scope` to only increment this once, so that `ui.scope` is equivalent to `ui.allocate_space`. let ret = add_contents(&mut child_ui); let response = self.allocate_rect(child_ui.min_rect(), Sense::hover()); @@ -2089,7 +2181,7 @@ impl Ui { child_rect.min.x += indent; let mut child_ui = - self.child_ui_with_id_source(child_rect, *self.layout(), id_source, None); + self.new_child(UiBuilder::new().id_source(id_source).max_rect(child_rect)); let ret = add_contents(&mut child_ui); let left_vline = self.visuals().indent_has_left_vline; @@ -2240,7 +2332,10 @@ impl Ui { /// See also [`Self::with_layout`] for more options. #[inline] pub fn vertical(&mut self, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse { - self.with_layout_dyn(Layout::top_down(Align::Min), Box::new(add_contents)) + self.allocate_new_ui( + UiBuilder::new().layout(Layout::top_down(Align::Min)), + add_contents, + ) } /// Start a ui with vertical layout. @@ -2259,7 +2354,10 @@ impl Ui { &mut self, add_contents: impl FnOnce(&mut Ui) -> R, ) -> InnerResponse { - self.with_layout_dyn(Layout::top_down(Align::Center), Box::new(add_contents)) + self.allocate_new_ui( + UiBuilder::new().layout(Layout::top_down(Align::Center)), + add_contents, + ) } /// Start a ui with vertical layout. @@ -2277,9 +2375,9 @@ impl Ui { &mut self, add_contents: impl FnOnce(&mut Ui) -> R, ) -> InnerResponse { - self.with_layout_dyn( - Layout::top_down(Align::Center).with_cross_justify(true), - Box::new(add_contents), + self.allocate_new_ui( + UiBuilder::new().layout(Layout::top_down(Align::Center).with_cross_justify(true)), + add_contents, ) } @@ -2303,21 +2401,7 @@ impl Ui { layout: Layout, add_contents: impl FnOnce(&mut Self) -> R, ) -> InnerResponse { - self.with_layout_dyn(layout, Box::new(add_contents)) - } - - fn with_layout_dyn<'c, R>( - &mut self, - layout: Layout, - add_contents: Box R + 'c>, - ) -> InnerResponse { - let mut child_ui = self.child_ui(self.available_rect_before_wrap(), layout, None); - let inner = add_contents(&mut child_ui); - let rect = child_ui.min_rect(); - let item_spacing = self.spacing().item_spacing; - self.placer.advance_after_rects(rect, rect, item_spacing); - - InnerResponse::new(inner, self.interact(rect, child_ui.id, Sense::hover())) + self.allocate_new_ui(UiBuilder::new().layout(layout), add_contents) } /// This will make the next added widget centered and justified in the available space. @@ -2327,9 +2411,9 @@ impl Ui { &mut self, add_contents: impl FnOnce(&mut Self) -> R, ) -> InnerResponse { - self.with_layout_dyn( - Layout::centered_and_justified(Direction::TopDown), - Box::new(add_contents), + self.allocate_new_ui( + UiBuilder::new().layout(Layout::centered_and_justified(Direction::TopDown)), + add_contents, ) } @@ -2394,8 +2478,11 @@ impl Ui { pos, pos2(pos.x + column_width, self.max_rect().right_bottom().y), ); - let mut column_ui = - self.child_ui(child_rect, Layout::top_down_justified(Align::LEFT), None); + let mut column_ui = self.new_child( + UiBuilder::new() + .max_rect(child_rect) + .layout(Layout::top_down_justified(Align::LEFT)), + ); column_ui.set_width(column_width); column_ui }) @@ -2448,8 +2535,11 @@ impl Ui { pos, pos2(pos.x + column_width, self.max_rect().right_bottom().y), ); - let mut column_ui = - self.child_ui(child_rect, Layout::top_down_justified(Align::LEFT), None); + let mut column_ui = self.new_child( + UiBuilder::new() + .max_rect(child_rect) + .layout(Layout::top_down_justified(Align::LEFT)), + ); column_ui.set_width(column_width); column_ui }); @@ -2574,7 +2664,10 @@ impl Ui { (InnerResponse { inner, response }, payload) } +} +/// # Menus +impl Ui { /// Close the menu we are in (including submenus), if any. /// /// See also: [`Self::menu_button`] and [`Response::context_menu`]. diff --git a/crates/egui/src/ui_builder.rs b/crates/egui/src/ui_builder.rs new file mode 100644 index 00000000..7f2e8b92 --- /dev/null +++ b/crates/egui/src/ui_builder.rs @@ -0,0 +1,119 @@ +use std::{hash::Hash, sync::Arc}; + +use crate::{Id, Layout, Rect, Style, UiStackInfo}; + +#[allow(unused_imports)] // Used for doclinks +use crate::Ui; + +/// Build a [`Ui`] as the chlild of another [`Ui`]. +/// +/// By default, everything is inherited from the parent, +/// except for `max_rect` which by default is set to +/// the parent [`Ui::available_rect_before_wrap`]. +#[must_use] +#[derive(Default)] +pub struct UiBuilder { + pub id_source: Option, + pub ui_stack_info: UiStackInfo, + pub max_rect: Option, + pub layout: Option, + pub disabled: bool, + pub invisible: bool, + pub sizing_pass: bool, + pub style: Option>, +} + +impl UiBuilder { + #[inline] + pub fn new() -> Self { + Self::default() + } + + /// Seed the child `Ui` with this `id_source`, which will be mixed + /// with the [`Ui::id`] of the parent. + /// + /// You should give each [`Ui`] an `id_source` that is unique + /// within the parent, or give it none at all. + #[inline] + pub fn id_source(mut self, id_source: impl Hash) -> Self { + self.id_source = Some(Id::new(id_source)); + self + } + + /// Provide some information about the new `Ui` being built. + #[inline] + pub fn ui_stack_info(mut self, ui_stack_info: UiStackInfo) -> Self { + self.ui_stack_info = ui_stack_info; + self + } + + /// Set the max rectangle, within which widgets will go. + /// + /// New widgets will *try* to fit within this rectangle. + /// + /// Text labels will wrap to fit within `max_rect`. + /// Separator lines will span the `max_rect`. + /// + /// If a new widget doesn't fit within the `max_rect` then the + /// [`Ui`] will make room for it by expanding both `min_rect` and + /// + /// If not set, this will be set to the parent + /// [`Ui::available_rect_before_wrap`]. + #[inline] + pub fn max_rect(mut self, max_rect: Rect) -> Self { + self.max_rect = Some(max_rect); + self + } + + /// Override the layout. + /// + /// Will otherwise be inherited from the parent. + #[inline] + pub fn layout(mut self, layout: Layout) -> Self { + self.layout = Some(layout); + self + } + + /// Make the new `Ui` disabled, i.e. grayed-out and non-interactive. + /// + /// Note that if the parent `Ui` is disabled, the child will always be disabled. + #[inline] + pub fn disabled(mut self) -> Self { + self.disabled = true; + self + } + + /// Make the contents invisible. + /// + /// Will also disable the `Ui` (see [`Self::disabled`]). + /// + /// If the parent `Ui` is invisible, the child will always be invisible. + #[inline] + pub fn invisible(mut self) -> Self { + self.invisible = true; + self.disabled = true; + self + } + + /// Set to true in special cases where we do one frame + /// where we size up the contents of the Ui, without actually showing it. + /// + /// If the `sizing_pass` flag is set on the parent, + /// the child will inherit it automatically. + #[inline] + pub fn sizing_pass(mut self) -> Self { + self.sizing_pass = true; + self.invisible = true; + self.disabled = true; + self + } + + /// Override the style. + /// + /// Otherwise will inherit the style of the parent. + #[inline] + pub fn style(mut self, style: impl Into>) -> Self { + self.style = Some(style.into()); + self + } +} diff --git a/crates/egui_demo_lib/src/demo/widget_gallery.rs b/crates/egui_demo_lib/src/demo/widget_gallery.rs index f942c862..7be6e20c 100644 --- a/crates/egui_demo_lib/src/demo/widget_gallery.rs +++ b/crates/egui_demo_lib/src/demo/widget_gallery.rs @@ -61,10 +61,15 @@ impl crate::Demo for WidgetGallery { impl crate::View for WidgetGallery { fn ui(&mut self, ui: &mut egui::Ui) { - ui.add_enabled_ui(self.enabled, |ui| { - if !self.visible { - ui.set_invisible(); - } + let mut ui_builder = egui::UiBuilder::new(); + if !self.enabled { + ui_builder = ui_builder.disabled(); + } + if !self.visible { + ui_builder = ui_builder.invisible(); + } + + ui.scope_builder(ui_builder, |ui| { ui.multiply_opacity(self.opacity); egui::Grid::new("my_grid") diff --git a/crates/egui_extras/src/layout.rs b/crates/egui_extras/src/layout.rs index 1c8f6b0c..6eb951c8 100644 --- a/crates/egui_extras/src/layout.rs +++ b/crates/egui_extras/src/layout.rs @@ -1,4 +1,4 @@ -use egui::{Id, Pos2, Rect, Response, Sense, Ui}; +use egui::{Id, Pos2, Rect, Response, Sense, Ui, UiBuilder}; #[derive(Clone, Copy)] pub(crate) enum CellSize { @@ -197,11 +197,12 @@ impl<'l> StripLayout<'l> { child_ui_id_source: egui::Id, add_cell_contents: impl FnOnce(&mut Ui), ) -> Ui { - let mut child_ui = self.ui.child_ui_with_id_source( - max_rect, - self.cell_layout, - child_ui_id_source, - Some(egui::UiStackInfo::new(egui::UiKind::TableCell)), + let mut child_ui = self.ui.new_child( + UiBuilder::new() + .id_source(child_ui_id_source) + .ui_stack_info(egui::UiStackInfo::new(egui::UiKind::TableCell)) + .max_rect(max_rect) + .layout(self.cell_layout), ); if flags.clip { diff --git a/examples/custom_window_frame/src/main.rs b/examples/custom_window_frame/src/main.rs index 2912e5bb..bdf1fa63 100644 --- a/examples/custom_window_frame/src/main.rs +++ b/examples/custom_window_frame/src/main.rs @@ -71,7 +71,7 @@ fn custom_window_frame(ctx: &egui::Context, title: &str, add_contents: impl FnOn rect } .shrink(4.0); - let mut content_ui = ui.child_ui(content_rect, *ui.layout(), None); + let mut content_ui = ui.new_child(UiBuilder::new().max_rect(content_rect)); add_contents(&mut content_ui); }); } @@ -116,14 +116,17 @@ fn title_bar_ui(ui: &mut egui::Ui, title_bar_rect: eframe::epaint::Rect, title: ui.ctx().send_viewport_cmd(ViewportCommand::StartDrag); } - ui.allocate_ui_at_rect(title_bar_rect, |ui| { - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + ui.allocate_new_ui( + UiBuilder::new() + .max_rect(title_bar_rect) + .layout(egui::Layout::right_to_left(egui::Align::Center)), + |ui| { ui.spacing_mut().item_spacing.x = 0.0; ui.visuals_mut().button_frame = false; ui.add_space(8.0); close_maximize_minimize(ui); - }); - }); + }, + ); } /// Show some close/maximize/minimize buttons for the native window. diff --git a/tests/test_viewports/src/main.rs b/tests/test_viewports/src/main.rs index 401ef4c1..52cba800 100644 --- a/tests/test_viewports/src/main.rs +++ b/tests/test_viewports/src/main.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use eframe::egui; -use egui::{mutex::RwLock, Id, InnerResponse, ViewportBuilder, ViewportId}; +use egui::{mutex::RwLock, Id, InnerResponse, UiBuilder, ViewportBuilder, ViewportId}; // Drag-and-drop between windows is not yet implemented, but if you wanna work on it, enable this: pub const DRAG_AND_DROP_TEST: bool = false; @@ -460,7 +460,7 @@ fn drop_target( let available_rect = ui.available_rect_before_wrap(); let inner_rect = available_rect.shrink2(margin); - let mut content_ui = ui.child_ui(inner_rect, *ui.layout(), None); + let mut content_ui = ui.new_child(UiBuilder::new().max_rect(inner_rect)); let ret = body(&mut content_ui); let outer_rect =