From 7711b49f6b11d45f14338c4ba81a9089b1d07828 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 21 Aug 2025 06:22:22 +0200 Subject: [PATCH 1/3] Propagate `Limits::compression` in `flex` layout --- core/src/layout/flex.rs | 50 ++++++++++++++++++++------------------- core/src/layout/limits.rs | 41 +++++++++++++++++++++++--------- 2 files changed, 56 insertions(+), 35 deletions(-) diff --git a/core/src/layout/flex.rs b/core/src/layout/flex.rs index 3f28f0d9..bec80427 100644 --- a/core/src/layout/flex.rs +++ b/core/src/layout/flex.rs @@ -78,13 +78,13 @@ where let total_spacing = spacing * items.len().saturating_sub(1) as f32; let max_cross = axis.cross(limits.max()); + let compression = limits.compression(); + let (main_compress, cross_compress) = + axis.pack(compression.width, compression.height); + let mut fill_main_sum = 0; let mut some_fill_cross = false; - let (mut cross, cross_compress) = match axis { - Axis::Vertical if width == Length::Shrink => (0.0, true), - Axis::Horizontal if height == Length::Shrink => (0.0, true), - _ => (max_cross, false), - }; + let mut cross = if cross_compress { 0.0 } else { max_cross }; let mut available = axis.main(limits.max()) - total_spacing; @@ -103,7 +103,8 @@ where axis.pack(size.width.fill_factor(), size.height.fill_factor()) }; - if fill_main_factor == 0 && (!cross_compress || fill_cross_factor == 0) + if (main_compress || fill_main_factor == 0) + && (!cross_compress || fill_cross_factor == 0) { let (max_width, max_height) = axis.pack( available, @@ -114,8 +115,11 @@ where }, ); - let child_limits = - Limits::new(Size::ZERO, Size::new(max_width, max_height)); + let child_limits = Limits::with_compression( + Size::ZERO, + Size::new(max_width, max_height), + compression, + ); let layout = child.as_widget_mut().layout(tree, renderer, &child_limits); @@ -159,8 +163,11 @@ where let (max_width, max_height) = axis.pack(available, cross); - let child_limits = - Limits::new(Size::ZERO, Size::new(max_width, max_height)); + let child_limits = Limits::with_compression( + Size::ZERO, + Size::new(max_width, max_height), + compression, + ); let layout = child.as_widget_mut().layout(tree, renderer, &child_limits); @@ -174,16 +181,7 @@ where } } - let remaining = match axis { - Axis::Horizontal => match width { - Length::Shrink => 0.0, - _ => available.max(0.0), - }, - Axis::Vertical => match height { - Length::Shrink => 0.0, - _ => available.max(0.0), - }, - }; + let remaining = available.max(0.0); // THIRD PASS // We lay out the elements that are fluid in the main axis. @@ -196,7 +194,7 @@ where axis.pack(size.width.fill_factor(), size.height.fill_factor()) }; - if fill_main_factor != 0 { + if !main_compress && fill_main_factor != 0 { let max_main = remaining * fill_main_factor as f32 / fill_main_sum as f32; @@ -222,9 +220,10 @@ where }, ); - let child_limits = Limits::new( + let child_limits = Limits::with_compression( Size::new(min_width, min_height), Size::new(max_width, max_height), + compression, ); let layout = @@ -254,8 +253,11 @@ where let (max_width, max_height) = axis.pack(main, cross); - let child_limits = - Limits::new(Size::ZERO, Size::new(max_width, max_height)); + let child_limits = Limits::with_compression( + Size::ZERO, + Size::new(max_width, max_height), + compression, + ); let layout = child.as_widget_mut().layout(tree, renderer, &child_limits); diff --git a/core/src/layout/limits.rs b/core/src/layout/limits.rs index 2bff8a55..888bc3fd 100644 --- a/core/src/layout/limits.rs +++ b/core/src/layout/limits.rs @@ -6,7 +6,7 @@ use crate::{Length, Size}; pub struct Limits { min: Size, max: Size, - compress: Size, + compression: Size, } impl Limits { @@ -14,15 +14,25 @@ impl Limits { pub const NONE: Limits = Limits { min: Size::ZERO, max: Size::INFINITE, - compress: Size::new(false, false), + compression: Size::new(false, false), }; /// Creates new [`Limits`] with the given minimum and maximum [`Size`]. pub const fn new(min: Size, max: Size) -> Limits { + Limits::with_compression(min, max, Size::new(false, false)) + } + + /// Creates new [`Limits`] with the given minimun and maximum [`Size`], and + /// whether fluid lengths should be compressed to intrinsic dimensions. + pub const fn with_compression( + min: Size, + max: Size, + compress: Size, + ) -> Self { Limits { min, max, - compress: Size::new(false, false), + compression: compress, } } @@ -36,18 +46,23 @@ impl Limits { self.max } + /// Returns the compression of the [`Limits`]. + pub fn compression(&self) -> Size { + self.compression + } + /// Applies a width constraint to the current [`Limits`]. pub fn width(mut self, width: impl Into) -> Limits { match width.into() { Length::Shrink => { - self.compress.width = true; + self.compression.width = true; } Length::Fixed(amount) => { let new_width = amount.min(self.max.width).max(self.min.width); self.min.width = new_width; self.max.width = new_width; - self.compress.width = false; + self.compression.width = false; } Length::Fill | Length::FillPortion(_) => {} } @@ -59,7 +74,7 @@ impl Limits { pub fn height(mut self, height: impl Into) -> Limits { match height.into() { Length::Shrink => { - self.compress.height = true; + self.compression.height = true; } Length::Fixed(amount) => { let new_height = @@ -67,7 +82,7 @@ impl Limits { self.min.height = new_height; self.max.height = new_height; - self.compress.height = false; + self.compression.height = false; } Length::Fill | Length::FillPortion(_) => {} } @@ -120,7 +135,7 @@ impl Limits { Limits { min, max, - compress: self.compress, + compression: self.compression, } } @@ -129,7 +144,7 @@ impl Limits { Limits { min: Size::ZERO, max: self.max, - compress: self.compress, + compression: self.compression, } } @@ -143,7 +158,9 @@ impl Limits { intrinsic_size: Size, ) -> Size { let width = match width.into() { - Length::Fill | Length::FillPortion(_) if !self.compress.width => { + Length::Fill | Length::FillPortion(_) + if !self.compression.width => + { self.max.width } Length::Fixed(amount) => { @@ -153,7 +170,9 @@ impl Limits { }; let height = match height.into() { - Length::Fill | Length::FillPortion(_) if !self.compress.height => { + Length::Fill | Length::FillPortion(_) + if !self.compression.height => + { self.max.height } Length::Fixed(amount) => { From 3ad578e24823bf4612c3573efb1ad481bb16134e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 21 Aug 2025 18:16:46 +0200 Subject: [PATCH 2/3] Skip third `flex` pass entirely if main axis is compressed --- core/src/layout/flex.rs | 90 +++++++++++++++++++------------------ examples/layout/src/main.rs | 8 ++-- 2 files changed, 50 insertions(+), 48 deletions(-) diff --git a/core/src/layout/flex.rs b/core/src/layout/flex.rs index bec80427..0afc6e1b 100644 --- a/core/src/layout/flex.rs +++ b/core/src/layout/flex.rs @@ -155,7 +155,9 @@ where axis.pack(size.width, size.height) }; - if main_size.fill_factor() == 0 && cross_size.fill_factor() != 0 { + if (main_compress || main_size.fill_factor() == 0) + && cross_size.fill_factor() != 0 + { if let Length::Fixed(main) = main_size { available -= main; continue; @@ -183,54 +185,57 @@ where let remaining = available.max(0.0); - // THIRD PASS + // THIRD PASS (conditional) // We lay out the elements that are fluid in the main axis. // We use the remaining space to evenly allocate space based on fill factors. - for (i, (child, tree)) in items.iter_mut().zip(trees.iter_mut()).enumerate() - { - let (fill_main_factor, fill_cross_factor) = { - let size = child.as_widget().size(); + if !main_compress { + for (i, (child, tree)) in + items.iter_mut().zip(trees.iter_mut()).enumerate() + { + let (fill_main_factor, fill_cross_factor) = { + let size = child.as_widget().size(); - axis.pack(size.width.fill_factor(), size.height.fill_factor()) - }; - - if !main_compress && fill_main_factor != 0 { - let max_main = - remaining * fill_main_factor as f32 / fill_main_sum as f32; - - let max_main = if max_main.is_nan() { - f32::INFINITY - } else { - max_main + axis.pack(size.width.fill_factor(), size.height.fill_factor()) }; - let min_main = if max_main.is_infinite() { - 0.0 - } else { - max_main - }; + if fill_main_factor != 0 { + let max_main = + remaining * fill_main_factor as f32 / fill_main_sum as f32; - let (min_width, min_height) = axis.pack(min_main, 0.0); - let (max_width, max_height) = axis.pack( - max_main, - if fill_cross_factor == 0 { - max_cross + let max_main = if max_main.is_nan() { + f32::INFINITY } else { - cross - }, - ); + max_main + }; - let child_limits = Limits::with_compression( - Size::new(min_width, min_height), - Size::new(max_width, max_height), - compression, - ); + let min_main = if max_main.is_infinite() { + 0.0 + } else { + max_main + }; - let layout = - child.as_widget_mut().layout(tree, renderer, &child_limits); - cross = cross.max(axis.cross(layout.size())); + let (min_width, min_height) = axis.pack(min_main, 0.0); + let (max_width, max_height) = axis.pack( + max_main, + if fill_cross_factor == 0 { + max_cross + } else { + cross + }, + ); - nodes[i] = layout; + let child_limits = Limits::with_compression( + Size::new(min_width, min_height), + Size::new(max_width, max_height), + compression, + ); + + let layout = + child.as_widget_mut().layout(tree, renderer, &child_limits); + cross = cross.max(axis.cross(layout.size())); + + nodes[i] = layout; + } } } @@ -253,11 +258,8 @@ where let (max_width, max_height) = axis.pack(main, cross); - let child_limits = Limits::with_compression( - Size::ZERO, - Size::new(max_width, max_height), - compression, - ); + let child_limits = + Limits::new(Size::ZERO, Size::new(max_width, max_height)); let layout = child.as_widget_mut().layout(tree, renderer, &child_limits); diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs index f477c406..c15a0bfc 100644 --- a/examples/layout/src/main.rs +++ b/examples/layout/src/main.rs @@ -94,14 +94,14 @@ impl Layout { let controls = row([ (!self.example.is_first()).then_some( - button("← Previous") + button(text("← Previous").shaping(text::Shaping::Advanced)) .padding([5, 10]) .on_press(Message::Previous) .into(), ), Some(horizontal_space().into()), (!self.example.is_last()).then_some( - button("Next →") + button(text("Next →").shaping(text::Shaping::Advanced)) .padding([5, 10]) .on_press(Message::Next) .into(), @@ -294,7 +294,7 @@ fn quotes<'a>() -> Element<'a, Message> { fn quote<'a>( content: impl Into>, ) -> Element<'a, Message> { - row![vertical_rule(2), content.into()] + row![vertical_rule(1), content.into()] .spacing(10) .height(Shrink) .into() @@ -313,7 +313,7 @@ fn quotes<'a>() -> Element<'a, Message> { "This is another reply", ), horizontal_rule(1), - "A separator ↑", + text("A separator ↑").shaping(text::Shaping::Advanced), ] .width(Shrink) .spacing(10) From b476ab277a03b1fa87221defcd9489fba43ed89d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 21 Aug 2025 23:06:07 +0200 Subject: [PATCH 3/3] Remove `debug_assert!` from `scrollable` by enforcing compression --- widget/src/scrollable.rs | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 0b0b6f9b..9c4748ea 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -111,22 +111,12 @@ where class: Theme::default(), last_status: None, } - .validate() + .enclose() } - fn validate(mut self) -> Self { + fn enclose(mut self) -> Self { let size_hint = self.content.as_widget().size_hint(); - debug_assert!( - self.direction.vertical().is_none() || !size_hint.height.is_fill(), - "scrollable content must not fill its vertical scrolling axis" - ); - - debug_assert!( - self.direction.horizontal().is_none() || !size_hint.width.is_fill(), - "scrollable content must not fill its horizontal scrolling axis" - ); - if self.direction.horizontal().is_none() { self.width = self.width.enclose(size_hint.width); } @@ -146,7 +136,7 @@ where /// Sets the [`Direction`] of the [`Scrollable`]. pub fn direction(mut self, direction: impl Into) -> Self { self.direction = direction.into(); - self.validate() + self.enclose() } /// Sets the [`Id`] of the [`Scrollable`]. @@ -437,20 +427,24 @@ where ..Padding::ZERO }, |limits| { - let child_limits = layout::Limits::new( + let is_horizontal = self.direction.horizontal().is_some(); + let is_vertical = self.direction.vertical().is_some(); + + let child_limits = layout::Limits::with_compression( limits.min(), Size::new( - if self.direction.horizontal().is_some() { + if is_horizontal { f32::INFINITY } else { limits.max().width }, - if self.direction.vertical().is_some() { + if is_vertical { f32::INFINITY } else { limits.max().height }, ), + Size::new(is_horizontal, is_vertical), ); self.content.as_widget_mut().layout(