From 9fe6aca1f1982a49c73d86aaed637c46801c7e1c Mon Sep 17 00:00:00 2001 From: iris <2+iris@noreply.localhost> Date: Sat, 12 Sep 2026 21:18:09 -0400 Subject: [PATCH] iris: make span compaction explicit --- app/src/ui/row.rs | 2 +- docs/PLAN.md | 9 ++++++ iris/examples/tabs/lib.rs | 2 ++ iris/examples/text/lib.rs | 28 ++++++----------- iris/src/layout_tests.rs | 57 ++++++++++++++++++++++++++++++++++ iris/src/widget/layout/span.rs | 22 ++++++++++++- 6 files changed, 100 insertions(+), 20 deletions(-) diff --git a/app/src/ui/row.rs b/app/src/ui/row.rs index c4a3710..c0e74bb 100644 --- a/app/src/ui/row.rs +++ b/app/src/ui/row.rs @@ -331,7 +331,7 @@ where None => Span::empty(Dir::DOWN).add(rsc), }; - let widget = (header, column.width(rest(1))) + let widget = (header, column) .span(Dir::DOWN) .gap(dp(4)) .pad(dp(10)) diff --git a/docs/PLAN.md b/docs/PLAN.md index d387939..4d86e32 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -847,6 +847,15 @@ such as corner radius and text's position within horizontal overflow use `Len`. Flexible padding participates in the same proportional allocation as a span rather than silently discarding its `rest` component. +**A `Span` leaves its children in the offered orthogonal region unless +explicitly compacted** (2026-09-12). It still reports the widest orthogonal +child as its intrinsic size, so a row nested in a column keeps its content +height rather than claiming the column's remaining height. By default it does +not squeeze child regions to that intrinsic size, because doing so makes child +alignment operate inside the widest child rather than a known-width container. +`.compact()` opts into that shrink-to-widest-child placement for deliberately +dense groups. + **Text overflow is treatment plus position, not alignment** (2026-09-12). `TextOverflow` selects `Visible`, `Wrap`, `Hidden`, or the single `Ellipsis` treatment. Unwrapped hidden and ellipsized text retains one canonical shaped diff --git a/iris/examples/tabs/lib.rs b/iris/examples/tabs/lib.rs index 1bd6728..0e24f01 100644 --- a/iris/examples/tabs/lib.rs +++ b/iris/examples/tabs/lib.rs @@ -100,10 +100,12 @@ where rect(PaintId::PURPLE).sized((50, 50)).align(Align::TOP), ) .span(Dir::RIGHT) + .compact() .center(), wtext("pretty cool right?").size(50), ) .span(Dir::DOWN) + .compact() .add(rsc); let texts = Span::empty(Dir::DOWN).gap(10).add(rsc); diff --git a/iris/examples/text/lib.rs b/iris/examples/text/lib.rs index b64ef12..42c1312 100644 --- a/iris/examples/text/lib.rs +++ b/iris/examples/text/lib.rs @@ -38,24 +38,17 @@ where .add(Srgba8::new(53, 57, 66, 255).to_linear()); let styled = "Bold, italic, underlined, and colored spans"; - let styled = wtext(styled) - .size(20) - .spans(vec![ - SpanStyle::new(0..4).bold(), - SpanStyle::new(6..12).italic(), - SpanStyle::new(14..24).underline(), - SpanStyle::new(30..37).color(PaintId::SKY), - ]) - .width(rest(1)); + let styled = wtext(styled).size(20).spans(vec![ + SpanStyle::new(0..4).bold(), + SpanStyle::new(6..12).italic(), + SpanStyle::new(14..24).underline(), + SpanStyle::new(30..37).color(PaintId::SKY), + ]); let aligned = ( - wtext("Left aligned") - .text_align(Align::CENTER_LEFT) - .width(rest(1)), - wtext("Centered").text_align(Align::CENTER).width(rest(1)), - wtext("Right aligned") - .text_align(Align::CENTER_RIGHT) - .width(rest(1)), + wtext("Left aligned").text_align(Align::CENTER_LEFT), + wtext("Centered").text_align(Align::CENTER), + wtext("Right aligned").text_align(Align::CENTER_RIGHT), ) .span(Dir::DOWN) .gap(dp(4)) @@ -84,8 +77,7 @@ where .spans(vec![SpanStyle::new(0..9).bold()]), wtext("Drag across display text to select it. The overflow markers select hidden source text, but are never copied.") .overflow(TextOverflow::Wrap) - .color(PaintId::GRAY) - .width(rest(1)), + .color(PaintId::GRAY), ) .span(Dir::DOWN) .gap(dp(6)) diff --git a/iris/src/layout_tests.rs b/iris/src/layout_tests.rs index ef1a50b..fc58399 100644 --- a/iris/src/layout_tests.rs +++ b/iris/src/layout_tests.rs @@ -204,6 +204,7 @@ fn a_span_reuses_unchanged_sibling_sizes_when_only_its_along_extent_changes() { children: vec![changed.any(), sibling.any()], dir: Dir::DOWN, gap: LayoutLen::ZERO, + compact_orthogonal: false, }); let root = rsc .ui @@ -231,6 +232,61 @@ fn a_span_reuses_unchanged_sibling_sizes_when_only_its_along_extent_changes() { ); } +#[test] +fn a_span_leaves_children_in_its_offered_orthogonal_axis_by_default() { + let mut rsc = TestRsc { ui: Ui::default() }; + let short = wtext("short").add(&mut rsc); + let root = (short, wtext("a much wider line")) + .span(Dir::DOWN) + .add_strong(&mut rsc); + let root_id = root.id(); + let root = root.any(); + let mut render = UiRenderState::new(); + render.resize((200.0, 100.0)); + render.update(&root, &mut rsc); + + let width = render.active[&root_id].size.x; + assert_eq!(width.rel, 0.0); + assert_eq!(width.rest, 0.0); + assert!(width.abs > 0.0 && width.abs < 200.0); + assert_eq!( + render.active[&short.id()] + .region + .to_px(vec2(200.0, 100.0)) + .size() + .x, + 200.0, + ); +} + +#[test] +fn a_compact_span_shrinks_its_orthogonal_axis_to_its_widest_child() { + let mut rsc = TestRsc { ui: Ui::default() }; + let short = wtext("short").add(&mut rsc); + let root = (short, wtext("a much wider line")) + .span(Dir::DOWN) + .compact() + .add_strong(&mut rsc); + let root_id = root.id(); + let root = root.any(); + let mut render = UiRenderState::new(); + render.resize((200.0, 100.0)); + render.update(&root, &mut rsc); + + let width = render.active[&root_id].size.x; + assert_eq!(width.rel, 0.0); + assert_eq!(width.rest, 0.0); + assert!(width.abs > 0.0 && width.abs < 200.0); + assert_eq!( + render.active[&short.id()] + .region + .to_px(vec2(200.0, 100.0)) + .size() + .x, + width.abs, + ); +} + #[test] fn a_child_coordinate_offset_moves_only_the_child_subtree() { let mut rsc = TestRsc { ui: Ui::default() }; @@ -1077,6 +1133,7 @@ fn a_span_of_padded_children_inside_a_span_draws_each_where_its_box_is() { children: vec![header.any(), inner.any()], dir: Dir::DOWN, gap: LayoutLen::ZERO, + compact_orthogonal: false, }); let mut list = LazySpan::new(Dir::DOWN, Pin::End); list.push_back(LazyItem::new(0, outer.any())); diff --git a/iris/src/widget/layout/span.rs b/iris/src/widget/layout/span.rs index f83606c..96a1ffc 100644 --- a/iris/src/widget/layout/span.rs +++ b/iris/src/widget/layout/span.rs @@ -5,6 +5,7 @@ pub struct Span { pub children: Vec, pub dir: Dir, pub gap: LayoutLen, + pub compact_orthogonal: bool, } impl Widget for Span { @@ -85,7 +86,7 @@ impl Widget for Span { } if ortho_mixed { ortho_len = LayoutLen::default(); - } else { + } else if self.compact_orthogonal { let ortho = ortho_len .apply_rest(painter.density()) .align(AxisAlign::Neg); @@ -111,9 +112,18 @@ impl Span { children: Vec::new(), dir, gap: LayoutLen::ZERO, + compact_orthogonal: false, } } + /// Shrink the span and its children to the widest orthogonal child + /// instead of leaving children in the orthogonal space offered by its + /// parent. + pub fn compact(mut self) -> Self { + self.compact_orthogonal = true; + self + } + pub fn gap(mut self, gap: impl Into) -> Self { self.gap = gap.into(); self @@ -132,6 +142,7 @@ pub struct SpanBuilder, } @@ -146,6 +157,7 @@ impl, Tag> WidgetFnTrait children: self.children.add(rsc).arr.into_iter().collect(), dir: self.dir, gap: self.gap, + compact_orthogonal: self.compact_orthogonal, } } } @@ -158,6 +170,7 @@ impl, Tag> children, dir, gap: LayoutLen::ZERO, + compact_orthogonal: false, _pd: PhantomData, } } @@ -166,6 +179,13 @@ impl, Tag> self.gap = gap.into(); self } + + /// Shrink the span and its children to the widest orthogonal child + /// instead of filling the orthogonal space offered by its parent. + pub fn compact(mut self) -> Self { + self.compact_orthogonal = true; + self + } } impl std::ops::Deref for Span {