From 3da1c718708056940ef152dd5f289bf5920b22e6 Mon Sep 17 00:00:00 2001 From: iris-ai <4+iris-ai@noreply.localhost> Date: Sat, 19 Sep 2026 23:24:55 -0400 Subject: [PATCH] Name the values layout carries, and say what a span's slot is `along` said nothing about what it did. It is `Span::slot` now: the stretch of the row between two distances from where the span starts laying out, as a span of its own box, with the mirror for a negative direction in one place. `far` is `row`, which is what the comment above it already called it, and `shares` is `has_room` beside the `any_leftover` it was folded into. `reached` now guards on the leftover weight it divides by rather than on the numerator that happened to be zero with it. The pairs layout returns are named rather than positional: `Answer` {size, holds} and `Drawn` {answer, drawing_holds} replace `(Size, LayoutHolds)` and a three-tuple with two `LayoutHolds` in it, which was the one shape the cold dump exists to catch. `try_reuse` answers `bool` rather than `Option<()>`, and the four hand-written copies of `move_idx != parent_move` are `ActiveData::is_region_node`. `AXES` was declared in three modules; it is `Axis::BOTH`. `rel_min`, `rel_max` and the unused `select_len` are gone -- `ZERO` and `FULL` already said those. Three doc comments sat on `impl` blocks instead of the single method inside them. `reposition` and `redepth` walked their children by index, looking the parent up again per child; both take the list and put it back. `Scroll`'s `fixed` and `fixed_len` are `answer_px` and `answer_is_px`, which says which one is the length. fmt, workspace clippy under `-D warnings` with and without `layout-diagnostics`, and the workspace tests are clean. The cold dump over 400 depth-5 trees is byte-identical to `6c84b6f`: 34,492 boxes, no seed moved. --- core/src/orientation/axis.rs | 3 + core/src/orientation/pos.rs | 12 --- core/src/ui/active.rs | 18 +++- core/src/ui/layout_holds.rs | 5 +- core/src/ui/painter.rs | 49 +++++------ core/src/ui/render_state.rs | 160 +++++++++++++++++----------------- src/widget/position/scroll.rs | 12 +-- src/widget/position/span.rs | 56 +++++++----- tests/cases/retained.rs | 2 +- 9 files changed, 167 insertions(+), 150 deletions(-) diff --git a/core/src/orientation/axis.rs b/core/src/orientation/axis.rs index 7e73f41..359aea1 100644 --- a/core/src/orientation/axis.rs +++ b/core/src/orientation/axis.rs @@ -9,6 +9,9 @@ pub enum Axis { } impl Axis { + /// Both of them, for the layout code that asks the same question of each. + pub const BOTH: [Self; 2] = [Self::X, Self::Y]; + /// A per-axis pair with `aligned` on this axis and `ortho` on the other, /// which is what `from_axis` does for a vector. pub fn pair(self, aligned: T, ortho: T) -> [T; 2] { diff --git a/core/src/orientation/pos.rs b/core/src/orientation/pos.rs index fa9fd8a..e4a87d9 100644 --- a/core/src/orientation/pos.rs +++ b/core/src/orientation/pos.rs @@ -163,14 +163,6 @@ impl Len { Self::from_parts(Rel::ZERO, Px::from_f32(px)) } - pub const fn rel_min() -> Self { - Self::ZERO - } - - pub const fn rel_max() -> Self { - Self::FULL - } - pub const fn max(&self, other: Self) -> Self { Self { rel: self.rel.max(other.rel), @@ -213,10 +205,6 @@ impl Len { }) } - pub fn select_len(&self, len: Len) -> Self { - len.within_len(*self) - } - pub const fn flip(&mut self) { self.rel = Rel::ONE.sub(self.rel); self.px = self.px.neg(); diff --git a/core/src/ui/active.rs b/core/src/ui/active.rs index ab0a493..401ac5a 100644 --- a/core/src/ui/active.rs +++ b/core/src/ui/active.rs @@ -27,7 +27,7 @@ pub struct ActiveData { pub region: UiRegion, /// The measured answer and its dependencies. A hint-only dependency or /// a widget first encountered during placement has no measurement yet. - pub answer: Option<(Size, LayoutHolds)>, + pub answer: Option, /// Asked more than once in its parent's last draw -- measured in one box /// and then asked in the one the parent decided. The parent's layout /// rests on the first answer and its drawing on the last, so only the @@ -82,6 +82,20 @@ impl ActiveData { /// all. Not `size`, which is what its last drawing reported: a drawing /// re-expressed in the box that answer chose is not a second answer. pub fn measured(&self) -> Option { - self.answer.map(|(size, _)| size) + self.answer.map(|answer| answer.size) + } + + /// Whether it owns a region node rather than sharing the one it was drawn + /// under, which is what its two move indices being different says. + pub fn is_region_node(&self) -> bool { + self.move_idx != self.parent_move } } + +/// What a widget answered when it was asked: the size it reported, and the +/// boxes and windows that answer holds for. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Answer { + pub size: Size, + pub holds: LayoutHolds, +} diff --git a/core/src/ui/layout_holds.rs b/core/src/ui/layout_holds.rs index 7ba7e3b..e52a916 100644 --- a/core/src/ui/layout_holds.rs +++ b/core/src/ui/layout_holds.rs @@ -1,8 +1,6 @@ use crate::util::impl_axis_index; use crate::{Axis, Holds, Len, Px, PxVec2, UiRegion, UiVec2}; -const AXES: [Axis; 2] = [Axis::X, Axis::Y]; - /// What one evaluation of a widget depends on along one axis: the window /// lengths its reads hold for, the pixel lengths of its own box, and the /// symbolic lengths of that box and of its rel base where either one is what @@ -101,7 +99,8 @@ impl LayoutHolds { } pub fn contains(self, window: PxVec2, rel_base: UiVec2, region: UiRegion) -> bool { - AXES.into_iter() + Axis::BOTH + .into_iter() .all(|axis| self[axis].contains(window[axis], rel_base[axis], region[axis].len())) } } diff --git a/core/src/ui/painter.rs b/core/src/ui/painter.rs index c2dc94f..39e8108 100644 --- a/core/src/ui/painter.rs +++ b/core/src/ui/painter.rs @@ -10,7 +10,6 @@ use crate::{ }, ui::render_state::{DrawInfo, Placing}, }; -const AXES: [Axis; 2] = [Axis::X, Axis::Y]; /// makes your surfaces look pretty pub struct Painter<'a> { @@ -155,8 +154,8 @@ impl<'a> Painter<'a> { /// where that is this widget's own narrowed the way the region is. An /// axis the region leaves whole is not read at all, so a wrapper that /// only moves its child does not pin its drawing to a rel base. - fn state_rel_base(&mut self, mut place: PlaceDesc) -> PlaceDesc { - for axis in AXES { + fn resolve_rel_base(&mut self, mut place: PlaceDesc) -> PlaceDesc { + for axis in Axis::BOTH { if let Some(span) = place[axis].narrows_rel_base() { let len = span.len(); let stated = (len != Len::FULL).then(|| len.within_len(self.rel_base(axis))); @@ -181,7 +180,7 @@ impl<'a> Painter<'a> { id: &'s StrongWidget, place: impl Into, ) -> DrawResult<'s, 'a, W> { - let place = self.state_rel_base(place.into()); + let place = self.resolve_rel_base(place.into()); let region_node = self.rsc.widgets().is_region_node(id.id()); let declared = self.declared_lens(id); let align = self.rsc.widgets().alignment(id.id()); @@ -198,7 +197,7 @@ impl<'a> Painter<'a> { self.children.push(id.id()); } let px = rel_base.to_px(self.window); - let (size, answer_holds, holds) = self.state.draw_inner( + let drawn = self.state.draw_inner( id.id(), DrawInfo { layer: self.layer, @@ -217,8 +216,8 @@ impl<'a> Painter<'a> { None, self.rsc, ); - let holds = self.in_parent(holds, region, place, declared); - let answer_holds = self.in_parent(answer_holds, region, place, declared); + let holds = self.in_parent(drawn.drawing_holds, region, place, declared); + let answer_holds = self.in_parent(drawn.answer.holds, region, place, declared); match self.under.iter_mut().find(|(child, _)| *child == id.id()) { Some((_, kept)) => *kept = holds, None => self.under.push((id.id(), holds)), @@ -226,7 +225,7 @@ impl<'a> Painter<'a> { DrawResult { child: id, painter: self, - size, + size: drawn.answer.size, answer_holds, } } @@ -255,8 +254,8 @@ impl<'a> Painter<'a> { id: &'s StrongWidget, place: impl Into, ) -> DrawResult<'s, 'a, W> { - let place = self.state_rel_base(place.into()); - let states_rel_base = AXES + let place = self.resolve_rel_base(place.into()); + let states_rel_base = Axis::BOTH .iter() .any(|&axis| place[axis].stated_rel_base().is_some()); if states_rel_base || !self.children.contains(&id.id()) { @@ -579,10 +578,10 @@ impl PrimitiveLike for &TextureHandle { } } -/// Moves what a child depends on into this widget's own terms: this -/// method's `impl` block is where a `Painter`'s own boxes are, so it takes -/// only what the child was asked with. impl Painter<'_> { + /// Moves what a child depends on into this widget's own terms, taking + /// only what the child was asked with. + /// /// Window ranges are already about the one unit and combine directly. /// A rel base pin becomes this widget's own rel base wherever a length of it /// is what reached the child; where only pixels did, no length of this @@ -602,7 +601,7 @@ impl Painter<'_> { declared: Declared, ) -> LayoutHolds { let mut result = LayoutHolds::ANY; - for axis in AXES { + for axis in Axis::BOTH { let declared = declared[axis]; let holds = holds[axis]; let result = &mut result[axis]; @@ -677,16 +676,16 @@ impl LayoutLen { } } -/// Where a widget's drawing goes inside the part its parent gave it: what -/// it reported, on the side of the part its alignment says, and the whole -/// part wherever the answer fills it. -/// -/// The length it reported is a length of its rel base, and the part is one too, -/// so this takes one from the other rather than composing it into the part. -/// That is what makes a fraction the same fraction wherever the part it is -/// placed in sits and however long it is -- the fraction is resolved once, -/// here, against the rel base it was reported of. impl PlaceDesc { + /// Where a widget's drawing goes inside the part its parent gave it: what + /// it reported, on the side of the part its alignment says, and the whole + /// part wherever the answer fills it. + /// + /// The length it reported is a length of its rel base, and the part is one + /// too, so this takes one from the other rather than composing it into the + /// part. That is what makes a fraction the same fraction wherever the part + /// it is placed in sits and however long it is -- the fraction is resolved + /// once, here, against the rel base it was reported of. pub(crate) fn placement( self, region: UiRegion, @@ -695,7 +694,7 @@ impl PlaceDesc { align: RegionAlign, ) -> UiRegion { let mut placed = region; - for axis in AXES { + for axis in Axis::BOTH { let reported = size[axis]; if reported.fills(declared[axis], self[axis].does_fill()) { continue; @@ -725,7 +724,7 @@ impl PlaceDesc { let given = self.of(own, align); let mut rel_base = parent_rel_base; let mut region = given; - for axis in AXES { + for axis in Axis::BOTH { let base = self[axis] .stated_rel_base() .unwrap_or_else(|| parent_rel_base[axis]); diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index fdcc65f..a34f01f 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -1,14 +1,12 @@ #[cfg(feature = "layout-diagnostics")] use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind}; use crate::{ - ActiveData, Axis, Declared, DrawLayers, IdLike, LayoutHolds, LayoutLen, Len, MaskIdx, MoveIdx, - Moves, Painter, PixelRegion, PlaceDesc, PxVec2, Rel, Size, StrongWidget, UiRegion, UiRsc, - UiSpan, UiVec2, Weight, WidgetId, Widgets, + ActiveData, Answer, Axis, Declared, DrawLayers, IdLike, LayoutHolds, LayoutLen, Len, MaskIdx, + MoveIdx, Moves, Painter, PixelRegion, PlaceDesc, PxVec2, Rel, Size, StrongWidget, UiRegion, + UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets, util::{HashMap, Vec2}, }; -const AXES: [Axis; 2] = [Axis::X, Axis::Y]; - /// Where a widget is drawn: what its parent decides about the draw besides /// the boxes themselves. #[derive(Clone, Copy)] @@ -36,6 +34,14 @@ pub(super) struct DrawInfo { pub px: PxVec2, } +/// What one draw of a widget came to: the answer it gave, and the boxes and +/// windows the drawing that gave it holds for. The two are separate ranges -- +/// a drawing can be invalid where its answer still stands. +pub(super) struct Drawn { + pub answer: Answer, + pub drawing_holds: LayoutHolds, +} + /// What a widget's children are placed in: its own box, the coordinates its /// drawing is in, and what else one ask of a child is decided from. pub(super) struct Placing { @@ -113,7 +119,7 @@ impl UiRenderState { // it will ask either again. let answer = active .answer - .is_some_and(|(_, holds)| holds.contains(size, active.rel_base, active.region)); + .is_some_and(|answer| answer.holds.contains(size, active.rel_base, active.region)); answer && active.holds.contains(size, active.rel_base, active.region) }); if !stands { @@ -207,7 +213,7 @@ impl UiRenderState { info: DrawInfo, mut old: Option, rsc: &mut dyn UiRsc, - ) -> (Size, LayoutHolds, LayoutHolds) { + ) -> Drawn { let old_parent = old .as_ref() .or_else(|| self.active.get(&id)) @@ -233,9 +239,9 @@ impl UiRenderState { .then(|| self.retained_answer(id, region, info)) .flatten() .and_then(|answer| { - let placed = info.placed.placement(region, answer.0, declared, align); + let placed = info.placed.placement(region, answer.size, declared, align); self.try_reuse(id, region, placed, info, rsc) - .map(|()| answer) + .then_some(answer) }); let answer = reused.unwrap_or_else(|| { if old.is_none() { @@ -245,7 +251,7 @@ impl UiRenderState { // Where the drawing goes: the part its parent gave it, with the // answer placed inside that part on any axis the parent left // open. - let placed = info.placed.placement(region, answer.0, declared, align); + let placed = info.placed.placement(region, answer.size, declared, align); if placed != region { self.relocate(id, placed, info, rsc); } @@ -273,7 +279,10 @@ impl UiRenderState { { old_parent.children.retain(|child| *child != id); } - (answer.0, answer.1, drawing_holds) + Drawn { + answer, + drawing_holds, + } } /// Calls a widget's `draw` and keeps what it drew in `region`. @@ -284,7 +293,7 @@ impl UiRenderState { info: DrawInfo, old: Option, rsc: &mut dyn UiRsc, - ) -> (Size, LayoutHolds) { + ) -> Answer { let rel_base = info.rel_base; let (move_idx, region, retired_move) = match info.region_node { // A node entry is only a translation. Its local box keeps the @@ -395,9 +404,11 @@ impl UiRenderState { // was offered reports the height it needs. debug_assert!( mask == info.mask - || AXES - .into_iter() - .all(|axis| size.within_box(region, self.output_size, axis)), + || Axis::BOTH.into_iter().all(|axis| size.within_box( + region, + self.output_size, + axis + )), "'{}' ({id:?}) clips to {px:?} and reports {size}", rsc.widgets().label(id), ); @@ -417,7 +428,7 @@ impl UiRenderState { // that many pixels of this window -- the same pin a widget that read // its rel base took for its drawing. let mut own_holds = own; - for axis in AXES { + for axis in Axis::BOTH { let fraction = rules[axis].exact().is_some_and(|len| len.rel != Rel::ZERO); if fraction { own_holds[axis].rel_base = Some(info.rel_base[axis]); @@ -489,7 +500,10 @@ impl UiRenderState { }; rsc.on_draw(&active); self.active.insert(id, active); - (size, answer_holds) + Answer { + size, + holds: answer_holds, + } } /// Keeps a region node's entry across redraws because descendants retain @@ -516,23 +530,17 @@ impl UiRenderState { /// drawing ended up. Alignment is exactly that case: the first box is the /// question and the smaller placed box holds the drawing. Whether the /// answer is stale at all is its caller's question, asked once there. - fn retained_answer( - &self, - id: WidgetId, - region: UiRegion, - info: DrawInfo, - ) -> Option<(Size, LayoutHolds)> { + fn retained_answer(&self, id: WidgetId, region: UiRegion, info: DrawInfo) -> Option { let active = self.active.get(&id)?; - let has_region_node = active.move_idx != active.parent_move; if !active.drawn - || has_region_node != info.region_node + || active.is_region_node() != info.region_node || active.parent_move != info.parent_move { return None; } let answer = active.answer?; answer - .1 + .holds .contains(self.output_size, info.rel_base, region) .then_some(answer) } @@ -546,7 +554,7 @@ impl UiRenderState { placed: UiRegion, info: DrawInfo, rsc: &mut dyn UiRsc, - ) -> Option<()> { + ) -> bool { #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::ReuseAttempts); if rsc.widgets().needs_redraw.contains(&id) { @@ -555,19 +563,20 @@ impl UiRenderState { diag::bump(Counter::ReuseDirty); diag::reuse(id, ReuseOutcome::Dirty); } - return None; + return false; } - let active = self.active.get(&id)?; + let Some(active) = self.active.get(&id) else { + return false; + }; if !active.drawn { #[cfg(feature = "layout-diagnostics")] diag::reuse(id, ReuseOutcome::Undrawn); - return None; + return false; } - let has_region_node = active.move_idx != active.parent_move; - if has_region_node != info.region_node { + if active.is_region_node() != info.region_node { #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::ReuseWrongNode); - return None; + return false; } // Drawn on another layer: the drawing sits in that layer's list and // paints at its moment, which no amount of geometry says. A container @@ -580,10 +589,10 @@ impl UiRenderState { diag::bump(Counter::ReuseWrongLayer); diag::reuse(id, ReuseOutcome::WrongLayer); } - return None; + return false; } if active.parent_mask != info.mask { - return None; + return false; } // Drawn somewhere else in the tree: its box is in coordinates it no // longer sits in, and its slot names the wrong parent. @@ -593,7 +602,7 @@ impl UiRenderState { diag::bump(Counter::ReuseWrongParent); diag::reuse(id, ReuseOutcome::WrongParent); } - return None; + return false; } // In pixels, because the box is a fraction of the window and that // may be what changed -- an unchanged fraction of a window half the @@ -607,7 +616,7 @@ impl UiRenderState { // Which of the three said no, so a rel base that redraws more // than it should says where to look. They overlap: a drawing // can be outside two of them at once. - for axis in AXES { + for axis in Axis::BOTH { let holds = active.holds[axis]; let len = region[axis].len(); let window = self.output_size[axis]; @@ -628,10 +637,10 @@ impl UiRenderState { diag::bump(Counter::ReuseOutside); diag::reuse(id, ReuseOutcome::Outside); } - return None; + return false; } self.relocate(id, placed, info, rsc); - Some(()) + true } /// Puts a retained drawing where its parent now has it, without drawing: @@ -644,14 +653,14 @@ impl UiRenderState { "'{}' ({id:?}) placed while marked to draw", rsc.widgets().label(id) ); - let has_region_node = active.move_idx != active.parent_move; - let local = match has_region_node { + let is_region_node = active.is_region_node(); + let local = match is_region_node { true => placed.at_origin(), false => placed, }; let moved = active.placement != local; let slot = active.move_idx; - if has_region_node { + if is_region_node { self.moves.set(slot, placed.as_translation()); } if moved { @@ -663,23 +672,13 @@ impl UiRenderState { active.placed = info.placed; #[cfg(feature = "layout-diagnostics")] { - match (moved, has_region_node) { - (true, true) => diag::bump(Counter::ReuseMoved), - (true, false) => diag::bump(Counter::ReuseRemapped), - (false, _) => diag::bump(Counter::ReuseExact), - } - diag::reuse( - id, - if moved { - if has_region_node { - ReuseOutcome::Moved - } else { - ReuseOutcome::Remapped - } - } else { - ReuseOutcome::Exact - }, - ); + let (counter, outcome) = match (moved, is_region_node) { + (true, true) => (Counter::ReuseMoved, ReuseOutcome::Moved), + (true, false) => (Counter::ReuseRemapped, ReuseOutcome::Remapped), + (false, _) => (Counter::ReuseExact, ReuseOutcome::Exact), + }; + diag::bump(counter); + diag::reuse(id, outcome); } } @@ -712,7 +711,7 @@ impl UiRenderState { parent: Some(at.id), depth: at.depth + 1, parent_move: at.move_idx, - region_node: active.move_idx != active.parent_move, + region_node: active.is_region_node(), mask: at.mask, rel_base, region, @@ -755,16 +754,18 @@ impl UiRenderState { move_idx: active.move_idx, mask: active.mask, }; - let children = active.children.len(); - for index in 0..children { - let child = self.active[&id].children[index]; + // Taken out and put back so that placing a child can borrow the state + // it needs; nothing on that path reads this widget's own child list. + let children = std::mem::take(&mut self.active.get_mut(&id).unwrap().children); + for &child in &children { self.place_child(child, &at, rsc); } + self.active.get_mut(&id).unwrap().children = children; } - /// A reused subtree keeps its shape, so every widget in it moves by the - /// same amount -- and where the top of it did not move, none of it did, - /// which is what makes this free in the ordinary case. + /// A reused subtree keeps its shape, so each widget in it keeps its depth + /// under the top -- and where the top's own depth did not change, none of + /// them did, which is what makes this free in the ordinary case. fn redepth(&mut self, id: WidgetId, depth: usize) { let Some(active) = self.active.get_mut(&id) else { return; @@ -773,18 +774,21 @@ impl UiRenderState { return; } active.depth = depth; - let children = active.children.len(); - for index in 0..children { - let child = self.active[&id].children[index]; + // Taken out and put back so the walk can borrow the state it needs; + // it only ever goes further down, so it reads no list but its own. + let children = std::mem::take(&mut active.children); + for &child in &children { self.redepth(child, depth + 1); } + self.active.get_mut(&id).unwrap().children = children; } fn hints_agree(id: WidgetId, size: Size, rsc: &dyn UiRsc) -> bool { let Some(widget) = rsc.widgets().get_dyn(id) else { return true; }; - AXES.into_iter() + Axis::BOTH + .into_iter() .all(|axis| widget.size_hint(axis).is_none_or(|hint| hint == size[axis])) } @@ -1118,13 +1122,13 @@ impl UiRenderState { diag::bump(Counter::LocalRedraws); let old = self.remove(id, false, rsc); - let answer = self.draw_inner(id, info, old, rsc); + let drawn = self.draw_inner(id, info, old, rsc); let active = self.active.get_mut(&id).unwrap(); // A wider contract does not invalidate the guarantee the parent kept. // Retain that guarantee so widening and narrowing back do not churn it. - if let Some((size, holds)) = was_answer - && answer.0 == size - && answer.1.covers(holds) + if let Some(was) = was_answer + && drawn.answer.size == was.size + && drawn.answer.holds.covers(was.holds) { active.answer = was_answer; } @@ -1169,11 +1173,11 @@ impl UiRenderState { } } -/// Whether what a widget reports along `axis` is inside the box it drew in. -/// Both are lengths of the window, so the comparison is in its pixels. A -/// share is a length only to whoever divides one, so it is not a claim about -/// this box and cannot exceed it. impl Size { + /// Whether what a widget reports along `axis` is inside the box it drew + /// in. Both are lengths of the window, so the comparison is in its + /// pixels. A share is a length only to whoever divides one, so it is not + /// a claim about this box and cannot exceed it. fn within_box(self, region: UiRegion, window: PxVec2, axis: Axis) -> bool { let len = self[axis]; let window = window[axis]; diff --git a/src/widget/position/scroll.rs b/src/widget/position/scroll.rs index f8973ef..60f14ee 100644 --- a/src/widget/position/scroll.rs +++ b/src/widget/position/scroll.rs @@ -16,9 +16,9 @@ impl Widget for Scroll { let answer_len = painter .widget_at(&self.inner, PlaceDesc::WHOLE.fills()) .len(self.axis); - let fixed = painter.to_px(answer_len.without_leftover(), self.axis); + let answer_px = painter.to_px(answer_len.without_leftover(), self.axis); self.container_len = container_len; - self.content_len = fixed.max(container_len); + self.content_len = answer_px.max(container_len); if self.snap_end { self.amt = self.content_len - self.container_len; @@ -31,10 +31,10 @@ impl Widget for Scroll { // the drawing holds for that length alone. One scrolled part way sits // where it is until the box shrinks past what is left of it. Kept to // the end, it moves with every length. - let fixed_len = answer_len.is_px(); - if fixed_len && self.content_len <= self.container_len && align == AxisAlign::NEG { - painter.holds(self.axis, fixed..=Px::MAX); - } else if fixed_len && !self.snap_end { + let answer_is_px = answer_len.is_px(); + if answer_is_px && self.content_len <= self.container_len && align == AxisAlign::NEG { + painter.holds(self.axis, answer_px..=Px::MAX); + } else if answer_is_px && !self.snap_end { let left = self.content_len - self.amt; painter.holds(self.axis, Px::MIN..=left); } diff --git a/src/widget/position/span.rs b/src/widget/position/span.rs index 06fed6b..9b54abb 100644 --- a/src/widget/position/span.rs +++ b/src/widget/position/span.rs @@ -10,21 +10,18 @@ pub struct Span { impl Widget for Span { fn draw(&mut self, painter: &mut Painter) -> Size { let axis = self.dir.axis; - // The row: this span's own box, as a length of the rel base its children - // are laid out against. Its start is nothing's business -- a slot is - // a length from it -- so what this reads is the length alone. - let far = painter.region_len(axis); - let along = |from: Len, to: Len| match self.dir.sign { - Sign::Pos => UiSpan::new(from, to), - Sign::Neg => UiSpan::new(far - to, far - from), - }; + // The row this span lays its children out along, as a length of the + // rel base they are laid out against. Where it starts is nothing's + // business -- a slot is a length from there -- so what this reads is + // the length alone. + let row = painter.region_len(axis); // A length for every child before their final slots are chosen: from // a hint where one says, and from drawing otherwise. The rel base passes // through unchanged, so `rel(0.5)` is half the area this span was // given whatever else is in it and wherever this child sits among // them; what a drawn child is asked in is the room left from the // cursor, because a text has to wrap at the width actually there. - let mut cursor = Len::rel_min(); + let mut cursor = Len::ZERO; let mut lens = Vec::with_capacity(self.children.len()); for child in &self.children { let len = match painter.size_hint(child, axis) { @@ -33,7 +30,7 @@ impl Widget for Span { // Across itself the child sits where its own alignment // says, in the whole of the row: a span is what contains // its children there, and nothing divides that axis. - let room = along(cursor, far).shifted_desc().on_axis(axis); + let room = self.slot(row, cursor, row).shifted_desc().on_axis(axis); painter.widget_at(child, room).len(axis) } }; @@ -55,7 +52,7 @@ impl Widget for Span { // What is left for the shares to divide: the row less everything // fixed, as a length of the rel base rather than a number of pixels. - let room = far - total.without_leftover(); + let room = row - total.without_leftover(); // Whether anything is left over is a question in pixels: `rel(0.5)` // beside 300 px is full at 600 and overfull at 400. Asked of `room` // itself, and answered back through the same expression, so the @@ -65,10 +62,10 @@ impl Widget for Span { // sign of `room.rel`, which `through` already reads. What the // generated oracle checks is the consequence, since which children // exist at all turns on this. - let mut shares = false; - if total.leftover > Weight::ZERO { - shares = painter.to_px(room, axis) > Px::ZERO; - let holds = match shares { + let any_leftover = total.leftover > Weight::ZERO; + let has_room = any_leftover && painter.to_px(room, axis) > Px::ZERO; + if any_leftover { + let holds = match has_room { true => Holds::from(Px::STEP..=Px::MAX), false => Holds::from(Px::MIN..=Px::ZERO), }; @@ -87,24 +84,26 @@ impl Widget for Span { // than stepped from the last child: the share of the room is // rounded, and taking each end from the one before it would carry // every rounding along the row. - let mut fixed = Len::rel_min(); + let mut fixed = Len::ZERO; let mut taken = Weight::ZERO; let mut ortho = LayoutLen::ZERO; - let reached = |fixed: Len, taken: Weight| match taken == Weight::ZERO { - true => fixed, - false => fixed + room.scale(Rel::ratio(taken, total.leftover)), + // Nothing divides the room where no child asked for any of it, and a + // ratio of a whole of nothing has no answer. + let reached = |fixed: Len, taken: Weight| match any_leftover { + false => fixed, + true => fixed + room.scale(Rel::ratio(taken, total.leftover)), }; for (child, &len) in self.children.iter().zip(&lens) { // A child asking for nothing but a part of what is left over, // when nothing is, is not drawn at all. One that also asked for // pixels or a fraction keeps those and overflows. - if len.is_only_leftover() && !shares { + if len.is_only_leftover() && !has_room { painter.undraw(child); fixed.px += self.gap; continue; } let from = reached(fixed, taken); - if len.leftover > Weight::ZERO && shares { + if len.leftover > Weight::ZERO && has_room { taken += len.leftover; } fixed += len.without_leftover(); @@ -116,9 +115,9 @@ impl Widget for Span { // it, since a text wraps at the width it is actually given. A // fixed child's slot is its own answer, so a drawing made in the // room is put there as it is, and one not made yet is made here. - let slot = along(from, to); + let slot = self.slot(row, from, to); let mut place = slot.shifted_desc().fills().on_axis(axis); - if len.leftover > Weight::ZERO && shares { + if len.leftover > Weight::ZERO && has_room { place = place.rel_base(axis, slot.len()); } let used = painter.place_at(child, place).len(!axis); @@ -152,6 +151,17 @@ impl Widget for Span { } impl Span { + /// The stretch of the row between two distances from where this span + /// starts laying children out, as a span of its own box. A negative + /// direction lays out from the far end, so the same two distances mirror + /// in a row `row` long. + fn slot(&self, row: Len, from: Len, to: Len) -> UiSpan { + match self.dir.sign { + Sign::Pos => from.to(to), + Sign::Neg => (row - to).to(row - from), + } + } + pub fn empty(dir: Dir) -> Self { Self { children: Vec::new(), diff --git a/tests/cases/retained.rs b/tests/cases/retained.rs index 019a0fb..059aa0d 100644 --- a/tests/cases/retained.rs +++ b/tests/cases/retained.rs @@ -125,7 +125,7 @@ fn moving_an_ordinary_subtree_remaps_its_mask() { let active = &h.render.active[&masked.id()]; assert_eq!( h.rsc.ui().masks[active.mask.idx()].region, - UiRegion::new(UiSpan::new(Len::px(150.0), Len::rel_max()), UiSpan::FULL,) + UiRegion::new(UiSpan::new(Len::px(150.0), Len::FULL), UiSpan::FULL,) ); assert_corners!(h, inner, (150, 0), (400, 200)); }