diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 3e23bf9d1dc8..e36060a49f9b 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -120,6 +120,7 @@ impl Ui { let clip_rect = max_rect; let layout = layout.unwrap_or_default(); let disabled = disabled || invisible; + let sizing_pass = sizing_pass.unwrap_or(false); let style = style.unwrap_or_else(|| ctx.global_style()); let sense = sense.unwrap_or_else(Sense::hover); let classes = classes.with_class(class::ROOT); @@ -236,7 +237,7 @@ impl Ui { if invisible { painter.set_invisible(); } - let sizing_pass = self.sizing_pass || sizing_pass; + let sizing_pass = sizing_pass.unwrap_or(self.sizing_pass); let style = style.unwrap_or_else(|| Arc::clone(&self.style)); let sense = sense.unwrap_or_else(Sense::hover); diff --git a/crates/egui/src/ui_builder.rs b/crates/egui/src/ui_builder.rs index 07f53ec29347..2a1164eee713 100644 --- a/crates/egui/src/ui_builder.rs +++ b/crates/egui/src/ui_builder.rs @@ -24,7 +24,9 @@ pub struct UiBuilder { pub layout: Option, pub disabled: bool, pub invisible: bool, - pub sizing_pass: bool, + + /// `None` inherits from the parent; a root defaults to `false`. + pub sizing_pass: Option, pub style: Option>, pub sense: Option, pub accessibility_parent: Option, @@ -150,10 +152,27 @@ impl UiBuilder { /// 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. + /// the child will inherit it automatically unless explicitly overridden + /// with [`Self::no_sizing_pass`]. #[inline] pub fn sizing_pass(mut self) -> Self { - self.sizing_pass = true; + self.sizing_pass = Some(true); + self + } + + /// Disable sizing mode for the new [`Ui`], even if the parent is sizing. + /// + /// Use this for contents whose layout should not participate in sizing, + /// such as widgets clipped inside a fixed viewport. Descendants inherit + /// normal mode unless they explicitly enable [`Self::sizing_pass`]. + /// The new [`Ui`] retains its parent chain, visibility, enabled state, and + /// input behavior. This does not change whether the current pass is discarded. + /// + /// To use a centered or justified layout, also specify [`Self::layout`]: + /// the parent's inherited layout may already have been adjusted for sizing. + #[inline] + pub fn no_sizing_pass(mut self) -> Self { + self.sizing_pass = Some(false); self } diff --git a/crates/egui/tests/sizing_pass.rs b/crates/egui/tests/sizing_pass.rs new file mode 100644 index 000000000000..39368cff81b1 --- /dev/null +++ b/crates/egui/tests/sizing_pass.rs @@ -0,0 +1,67 @@ +//! Sizing mode defaults, inheritance, and layout behavior. + +use egui::{Align, Context, Id, Layout, Rect, Ui, UiBuilder, Vec2}; + +#[test] +fn root_sizing_mode_defaults_to_false() { + let ctx = Context::default(); + let mut output = ctx.run_ui(Default::default(), |_| { + for (name, builder, expected) in [ + ("default", UiBuilder::new(), false), + ("opt_out", UiBuilder::new().no_sizing_pass(), false), + ("sizing", UiBuilder::new().sizing_pass(), true), + ] { + let root = Ui::new(ctx.clone(), Id::unique(name), builder); + assert_eq!(root.is_sizing_pass(), expected, "{name}"); + } + }); + output.textures_delta.clear(); +} + +#[test] +fn children_inherit_and_override_sizing_mode() { + let ctx = Context::default(); + let mut output = ctx.run_ui(Default::default(), |ui| { + assert!(!ui.new_child(UiBuilder::new()).is_sizing_pass()); + + let mut sizing = ui.new_child(UiBuilder::new().sizing_pass()); + assert!(sizing.is_sizing_pass()); + assert!(sizing.new_child(UiBuilder::new()).is_sizing_pass()); + + let mut normal = sizing.new_child(UiBuilder::new().no_sizing_pass()); + assert!(!normal.is_sizing_pass()); + assert!(!normal.new_child(UiBuilder::new()).is_sizing_pass()); + + // A descendant can enable sizing again after an ancestor opted out. + assert!( + normal + .new_child(UiBuilder::new().sizing_pass()) + .is_sizing_pass() + ); + }); + output.textures_delta.clear(); +} + +#[test] +fn sizing_opt_out_preserves_requested_layout_and_widget_widths() { + let ctx = Context::default(); + let mut output = ctx.run_ui(Default::default(), |ui| { + let rect = Rect::from_min_size(ui.cursor().min, Vec2::new(248.0, 90.0)); + let mut parent = ui.new_child(UiBuilder::new().sizing_pass().max_rect(rect)); + let builder = UiBuilder::new() + .max_rect(rect) + .layout(Layout::top_down(Align::Center).with_cross_justify(true)); + let mut inherited = parent.new_child(builder.clone()); + assert!(!inherited.layout().cross_justify); + assert_eq!(inherited.layout().cross_align, Align::Min); + assert_eq!(inherited.separator().rect.width(), 0.0); + let mut child = parent.new_child(builder.no_sizing_pass()); + assert!(child.layout().cross_justify); + assert_eq!(child.layout().cross_align, Align::Center); + assert_eq!(child.separator().rect.width(), 248.0); + // Opting out does not reconstruct a layout already adjusted in an ancestor. + let descendant = inherited.new_child(UiBuilder::new().no_sizing_pass()); + assert_eq!(descendant.layout(), inherited.layout()); + }); + output.textures_delta.clear(); +} diff --git a/crates/egui_kittest/src/app_kind.rs b/crates/egui_kittest/src/app_kind.rs index e1293dabe559..349dd9ca5153 100644 --- a/crates/egui_kittest/src/app_kind.rs +++ b/crates/egui_kittest/src/app_kind.rs @@ -60,7 +60,7 @@ impl AppKind<'_, State> { fn run_ui(&mut self, ui: &mut egui::Ui, state: &mut State, sizing_pass: bool) -> UiRunOutput { let mut builder = egui::UiBuilder::new(); if sizing_pass { - builder.sizing_pass = true; + builder = builder.sizing_pass(); } let egui::InnerResponse { inner: ui_id,