From d98969158fc21b11d8936a52b334814bd058cf3a Mon Sep 17 00:00:00 2001 From: iris-ai <4+iris-ai@noreply.localhost> Date: Mon, 14 Sep 2026 12:25:37 -0400 Subject: [PATCH] Give a slot to the children a container places, and nothing else A widget's region is now held in the coordinates of the slot it draws in rather than the window's, and `Painter::place` is how a container asks for a slot: it draws a child it decides the box of and may decide again. Everything under that slot is a fraction of its box, so placing the child a second time is one entry to write whether it moved or changed length. A child drawn any other way has no slot and shares its nearest ancestor's. That is what keeps the chain short. `chain_cost` measured depth as the cost -- free to 8, +42.6% at 16 -- and a slot per widget put a transcript's glyphs past that for nothing, since almost every slot was zero. `Span`, `Aligned` and `Scroll` are the containers that re-place a child after drawing it, and `tests/layout.rs` pins that four widgets between a span and a leaf leave the leaf's chain one deep. `UiRegion::stretch`, `UiRegion::stretchable` and `UiScalar::stretch` are gone. Nothing is inverted any more: a box that changed length is written to its slot, and the descendants recompose against it in the shader. That also retires the case the guard existed for, where a fixed length has no fraction to recover -- `tests/layout.rs` now stretches a 40-tall row on its other axis, which `stretchable` refused outright. What still walks the CPU is deciding who must draw again, which no chain can answer: `mark_resized` descends from the widget whose box changed and marks anything whose own box changed length and whose drawing reads it. A part of a box with no relative extent on an axis is a fixed length, and composing into it leaves none either, so the walk stops where a length did not change -- an 80-wide child in a widened row is not redrawn though it says `Redraw`. `Span`, `Pad`, `Stack`, `Offset`, `Aligned`, `SetSize` and `LayerOffset` say `Scale`: each places in fractions and offsets of its own box and none reads the box's pixel length. `Scroll` and `MaxSize` do read pixels and stay `Redraw`. 45 tests pass, five of them new. Render verification comes after the CPU side, per the owner. Co-Authored-By: Claude Opus 5 --- core/src/orientation/pos.rs | 35 ------ core/src/render/data.rs | 7 +- core/src/ui/active.rs | 5 +- core/src/ui/mod.rs | 26 ++++- core/src/ui/painter.rs | 39 +++++-- core/src/ui/render_state.rs | 198 ++++++++++++++++++++------------ src/widget/position/align.rs | 10 +- src/widget/position/layer.rs | 4 + src/widget/position/offset.rs | 4 + src/widget/position/pad.rs | 6 + src/widget/position/scroll.rs | 6 +- src/widget/position/set_size.rs | 4 + src/widget/position/span.rs | 10 +- src/widget/position/stack.rs | 4 + tests/chain_cost.rs | 2 +- tests/layout.rs | 56 +++++++++ tests/retained.rs | 51 +++++++- tests/stretch.rs | 46 -------- 18 files changed, 332 insertions(+), 181 deletions(-) delete mode 100644 tests/stretch.rs diff --git a/core/src/orientation/pos.rs b/core/src/orientation/pos.rs index bd96082..d2fc8f4 100644 --- a/core/src/orientation/pos.rs +++ b/core/src/orientation/pos.rs @@ -202,17 +202,6 @@ impl UiScalar { } } - /// `within` undone against `from` and redone against `to`, for one axis. - /// `from`'s relative extent is the denominator, so it must not be zero. - fn stretch(&self, from: &UiSpan, to: &UiSpan) -> Self { - let frac = (self.rel - from.start.rel) / (from.end.rel - from.start.rel); - Self { - rel: frac.lerp(to.start.rel, to.end.rel), - abs: self.abs - frac.lerp(from.start.abs, from.end.abs) - + frac.lerp(to.start.abs, to.end.abs), - } - } - pub fn within_len(&self, len: UiScalar) -> Self { self.within(&UiSpan { start: UiScalar::ZERO, @@ -391,30 +380,6 @@ impl UiRegion { }, } } - - /// Whether a stretch out of this box can be expressed. Each part inside a - /// box is held as a fraction of it, and a fixed length has no fraction to - /// hold one by -- every part of it is just an offset from its start. - pub fn stretchable(&self) -> bool { - self.x.start.rel != self.x.end.rel && self.y.start.rel != self.y.end.rel - } - - /// Re-expresses a region inside `from` as the same fractions of `to`. - /// `from` must be `stretchable`; a translation is `shift` instead, which - /// needs no fractions and works out of any box. - pub fn stretch(&self, from: &UiRegion, to: &UiRegion) -> UiRegion { - debug_assert!(from.stretchable(), "a fixed length has no fraction"); - UiRegion { - x: UiSpan { - start: self.x.start.stretch(&from.x, &to.x), - end: self.x.end.stretch(&from.x, &to.x), - }, - y: UiSpan { - start: self.y.start.stretch(&from.y, &to.y), - end: self.y.end.stretch(&from.y, &to.y), - }, - } - } } impl Display for UiRegion { diff --git a/core/src/render/data.rs b/core/src/render/data.rs index 517b694..16a2747 100644 --- a/core/src/render/data.rs +++ b/core/src/render/data.rs @@ -82,10 +82,7 @@ unsafe impl bytemuck::Pod for MoveOffset {} unsafe impl bytemuck::Zeroable for MoveOffset {} impl MoveOffset { - pub fn root(parent: MoveIdx) -> Self { - Self { - region: UiRegion::FULL, - parent, - } + pub fn new(parent: MoveIdx, region: UiRegion) -> Self { + Self { region, parent } } } diff --git a/core/src/ui/active.rs b/core/src/ui/active.rs index 6af43f3..2391a7b 100644 --- a/core/src/ui/active.rs +++ b/core/src/ui/active.rs @@ -15,8 +15,11 @@ pub struct ActiveData { pub size_deps: Vec, /// Whether it read the output's size, and so is wrong when that changes. pub reads_output: bool, - /// The move slot its primitives are positioned through. + /// The slot its primitives are positioned through: its own if its parent + /// placed it, otherwise the nearest ancestor that has one. pub move_idx: MoveIdx, + /// The slot `region` is given in, which is whatever its parent drew in. + pub parent_move: MoveIdx, pub mask: MaskIdx, pub layer: LayerId, } diff --git a/core/src/ui/mod.rs b/core/src/ui/mod.rs index 881dad5..879ae9e 100644 --- a/core/src/ui/mod.rs +++ b/core/src/ui/mod.rs @@ -36,9 +36,19 @@ pub struct Moves { } impl Moves { - pub fn push(&mut self, parent: MoveIdx) -> MoveIdx { + pub fn push(&mut self, parent: MoveIdx, region: UiRegion) -> MoveIdx { self.changed = true; - MoveIdx::slot(self.arena.push(MoveOffset::root(parent)).idx()) + MoveIdx::slot(self.arena.push(MoveOffset::new(parent, region)).idx()) + } + + /// Re-points a slot at a different parent, for a widget drawn somewhere + /// else in the tree than it was. + pub fn set_parent(&mut self, idx: MoveIdx, parent: MoveIdx) { + let entry = self.arena.get_mut(Id::preset(idx.idx() as u32)); + if entry.parent != parent { + entry.parent = parent; + self.changed = true; + } } pub fn remove(&mut self, idx: MoveIdx) { @@ -77,6 +87,18 @@ impl Moves { region } + /// How many slots a region in `idx` is composed through, which is what + /// the shader's walk costs per primitive. + pub fn depth(&self, idx: MoveIdx) -> usize { + let mut depth = 0; + let mut at = idx; + while at != MoveIdx::NONE && depth < CHAIN_LIMIT as usize { + at = self.arena[at.idx()].parent; + depth += 1; + } + depth + } + pub fn entries(&self) -> &[MoveOffset] { &self.arena } diff --git a/core/src/ui/painter.rs b/core/src/ui/painter.rs index d2258fe..e5098ce 100644 --- a/core/src/ui/painter.rs +++ b/core/src/ui/painter.rs @@ -13,6 +13,7 @@ pub struct Painter<'a> { pub(super) state: &'a mut UiRenderState, pub(super) rsc: &'a mut dyn UiRsc, + /// This widget's box, in the coordinates of `move_idx`. pub(super) region: UiRegion, pub(super) mask: MaskIdx, pub(super) textures: Vec, @@ -21,7 +22,8 @@ pub struct Painter<'a> { /// The children whose size this widget read while drawing. pub(super) size_deps: Vec, pub(super) reads_output: bool, - /// The move slot this widget's primitives are positioned through. + /// The slot this widget's primitives are positioned through: its own if + /// its parent placed it, otherwise the nearest ancestor that has one. pub(super) move_idx: MoveIdx, pub layer: usize, pub(super) id: WidgetId, @@ -78,24 +80,39 @@ impl<'a> Painter<'a> { /// Draws a widget within this widget's region. pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget) -> DrawResult<'s, 'a, W> { - self.widget_at(id, self.region) + self.widget_at(id, self.region, false) } - /// Draws a widget somewhere within this one. Drawing one a second time - /// gives it a new box, keeping the drawing it already has where it can. + /// Draws a widget somewhere within this one. pub fn widget_within<'s, W: ?Sized>( &'s mut self, id: &'s StrongWidget, region: UiRegion, ) -> DrawResult<'s, 'a, W> { let region = region.within(&self.region); - self.widget_at(id, region) + self.widget_at(id, region, false) + } + + /// Draws a child this widget decides the box of, and may decide again + /// once it knows what the child came to. The child gets a slot of its + /// own, so placing it a second time writes one entry however much it + /// drew -- moved or resized alike, since everything under the slot is + /// held as a fraction of its box. A child drawn any other way has no slot + /// and can only be given a different box by drawing again. + pub fn place<'s, W: ?Sized>( + &'s mut self, + id: &'s StrongWidget, + region: UiRegion, + ) -> DrawResult<'s, 'a, W> { + let region = region.within(&self.region); + self.widget_at(id, region, true) } fn widget_at<'s, W: ?Sized>( &'s mut self, id: &'s StrongWidget, region: UiRegion, + slotted: bool, ) -> DrawResult<'s, 'a, W> { // A child listed twice would be moved twice. if !self.children.contains(&id.id()) { @@ -106,6 +123,8 @@ impl<'a> Painter<'a> { id.id(), region, Some(self.id), + self.move_idx, + slotted, self.mask, None, self.rsc, @@ -165,6 +184,8 @@ impl<'a> Painter<'a> { } } + /// This widget's box, in the coordinates its own primitives are written + /// in -- so a region composed `within` it may be drawn directly. pub fn region(&self) -> UiRegion { self.region } @@ -176,11 +197,13 @@ impl<'a> Painter<'a> { self.state.output_size } - /// This widget's box in pixels. Resolved against the output's size, so a - /// widget that reads it draws again when the output changes. + /// This widget's box in pixels. Resolved against the output's size and + /// the boxes it sits within, so a widget that reads it draws again when + /// the output changes. pub fn px_size(&mut self) -> Vec2 { self.reads_output = true; - self.region.size().to_abs(self.state.output_size) + let region = self.state.moves.resolve(self.move_idx, self.region); + region.size().to_abs(self.state.output_size) } pub fn text_data(&mut self) -> &mut TextData { diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index 6d0e2b3..149df73 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -4,6 +4,8 @@ use crate::{ util::{HashMap, HashSet, Vec2, forget_ref}, }; +const AXES: [Axis; 2] = [Axis::X, Axis::Y]; + pub struct UiRenderState { pub active: HashMap, pub layers: DrawLayers, @@ -82,7 +84,17 @@ impl UiRenderState { self.clear(rsc); // free all resources & cache if let Some(id) = root { - self.draw_inner(0, id.id(), UiRegion::FULL, None, MaskIdx::NONE, None, rsc); + self.draw_inner( + 0, + id.id(), + UiRegion::FULL, + None, + MoveIdx::NONE, + false, + MaskIdx::NONE, + None, + rsc, + ); } } @@ -94,13 +106,15 @@ impl UiRenderState { id: WidgetId, region: UiRegion, parent: Option, + parent_move: MoveIdx, + slotted: bool, mask: MaskIdx, old_children: Option>, rsc: &mut dyn UiRsc, ) -> Size { let mut old_children = old_children.unwrap_or_default(); if self.active.contains_key(&id) { - if let Some(size) = self.try_reuse(id, region, rsc) { + if let Some(size) = self.try_reuse(id, region, parent_move, rsc) { return size; } // if not, then maintain resize and track old children to remove unneeded @@ -109,14 +123,21 @@ impl UiRenderState { } // draw widget - let move_idx = self.move_slot(id, parent); - self.moves.set(move_idx, UiRegion::FULL); + let (move_idx, local) = match slotted { + // Its box becomes its slot's, so it draws in the slot's own + // coordinates and the box it was given is one entry to rewrite. + true => (self.move_slot(id, parent_move, region), UiRegion::FULL), + false => { + self.drop_slot(id); + (parent_move, region) + } + }; rsc.widgets_mut().needs_redraw.remove(&id); self.draw_started.insert(id); let mut painter = Painter { state: self, - region, + region: local, mask, layer, id, @@ -136,7 +157,7 @@ impl UiRenderState { let Painter { state: _, rsc: _, - region, + region: _, mask, textures, primitives, @@ -166,6 +187,7 @@ impl UiRenderState { size_deps, reads_output, move_idx, + parent_move, mask, layer, }; @@ -182,101 +204,133 @@ impl UiRenderState { size } - /// The slot a widget's drawing is positioned through, made on its first - /// draw and kept until it stops being drawn. - fn move_slot(&mut self, id: WidgetId, parent: Option) -> MoveIdx { + /// The slot a widget's box is held in, made on its first placed draw and + /// kept until it stops being drawn -- a redraw replaces its `ActiveData` + /// while descendants go on naming the slot. + fn move_slot(&mut self, id: WidgetId, parent: MoveIdx, region: UiRegion) -> MoveIdx { if let Some(&idx) = self.slots.get(&id) { + self.moves.set_parent(idx, parent); + self.moves.set(idx, region); return idx; } - let parent = parent - .and_then(|p| self.slots.get(&p).copied()) - .unwrap_or(MoveIdx::NONE); - let idx = self.moves.push(parent); + let idx = self.moves.push(parent, region); self.slots.insert(id, idx); idx } + /// Gives up a slot a widget no longer needs, because it is drawn somewhere + /// that does not place it. Its descendants name it, so this is only + /// reached where they are about to be drawn again. + fn drop_slot(&mut self, id: WidgetId) { + if let Some(idx) = self.slots.remove(&id) { + self.moves.remove(idx); + } + } + /// The drawing a widget already has, kept for a new box if the box has not /// changed in a way it depends on. - fn try_reuse(&mut self, id: WidgetId, region: UiRegion, rsc: &dyn UiRsc) -> Option { + fn try_reuse( + &mut self, + id: WidgetId, + region: UiRegion, + parent_move: MoveIdx, + rsc: &mut dyn UiRsc, + ) -> Option { if rsc.widgets().needs_redraw.contains(&id) { return None; } let active = self.active.get(&id)?; - let (size, old, slot) = (active.size, active.region, active.move_idx); - // TODO: epsilon? - if old.size() == region.size() { - // The right shape and only somewhere else, which is one slot to - // write however much is under it. Both boxes are in the - // coordinates its parent drew, so the chain above applies alike. - let moved = - region.to_px(self.output_size).top_left - old.to_px(self.output_size).top_left; - self.moves.set(slot, UiRegion::FULL.offset(moved)); - return Some(size); - } - if !self.reusable(id, region, rsc) || !old.stretchable() { + // Drawn somewhere else in the tree: its box is in coordinates it no + // longer sits in, and its slot names the wrong parent. + if active.parent_move != parent_move { return None; } - // Its drawing stands, re-expressed as the same fractions of the box. - self.stretch(id, old, region); + let (size, old, slot) = (active.size, active.region, active.move_idx); + if old == region { + return Some(size); + } + // Only a placed widget can be given a different box without drawing + // again: everything it drew is a fraction of its slot's box, so one + // entry says where all of it went. + if slot == parent_move { + return None; + } + let mut changed = [false; 2]; + for (axis, c) in AXES.into_iter().zip(changed.iter_mut()) { + *c = region.axis(axis).len() != old.axis(axis).len(); + } + if changed.iter().any(|&c| c) { + let widget = rsc.widgets().get_dyn(id)?; + let redraws = AXES + .into_iter() + .zip(changed) + .any(|(axis, c)| c && widget.on_resize(axis) != OnResize::Scale); + if redraws { + return None; + } + } + self.moves.set(slot, region); + self.active.get_mut(&id).unwrap().region = region; + if changed.iter().any(|&c| c) { + self.mark_resized(id, changed, rsc); + } Some(size) } - /// Whether the widget can keep the drawing it has and be given `region` - /// instead, asked one axis at a time: a change on an axis it does not - /// depend on costs nothing, whatever it depends on elsewhere. - fn reusable(&self, id: WidgetId, region: UiRegion, rsc: &dyn UiRsc) -> bool { + /// Marks every descendant whose drawing cannot survive the box it is a + /// fraction of changing length, `changed` saying which axes of that box + /// did. + /// + /// A part of a box with no relative extent on an axis is a fixed length, + /// held as offsets from that box's start, and composing anything into it + /// leaves no relative extent either. So a widget whose own box did not + /// change length has no descendant whose box did, and the walk stops + /// there. + fn mark_resized(&mut self, id: WidgetId, changed: [bool; 2], rsc: &mut dyn UiRsc) { let Some(active) = self.active.get(&id) else { - return false; + return; }; - let Some(widget) = rsc.widgets().get_dyn(id) else { - return false; - }; - [Axis::X, Axis::Y].into_iter().all(|axis| { - let offered = region.axis(axis).len(); - let had = active.region.axis(axis).len(); - match widget.on_resize(axis) { - OnResize::Scale => true, - // `Translate` is not acted on yet, and cannot be until a - // drawing can sit somewhere other than its box. `region` is - // both the box a widget was given and the box its primitives - // are in, and `mov` remaps from it -- so carrying a drawing at - // its old size while the box grows makes the next move stretch - // it. The offset chain is what separates the two. - OnResize::Translate | OnResize::Redraw => offered == had, + // SAFETY: children cannot be recursive + let children = unsafe { forget_ref(&active.children) }; + for &child in children { + let Some(data) = self.active.get(&child) else { + continue; + }; + let region = data.region; + let mut own = changed; + for (axis, c) in AXES.into_iter().zip(own.iter_mut()) { + *c &= region.axis(axis).len().rel != 0.0; } - }) + if !own.iter().any(|&c| c) { + continue; + } + let redraws = match rsc.widgets().get_dyn(child) { + Some(widget) => AXES + .into_iter() + .zip(own) + .any(|(axis, c)| c && widget.on_resize(axis) != OnResize::Scale), + None => true, + }; + match redraws { + true => { + rsc.widgets_mut().needs_redraw.insert(child); + } + false => self.mark_resized(child, own, rsc), + } + } } fn hints_agree(id: WidgetId, size: Size, rsc: &dyn UiRsc) -> bool { let Some(widget) = rsc.widgets().get_dyn(id) else { return true; }; - [Axis::X, Axis::Y].into_iter().all(|axis| { + AXES.into_iter().all(|axis| { widget .size_hint(axis) .is_none_or(|hint| hint == size.axis(axis)) }) } - /// Rewrites a subtree's regions as the same fractions of a new box, for a - /// change of length that a slot cannot express. Every region it rewrites - /// is a region some slot was a delta from, so those go back to zero. - fn stretch(&mut self, id: WidgetId, from: UiRegion, to: UiRegion) { - let active = self.active.get_mut(&id).unwrap(); - for h in &active.primitives { - let region = self.layers[h.layer].region_mut(h); - *region = region.stretch(&from, &to); - } - active.region = active.region.stretch(&from, &to); - self.moves.set(active.move_idx, UiRegion::FULL); - // SAFETY: children cannot be recursive - let children = unsafe { forget_ref(&active.children) }; - for child in children { - self.stretch(*child, from, to); - } - } - /// NOTE: instance textures are cleared and self.textures freed fn remove(&mut self, id: WidgetId, undraw: bool, rsc: &mut dyn UiRsc) -> Option { let mut active = self.active.remove(&id); @@ -363,11 +417,11 @@ impl UiRenderState { } } - /// Where a widget is on screen: the box it drew against plus whatever the - /// chain above has moved since, which is the walk the vertex shader does. + /// Where a widget is on screen: its box composed through the boxes it + /// sits within, which is the walk the vertex shader does. pub fn window_region(&self, id: &impl IdLike) -> Option { let active = self.active.get(&id.id())?; - let region = self.moves.resolve(active.move_idx, active.region); + let region = self.moves.resolve(active.parent_move, active.region); Some(region.to_px(self.output_size)) } @@ -400,6 +454,8 @@ impl UiRenderState { id, active.region, active.parent, + active.parent_move, + active.move_idx != active.parent_move, active.mask, Some(active.children), rsc, diff --git a/src/widget/position/align.rs b/src/widget/position/align.rs index d63ec30..17d15c9 100644 --- a/src/widget/position/align.rs +++ b/src/widget/position/align.rs @@ -9,14 +9,20 @@ impl Widget for Aligned { fn draw(&mut self, painter: &mut Painter) -> Size { // Drawn where it may be too big, then given its aligned box once its // size is known. - let size = painter.widget(&self.inner).size(); + let size = painter.place(&self.inner, UiRegion::FULL).size(); let region = match self.align.tuple() { (Some(x), Some(y)) => size.to_uivec2().align(RegionAlign { x, y }), (Some(x), None) => UiRegion::new(size.x.apply_rest().align(x), UiSpan::FULL), (None, Some(y)) => UiRegion::new(UiSpan::FULL, size.y.apply_rest().align(y)), (None, None) => UiRegion::FULL, }; - painter.widget_within(&self.inner, region); + painter.place(&self.inner, region); size } + + /// The aligned box is a fraction of its own, so the child keeps its + /// length and stays against the edge it was aligned to. + fn on_resize(&self, _: Axis) -> OnResize { + OnResize::Scale + } } diff --git a/src/widget/position/layer.rs b/src/widget/position/layer.rs index 6b111a4..c3d2b01 100644 --- a/src/widget/position/layer.rs +++ b/src/widget/position/layer.rs @@ -12,4 +12,8 @@ impl Widget for LayerOffset { } painter.widget(&self.inner).size() } + + fn on_resize(&self, _: Axis) -> OnResize { + OnResize::Scale + } } diff --git a/src/widget/position/offset.rs b/src/widget/position/offset.rs index 5f490b6..4b5b9cb 100644 --- a/src/widget/position/offset.rs +++ b/src/widget/position/offset.rs @@ -10,4 +10,8 @@ impl Widget for Offset { let region = UiRegion::FULL.offset(self.amt); painter.widget_within(&self.inner, region).size() } + + fn on_resize(&self, _: Axis) -> OnResize { + OnResize::Scale + } } diff --git a/src/widget/position/pad.rs b/src/widget/position/pad.rs index 7dbd6a7..6894421 100644 --- a/src/widget/position/pad.rs +++ b/src/widget/position/pad.rs @@ -21,6 +21,12 @@ impl Widget for Pad { }, } } + + /// The padding is an offset from each edge, so a longer box pads the same + /// amount and the child takes the rest. + fn on_resize(&self, _: Axis) -> OnResize { + OnResize::Scale + } } pub struct Padding { diff --git a/src/widget/position/scroll.rs b/src/widget/position/scroll.rs index 41e0801..5b7cd6f 100644 --- a/src/widget/position/scroll.rs +++ b/src/widget/position/scroll.rs @@ -12,10 +12,10 @@ pub struct Scroll { impl Widget for Scroll { fn draw(&mut self, painter: &mut Painter) -> Size { let output_len = painter.output_size().axis(self.axis); - let container_len = painter.region().axis(self.axis).len(); + let container_len = UiScalar::abs(painter.px_size().axis(self.axis)); // Drawn in the whole container to learn its length, then placed at // the scrolled offset. - let child = painter.widget(&self.inner).size(); + let child = painter.place(&self.inner, UiRegion::FULL).size(); let content_len = child .axis(self.axis) .apply_rest() @@ -31,7 +31,7 @@ impl Widget for Scroll { let mut region = UiRegion::FULL.offset(Vec2::from_axis(self.axis, -self.amt, 0.0)); region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len); - painter.widget_within(&self.inner, region); + painter.place(&self.inner, region); child } } diff --git a/src/widget/position/set_size.rs b/src/widget/position/set_size.rs index 2632198..ffb4e52 100644 --- a/src/widget/position/set_size.rs +++ b/src/widget/position/set_size.rs @@ -23,4 +23,8 @@ impl Widget for SetSize { Axis::Y => self.y, } } + + fn on_resize(&self, _: Axis) -> OnResize { + OnResize::Scale + } } diff --git a/src/widget/position/span.rs b/src/widget/position/span.rs index 0e7c724..94346e5 100644 --- a/src/widget/position/span.rs +++ b/src/widget/position/span.rs @@ -17,7 +17,7 @@ impl Widget for Span { .iter() .map(|child| match painter.size_hint(child, axis) { Some(len) => len, - None => painter.widget(child).len(axis), + None => painter.place(child, UiRegion::FULL).len(axis), }) .collect(); @@ -42,7 +42,7 @@ impl Widget for Span { if self.dir.sign == Sign::Neg { region.flip(axis); } - let used = painter.widget_within(child, region).size().axis(!axis); + let used = painter.place(child, region).size().axis(!axis); // TODO: rel shouldn't do this, but no easy way before actually calculating pixels if used.rel > 0.0 || used.rest > 0.0 { ortho = Len::REST; @@ -58,6 +58,12 @@ impl Widget for Span { }; Size::from_axis(axis, along, ortho) } + + /// Every child is placed in fractions and offsets of the span's own box, + /// so a longer box holds the same layout and the children follow it. + fn on_resize(&self, _: Axis) -> OnResize { + OnResize::Scale + } } impl Span { diff --git a/src/widget/position/stack.rs b/src/widget/position/stack.rs index 36d1db3..a9beead 100644 --- a/src/widget/position/stack.rs +++ b/src/widget/position/stack.rs @@ -28,6 +28,10 @@ impl Widget for Stack { } size } + + fn on_resize(&self, _: Axis) -> OnResize { + OnResize::Scale + } } #[derive(Default, Debug)] diff --git a/tests/chain_cost.rs b/tests/chain_cost.rs index 6d6468d..fc05aa3 100644 --- a/tests/chain_cost.rs +++ b/tests/chain_cost.rs @@ -77,7 +77,7 @@ fn fill(ui: &mut UiData, render: &mut UiRenderState, depth: usize) { let mut slot = MoveIdx::NONE; for _ in 0..depth { - slot = render.moves.push(slot); + slot = render.moves.push(slot, UiRegion::FULL); } let px = |v: f32| UiScalar { rel: 0.0, abs: v }; diff --git a/tests/layout.rs b/tests/layout.rs index dddb0f1..8ae1a10 100644 --- a/tests/layout.rs +++ b/tests/layout.rs @@ -126,3 +126,59 @@ fn a_moved_subtree_takes_its_children_with_it() { // `inner`'s own region was never rewritten. assert_corners!(h, inner, (10, 90), (390, 110)); } + +#[test] +fn a_fixed_length_child_keeps_it_when_the_box_around_it_grows() { + let mut h = Harness::new((400, 200)); + let fixed = rect(Color::BLUE).width(50).add(&mut h.rsc); + let rest = rect(Color::GREEN).add(&mut h.rsc); + let panel = (fixed, rest).span(Dir::RIGHT).add(&mut h.rsc); + // Changing the bar's width is the only thing that changes the box the + // panel and everything under it was drawn for. + let bar = rect(Color::RED).width(100).add(&mut h.rsc); + h.set_root((bar, panel).span(Dir::RIGHT)); + assert_corners!(h, fixed, (100, 0), (150, 200)); + assert_corners!(h, rest, (150, 0), (400, 200)); + + h.rsc[bar].x = Some(Len::abs(200)); + h.frame(); + + // The panel's box is 100 shorter, so the fixed child is the same 50 wide + // against its new start and the one taking the rest absorbs the change. + assert_corners!(h, fixed, (200, 0), (250, 200)); + assert_corners!(h, rest, (250, 0), (400, 200)); +} + +#[test] +fn a_box_with_a_fixed_length_can_be_stretched_on_its_other_axis() { + let mut h = Harness::new((400, 200)); + // The row is 40 tall whatever happens, which used to make its drawing + // impossible to take out of: recovering a fraction of a box needs a + // relative extent, and it has none on that axis. + let inner = rect(Color::BLUE).add(&mut h.rsc); + let row = inner.pad(10).height(40).add(&mut h.rsc); + let filler = rect(Color::GREEN).add(&mut h.rsc); + let column = (row, filler).span(Dir::DOWN).add(&mut h.rsc); + let bar = rect(Color::RED).width(100).add(&mut h.rsc); + h.set_root((bar, column).span(Dir::RIGHT)); + assert_corners!(h, inner, (110, 10), (390, 30)); + + h.rsc[bar].x = Some(Len::abs(200)); + h.frame(); + + assert_corners!(h, inner, (210, 10), (390, 30)); +} + +#[test] +fn only_a_container_that_places_its_children_lengthens_the_chain() { + let mut h = Harness::new((400, 200)); + let leaf = rect(Color::BLUE).add(&mut h.rsc); + // Four widgets between the span and the leaf, none of which places what + // it draws, so all of them share the span's slot. + let buried = leaf.pad(4).pad(4).pad(4).pad(4).add(&mut h.rsc); + let bar = rect(Color::RED).width(100).add(&mut h.rsc); + h.set_root((bar, buried).span(Dir::RIGHT)); + + let slot = h.render.active[&leaf.id()].parent_move; + assert_eq!(h.render.moves.depth(slot), 1, "one span above the leaf"); +} diff --git a/tests/retained.rs b/tests/retained.rs index 7cedb57..7beceaf 100644 --- a/tests/retained.rs +++ b/tests/retained.rs @@ -251,9 +251,8 @@ fn a_change_two_levels_under_its_reader_still_reaches_it() { assert_corners!(h, below, (12, 232), (388, 388)); } -/// Claims its drawing may be stretched, and has a child so that the stretch -/// has to reach one. No shipped container claims `Scale` -- `Rect`, `Image` -/// and `()` are all childless -- so nothing else walks a subtree to remap it. +/// Claims its drawing survives its box changing length, and has a child so +/// that the walk looking for what does not has one to reach. struct Stretchy { inner: StrongWidget, draws: Rc>, @@ -271,7 +270,7 @@ impl Widget for Stretchy { } #[test] -fn stretching_a_subtree_remaps_the_children_in_it() { +fn stretching_a_subtree_carries_the_children_in_it() { let mut h = Harness::new((400, 400)); let first = rect(Color::RED).height(40).add(&mut h.rsc); let inner = rect(Color::BLUE).add(&mut h.rsc); @@ -291,8 +290,50 @@ fn stretching_a_subtree_remaps_the_children_in_it() { assert_eq!( draws.get(), settled, - "its drawing is stretched, not redrawn" + "its drawing follows its box, rather than being made again" ); assert_corners!(h, outer, (0, 80), (400, 400)); assert_corners!(h, inner, (0, 80), (400, 400)); } + +#[test] +fn a_widened_row_redraws_what_reads_its_length_and_nothing_else() { + let mut h = Harness::new((400, 200)); + // What a transcript row is: something whose shaping depends on the width + // it is given, beside something that only has to be the right shape. + let (wraps, wrap_draws) = counted(&mut h, Size::REST, OnResize::Redraw); + let (backing, back_draws) = counted(&mut h, Size::REST, OnResize::Scale); + let row = (backing, wraps).span(Dir::RIGHT).add(&mut h.rsc); + let bar = rect(Color::RED).width(100).add(&mut h.rsc); + h.set_root((bar, row).span(Dir::RIGHT)); + let (settled_wrap, settled_back) = (wrap_draws.get(), back_draws.get()); + + h.rsc[bar].x = Some(Len::abs(200)); + h.frame(); + + // The span reads every child's size, so redrawing one takes the span + // with it -- and the span then measures and places the redrawn child. + assert!(wrap_draws.get() > settled_wrap, "reads the width it got"); + assert_eq!(back_draws.get(), settled_back, "only has to be the shape"); + assert_corners!(h, backing, (200, 0), (300, 200)); + assert_corners!(h, wraps, (300, 0), (400, 200)); +} + +#[test] +fn a_fixed_length_child_is_not_redrawn_when_the_box_around_it_grows() { + let mut h = Harness::new((400, 200)); + // It would be drawn again for a width it does not have: its own box is + // a fixed 80 wherever the row's edges end up. + let (fixed, draws) = counted(&mut h, Size::from((80, 200)), OnResize::Redraw); + let (rest, _) = counted(&mut h, Size::REST, OnResize::Scale); + let row = (fixed, rest).span(Dir::RIGHT).add(&mut h.rsc); + let bar = rect(Color::RED).width(100).add(&mut h.rsc); + h.set_root((bar, row).span(Dir::RIGHT)); + let settled = draws.get(); + + h.rsc[bar].x = Some(Len::abs(200)); + h.frame(); + + assert_eq!(draws.get(), settled, "its own length did not change"); + assert_corners!(h, fixed, (200, 0), (280, 200)); +} diff --git a/tests/stretch.rs b/tests/stretch.rs deleted file mode 100644 index f2c3452..0000000 --- a/tests/stretch.rs +++ /dev/null @@ -1,46 +0,0 @@ -//! What a drawing can be taken out of, and what it cannot. - -use iris::core::{UiRegion, UiScalar, UiSpan}; - -/// A box `size` tall whose top is `rel` of the way down the window. -fn fixed(rel: f32, size: f32) -> UiRegion { - UiRegion::new( - UiSpan::FULL, - UiSpan::new(UiScalar { rel, abs: 0.0 }, UiScalar { rel, abs: size }), - ) -} - -#[test] -fn a_fixed_length_cannot_be_stretched_out_of() { - assert!(!fixed(0.0, 164.0).stretchable()); - assert!(!fixed(0.5, 164.0).stretchable()); - assert!(UiRegion::FULL.stretchable()); -} - -#[test] -fn a_stretch_keeps_each_part_at_its_fraction() { - let to = fixed(0.0, 98.0); - // A part filling the window fills what replaced it. - assert_eq!(UiRegion::FULL.stretch(&UiRegion::FULL, &to), to); - // And the middle half of it stays the middle half. - let half = UiRegion::new( - UiSpan::FULL, - UiSpan::new(UiScalar::rel(0.25), UiScalar::rel(0.75)), - ); - assert_eq!( - half.stretch(&UiRegion::FULL, &to), - UiRegion::new( - UiSpan::FULL, - UiSpan::new( - UiScalar { - rel: 0.0, - abs: 24.5 - }, - UiScalar { - rel: 0.0, - abs: 73.5 - } - ) - ) - ); -}