Compare commits

..
Author SHA1 Message Date
iris-ai e44dea34b4 Record which widget is drawing a subtree that changed hands
A subtree can be reused whole under a different parent -- same box, same
layer, same region node, clean -- and nothing in the drawing says it
moved. Two things read who its parent is, and both were wrong after one
of these.

The old parent still listed it as a child, and a parent's next draw
undraws whatever is missing from that list: two spans under one root,
with the root swapping which of them it holds, drew the subtree under
the new span and then erased it when the old one drew. The move is
recorded on both sides where `draw_inner` already writes what the ask
decided, rather than guarded at each reader.

Its depth was also the one it had under the old parent, which is what
the settling walk orders by, so a change made under it afterwards
settled at the wrong point in the frame. `try_reuse` re-walks the
subtree's depths, and only where the top of it moved, which is what
makes that free in the ordinary case.

Two tests: one shape where the subtree's box does not move and the span
it left erases it, one where it changes depth and the change made under
it has to reach the span it moved to. Each fails without one half.
2026-09-17 13:05:15 -04:00
iris-ai a0693acc56 Let a resize settle through the walk, and drop the stale-answer guard
A resize drew the root outside `redraw_updates`, top-down over a tree
with dirty widgets still in it, which is the one entry point
`dirty_size_under` was guarding: since `a92c6ac` settles a frame strictly
bottom-up, no fuzzer could tell whether that guard still did anything
anywhere else. Closing the entry point retires the guard rather than
keeping a check for a hole reasoned rather than measured.

The root is marked instead, and only where the new output falls outside
what its answer holds for. That range is the intersection of everything
under it, so admitting the new output says the whole tree still stands,
and nothing above the root moved -- the window is no entry to rewrite.
Marking it unconditionally would have cost the root its own `Holds`: a
leaf root that scales with its box was drawn again on every resize.

`dirty_size_under` goes at both call sites. `resize` takes `Widgets`
because a mark is what it now leaves behind.
2026-09-17 13:00:32 -04:00
iris-ai 25e456e0b5 Say what the fuzzers can no longer tell about the stale-answer guard
Dropping `dirty_size_under` from it now passes every run there is. It stays
for the one entry the bottom-up ordering does not reach -- `update` draws
the root for a resize before `redraw_updates` runs -- which is a hole
reasoned rather than measured, and the note says which.
2026-09-17 05:12:44 -04:00
iris-ai 53b00c68e9 Find a span's leftover boundary through the inverse it already has
The decision used a rounded division, `total.px.div(fixed)`, where the room
the children get is a floored multiply, so the boundary and the drawing it
guards were two expressions for one length and disagreed at the edge of it.
`room` is that length as a `Len`, `room.to_px` is the multiply, and
`Holds::through` is its exact preimage -- so ask `room` whether anything is
left and hand the answer back through the same expression.

The three branches go with the division. They were the sign of `1 - rel`:
the fixed parts growing slower than the box, faster, or exactly with it, and
`through` reads that sign already. Forty lines become twelve, one `div`
leaves layout, and the boundary is the drawing's own.

Green on the suite, the shrinker at 400 seeds of depth 5, the oracle at 1000
seeds of depth 6 and 120 in debug, and 2000 seeds at depth 4 over all
fifteen cases. `tabs`, `view`, `minimal` and `random` byte-identical.
2026-09-17 05:02:45 -04:00
12 changed files with 264 additions and 202 deletions

No files matched your search

+8
View File
@@ -74,4 +74,12 @@ impl ActiveData {
pub fn holds_at(&self, px: crate::PxVec2) -> bool { pub fn holds_at(&self, px: crate::PxVec2) -> bool {
self.holds[0].contains(px.x) && self.holds[1].contains(px.y) self.holds[0].contains(px.x) && self.holds[1].contains(px.y)
} }
/// Whether what it answered still stands for a box of these pixel
/// lengths -- the box it was asked in, where `holds` is about the box its
/// answer then chose.
pub fn answers_at(&self, px: crate::PxVec2) -> bool {
let (_, holds) = self.answer;
holds[0].contains(px.x) && holds[1].contains(px.y)
}
} }
+41 -19
View File
@@ -139,20 +139,30 @@ impl<'a> Painter<'a> {
id: &'s StrongWidget<W>, id: &'s StrongWidget<W>,
region: UiRegion, region: UiRegion,
) -> DrawResult<'s, 'a, W> { ) -> DrawResult<'s, 'a, W> {
self.widget_at(id, region, [false; 2]) self.widget_at(id, region, region.size(), [false; 2])
} }
/// Draws a widget in a box this widget chose from the widget's own /// Draws a widget in `region`, saying what the answer means.
/// answer along the `decided` axes. On those the answer is not placed ///
/// inside the box again: it already is the box, and a fraction the /// `reports_of` is what a fraction the child reports is a fraction of, as
/// widget reported, taken of this box a second time, would shrink it /// lengths of this widget's own box. It is the box the child was given
/// twice. A container uses this where it hands back exactly what a child /// wherever that box is the child's whole area -- a pad's inset, a stack
/// asked for -- a span placing a child at the length it reported, a /// child, a scroll's content -- and a span passes its own extent along
/// scroll giving its content the content's own length. /// the row instead: it offers each child the room left from its cursor,
/// because a text has to wrap at the width actually there, while
/// `rel(0.5)` still means half the span wherever the child sits in it.
///
/// A `decided` axis is one where this box was chosen from the widget's
/// own answer. On those the answer is not placed inside the box again: it
/// already is the box, and a fraction taken of it a second time would
/// shrink it twice. A container uses that where it hands back exactly
/// what a child asked for -- a span placing a child at the length it
/// reported, a scroll giving its content the content's own length.
pub fn widget_at<'s, W: ?Sized>( pub fn widget_at<'s, W: ?Sized>(
&'s mut self, &'s mut self,
id: &'s StrongWidget<W>, id: &'s StrongWidget<W>,
region: UiRegion, region: UiRegion,
reports_of: UiVec2,
decided: [bool; 2], decided: [bool; 2],
) -> DrawResult<'s, 'a, W> { ) -> DrawResult<'s, 'a, W> {
let region_node = self.rsc.widgets().is_region_node(id.id()); let region_node = self.rsc.widgets().is_region_node(id.id());
@@ -222,18 +232,10 @@ impl<'a> Painter<'a> {
for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) { for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) {
*under = under.and(holds[axis as usize].through(local.axis(axis).len())); *under = under.and(holds[axis as usize].through(local.axis(axis).len()));
} }
// The answer as it was given. A fraction in it is a fraction of this
// widget's box, which is the same thing a rule beside the child
// means and the same thing for every box this widget hands out: a
// span offers each child the room left from its cursor, because a
// text has to wrap at the width actually there, and `rel(0.5)` is
// still half the span. Padding is outside what it pads for the same
// reason -- inset the fraction and a child's `rel` would mean the
// inner box while its `px` meant the outer one.
DrawResult { DrawResult {
child: id, child: id,
painter: self, painter: self,
size, size: in_parent_frame(size, reports_of, declared),
} }
} }
@@ -267,12 +269,14 @@ impl<'a> Painter<'a> {
/// A child's length in the box it is about to be offered, if it can be /// A child's length in the box it is about to be offered, if it can be
/// had without drawing it: from its hint, or from a drawing it already /// had without drawing it: from its hint, or from a drawing it already
/// has that holds for that box. /// has that holds for that box. `reports_of` is what a fraction in the
/// answer is a fraction of, as it is for [`Self::widget_at`].
pub fn known_len<W: ?Sized>( pub fn known_len<W: ?Sized>(
&mut self, &mut self,
child: &StrongWidget<W>, child: &StrongWidget<W>,
axis: Axis, axis: Axis,
region: UiRegion, region: UiRegion,
reports_of: UiVec2,
) -> Option<LayoutLen> { ) -> Option<LayoutLen> {
let declared = self.declared_lens(child); let declared = self.declared_lens(child);
let align = self.rsc.widgets().alignment(child.id()); let align = self.rsc.widgets().alignment(child.id());
@@ -298,7 +302,7 @@ impl<'a> Painter<'a> {
for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) { for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) {
*under = under.and(holds[axis as usize].through(local.axis(axis).len())); *under = under.and(holds[axis as usize].through(local.axis(axis).len()));
} }
Some(size.axis(axis)) Some(in_parent_frame(size, reports_of, declared).axis(axis))
} }
/// Whether this is the first box a child is asked about in during a draw /// Whether this is the first box a child is asked about in during a draw
@@ -517,6 +521,24 @@ impl PrimitiveLike for &TextureHandle {
} }
} }
/// A child's answer as lengths of the parent's own box. A widget reports a
/// fraction, and `reports_of` is the length that fraction is of: the box the
/// child was given wherever that is the child's whole area, and the parent's
/// own extent wherever the box is a positional remainder, as a span's is
/// after an earlier child. Pixels come through untouched either way, being
/// that many pixels wherever they end up. A declared axis is already the
/// parent's: it resolved the rule in its own box, and the rule is what the
/// report says.
fn in_parent_frame(size: Size, reports_of: UiVec2, declared: [Option<LayoutLen>; 2]) -> Size {
let mut size = size;
for (axis, declared) in AXES.into_iter().zip(declared) {
if declared.is_none() {
*size.axis_mut(axis) = size.axis(axis).within_len(reports_of.axis(axis));
}
}
size
}
/// What a widget declares a length of its box to be. `leftover` is not one: a /// What a widget declares a length of its box to be. `leftover` is not one: a
/// share of what is left over is only a length to the widget dividing one, /// share of what is left over is only a length to the widget dividing one,
/// so it passes up in the size instead. /// so it passes up in the size instead.
+56 -40
View File
@@ -43,7 +43,9 @@ pub struct UiRenderState {
old_root: Option<WidgetId>, old_root: Option<WidgetId>,
/// Whether the output has changed since the last update. A frame is /// Whether the output has changed since the last update. A frame is
/// owed for that whether or not anything has to be drawn again. /// owed for that whether or not anything has to be drawn again: every
/// fraction becomes pixels against the output, in the shader's uniform
/// as well as here.
resized: bool, resized: bool,
/// A widget's move slot, which outlives any one `ActiveData`: a redraw /// A widget's move slot, which outlives any one `ActiveData`: a redraw
/// replaces that while its children go on pointing at the slot. /// replaces that while its children go on pointing at the slot.
@@ -83,13 +85,28 @@ impl UiRenderState {
/// size is applied where a fraction becomes pixels -- here in `to_px`, /// size is applied where a fraction becomes pixels -- here in `to_px`,
/// and in the shader by its uniform. A resize therefore rewrites no /// and in the shader by its uniform. A resize therefore rewrites no
/// retained entry at all. /// retained entry at all.
pub fn resize(&mut self, size: impl Into<Vec2>) { ///
/// The root is the only widget a resize marks, and only where the new
/// output falls outside what its answer holds for: that range is the
/// intersection of everything under it, so admitting the new output says
/// the whole tree still stands. Where it does not, the ordinary walk
/// draws the root, and each widget's own range decides how far down the
/// new length reaches.
pub fn resize(&mut self, size: impl Into<Vec2>, widgets: &mut Widgets) {
let size = PxVec2::from_f32(size.into()); let size = PxVec2::from_f32(size.into());
if size == self.output_size { if size == self.output_size {
return; return;
} }
self.output_size = size; self.output_size = size;
self.resized = true; self.resized = true;
let Some(root) = self.old_root else { return };
let stands = self
.active
.get(&root)
.is_some_and(|active| active.answers_at(active.given_len.to_px(size)));
if !stands {
widgets.needs_redraw.insert(root);
}
} }
/// The root is asked about in the output: the window is where a fraction /// The root is asked about in the output: the window is where a fraction
@@ -143,17 +160,6 @@ impl UiRenderState {
if self.root_changed(root) { if self.root_changed(root) {
self.redraw_all(root, rsc); self.redraw_all(root, rsc);
self.old_root = root.map(|r| r.id()); self.old_root = root.map(|r| r.id());
} else if let Some(root) = root
&& self.resized
{
// The output is the root's box, so a resize is that box changing
// length, found the way every other box change is found. Before
// anything dirty settles, so that whatever a new output draws
// again is drawn once, in the box it will have.
let region = Self::root_region(root.id(), rsc.widgets());
let info = self.root_info(region);
let answer = self.draw_inner(root.id(), region, info, None, rsc);
self.active.get_mut(&root.id()).unwrap().answer = answer;
} }
self.resized = false; self.resized = false;
if rsc.widgets().has_updates() { if rsc.widgets().has_updates() {
@@ -196,14 +202,10 @@ impl UiRenderState {
diag::draw_request(id, info.parent, region, info.px, info.region_node); diag::draw_request(id, info.parent, region, info.px, info.region_node);
} }
let align = rsc.widgets().alignment(id); let align = rsc.widgets().alignment(id);
// Nothing this widget has is an answer while something it measured // Nothing this widget measured can be dirty while it draws: layout is
// is dirty: settling that changes what it would report, and a widget // one bottom-up walk, so anything deeper has settled or deferred to
// settled inside its parent's draw tells nobody -- the comparison // its own parent, and a deferred one leaves that parent marked.
// that marks a reader is in `redraw`, which is not what asked here. let stale = rsc.widgets().needs_redraw.contains(&id);
// Both retained routes are an answer, so the question is asked once
// rather than by each of them.
let stale =
rsc.widgets().needs_redraw.contains(&id) || self.dirty_size_under(id, rsc.widgets());
let replace_answer = self.answer_invalid.remove(&id) || (self.replace_answers && stale); let replace_answer = self.answer_invalid.remove(&id) || (self.replace_answers && stale);
let retained = match replace_answer || stale { let retained = match replace_answer || stale {
true => None, true => None,
@@ -251,7 +253,18 @@ impl UiRenderState {
active.answer = settled; active.answer = settled;
active.decided = info.decided; active.decided = info.decided;
active.own_align = align; active.own_align = align;
active.depth = info.depth; // A subtree can be reused whole under a different parent -- same box,
// same layer, same region node -- and nothing in the drawing says it
// changed hands. Two things read who its parent is: a deferral, which
// marks whoever has it to draw, and the old parent's list of children,
// which its next draw undraws whatever is missing from.
let old_parent = std::mem::replace(&mut active.parent, info.parent);
if old_parent != info.parent
&& let Some(old_parent) = old_parent
&& let Some(old_parent) = self.active.get_mut(&old_parent)
{
old_parent.children.retain(|child| *child != id);
}
settled settled
} }
@@ -484,7 +497,7 @@ impl UiRenderState {
parent_move: MoveIdx, parent_move: MoveIdx,
widgets: &Widgets, widgets: &Widgets,
) -> Option<(Size, [Holds; 2])> { ) -> Option<(Size, [Holds; 2])> {
if widgets.needs_redraw.contains(&id) || self.dirty_size_under(id, widgets) { if widgets.needs_redraw.contains(&id) {
return None; return None;
} }
let active = self.active.get(&id)?; let active = self.active.get(&id)?;
@@ -509,22 +522,7 @@ impl UiRenderState {
{ {
return None; return None;
} }
let (size, holds) = active.answer; active.answers_at(info.px).then_some(active.answer)
(holds[0].contains(info.px.x) && holds[1].contains(info.px.y)).then_some((size, holds))
}
/// Whether anything whose size this widget's own size was read from is
/// dirty, which makes what it would answer not yet known. It also keeps
/// a reader that asks first from laying out twice, which is all it was
/// here for while a changed size was thought to reach its reader in any
/// order; it does not, where the change settles inside the reader's own
/// draw.
fn dirty_size_under(&self, id: WidgetId, widgets: &Widgets) -> bool {
self.active.get(&id).is_some_and(|active| {
active.size_deps.iter().any(|child| {
widgets.needs_redraw.contains(child) || self.dirty_size_under(*child, widgets)
})
})
} }
/// The pixel lengths of the box a widget was given and of the box it was /// The pixel lengths of the box a widget was given and of the box it was
@@ -638,12 +636,12 @@ impl UiRenderState {
self.remap_subtree(id, &remap, info.parent_move, rsc); self.remap_subtree(id, &remap, info.parent_move, rsc);
} }
} }
self.redepth(id, info.depth);
let active = self.active.get_mut(&id).unwrap(); let active = self.active.get_mut(&id).unwrap();
active.region = region; active.region = region;
active.given = region; active.given = region;
active.given_len = info.given_len; active.given_len = info.given_len;
active.offer_len = info.offer_len; active.offer_len = info.offer_len;
active.depth = info.depth;
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
{ {
match (moved, has_region_node) { match (moved, has_region_node) {
@@ -667,6 +665,24 @@ impl UiRenderState {
Some(answer) Some(answer)
} }
/// 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.
fn redepth(&mut self, id: WidgetId, depth: usize) {
let Some(active) = self.active.get_mut(&id) else {
return;
};
if active.depth == depth {
return;
}
active.depth = depth;
let children = active.children.len();
for index in 0..children {
let child = self.active[&id].children[index];
self.redepth(child, depth + 1);
}
}
/// Re-expresses an ordinary retained subtree in a new parent region. /// Re-expresses an ordinary retained subtree in a new parent region.
/// An independently movable descendant needs only its own region changed; /// An independently movable descendant needs only its own region changed;
/// its contents stay in that region's coordinate space. /// its contents stay in that region's coordinate space.
+1 -1
View File
@@ -251,7 +251,7 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
ui_state.renderer.draw(); ui_state.renderer.draw();
} }
WindowEvent::Resized(size) => { WindowEvent::Resized(size) => {
render.resize((size.width, size.height)); render.resize((size.width, size.height), rsc.widgets_mut());
ui_state.renderer.resize(size) ui_state.renderer.resize(size)
} }
WindowEvent::KeyboardInput { event, .. } => { WindowEvent::KeyboardInput { event, .. } => {
+3 -3
View File
@@ -144,9 +144,9 @@ impl Harness {
// bound that comes with `SyncSender` is far past anything a test // bound that comes with `SyncSender` is far past anything a test
// leaves unread. // leaves unread.
let (send, updates) = sync_channel(1024); let (send, updates) = sync_channel(1024);
let rsc = DefaultRsc::init(Arc::new(Queue(send))); let mut rsc = DefaultRsc::init(Arc::new(Queue(send)));
let mut render = UiRenderState::new(); let mut render = UiRenderState::new();
render.resize(size); render.resize(size, rsc.widgets_mut());
Self { Self {
rsc, rsc,
render, render,
@@ -161,7 +161,7 @@ impl Harness {
} }
pub fn resize(&mut self, size: impl Into<Vec2>) { pub fn resize(&mut self, size: impl Into<Vec2>) {
self.render.resize(size); self.render.resize(size, self.rsc.widgets_mut());
} }
/// Changes a length rule after the fact, the way `.width()` sets one. /// Changes a length rule after the fact, the way `.width()` sets one.
+4 -60
View File
@@ -9,10 +9,10 @@ impl Widget for Pad {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
// The inner's own alignment, not the near edge. This reports the // The inner's own alignment, not the near edge. This reports the
// inner's size plus the padding, so where the box is that answer the // inner's size plus the padding, so where the box is that answer the
// inner is exactly what it asked for and alignment has no room to // inset box is exactly the inner and alignment has no room to move
// move it; where the box is bigger -- a share of a row, a rule over // it; where the box is bigger -- a share of a row, a rule over this
// this widget -- the slack is the inner's to sit in, and forcing the // widget -- the slack is the inner's to sit in, and forcing the near
// near edge pinned it to a corner it had not asked for. // edge pinned it to a corner it had not asked for.
let inner = painter let inner = painter
.widget_within(&self.inner, self.padding.region()) .widget_within(&self.inner, self.padding.region())
.size(); .size();
@@ -29,45 +29,6 @@ impl Widget for Pad {
} }
} }
/// Room taken off the inside rather than added round the outside: the child
/// draws in what is left once both edges are gone, and this widget is
/// exactly as long as the box it was given.
///
/// So `rel(1.0)` under an [`Inset`] is the room inside it, where the same
/// rule under a [`Pad`] is the pad's whole box and overflows it by the
/// padding. Both are wanted; which one a layout means is which widget it
/// reaches for.
pub struct Inset {
pub padding: Padding,
pub inner: StrongWidget,
}
impl Widget for Inset {
fn draw(&mut self, painter: &mut Painter) -> Size {
let region = self.padding.inset_region();
let inner = painter.widget_within(&self.inner, region).size();
// What a fraction the child reported is a fraction of is this
// widget's to say, and it says the room inside: the child asked for
// a part of the box it drew in, and that box is shorter than this
// one by both edges. Then the edges go back on, so this widget is
// its child and the room taken off around it.
let (x, y) = (
inner.x.within_len(region.x.len()),
inner.y.within_len(region.y.len()),
);
Size {
x: LayoutLen {
px: x.px + self.padding.left + self.padding.right,
..x
},
y: LayoutLen {
px: y.px + self.padding.top + self.padding.bottom,
..y
},
}
}
}
pub struct Padding { pub struct Padding {
pub left: Px, pub left: Px,
pub right: Px, pub right: Px,
@@ -92,24 +53,7 @@ impl Padding {
bottom: amt, bottom: amt,
} }
} }
/// The box a [`Pad`] gives its child: as long as the pad's own, moved in
/// by the near edge. Padding is outside what it pads, so a fraction the
/// child asks for is a fraction of the same length whether a rule beside
/// it states one or it reports one, and its pixels are the same pixels.
/// Shrinking the box instead would make `rel` mean the inner box while
/// `px` meant the outer one. [`Inset`] is the widget that shrinks.
pub fn region(&self) -> UiRegion { pub fn region(&self) -> UiRegion {
let mut region = UiRegion::FULL;
region.x.start.px += self.left;
region.y.start.px += self.top;
region.x.end.px += self.left;
region.y.end.px += self.top;
region
}
/// The box an [`Inset`] gives its child: shorter than its own by both
/// edges, so what the child fills is the room left inside.
pub fn inset_region(&self) -> UiRegion {
let mut region = UiRegion::FULL; let mut region = UiRegion::FULL;
region.x.start.px += self.left; region.x.start.px += self.left;
region.y.start.px += self.top; region.y.start.px += self.top;
+3 -2
View File
@@ -14,7 +14,8 @@ impl Widget for Scroll {
let container_len = painter.px_len(self.axis); let container_len = painter.px_len(self.axis);
// Draw in the whole container only when its scrolling-axis length is // Draw in the whole container only when its scrolling-axis length is
// not already known, then draw it at the scrolled offset. // not already known, then draw it at the scrolled offset.
let answer_len = match painter.known_len(&self.inner, self.axis, UiRegion::FULL) { let whole = UiRegion::FULL;
let answer_len = match painter.known_len(&self.inner, self.axis, whole, whole.size()) {
Some(len) => len, Some(len) => len,
None => painter.widget(&self.inner).size().axis(self.axis), None => painter.widget(&self.inner).size().axis(self.axis),
}; };
@@ -63,7 +64,7 @@ impl Widget for Scroll {
region = region.offset(offset); region = region.offset(offset);
region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len); region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len);
} }
painter.widget_at(&self.inner, region, [true; 2]); painter.widget_at(&self.inner, region, region.size(), [true; 2]);
// What it occupies is its box, on both axes: it clips its content to // What it occupies is its box, on both axes: it clips its content to
// that box, so it can neither take less of one nor honestly ask for // that box, so it can neither take less of one nor honestly ask for
// more. The content's length is what it scrolls through, not what it // more. The content's length is what it scrolls through, not what it
+29 -40
View File
@@ -21,12 +21,14 @@ impl Widget for Span {
} }
let region = UiRegion::from_axis(axis, span, UiSpan::FULL); let region = UiRegion::from_axis(axis, span, UiSpan::FULL);
// Offered the room left from the cursor, because a text has to // Offered the room left from the cursor, because a text has to
// wrap at the width actually there, while what it reports is a // wrap at the width actually there, but reporting a fraction of
// fraction of the whole row: `rel(0.5)` is half the span // the whole row: `rel(0.5)` is half the span whatever else is in
// whatever else is in it and wherever this child sits. // it and wherever this child sits among them.
let len = match painter.known_len(child, axis, region) { let len = match painter.known_len(child, axis, region, UiVec2::FULL_SIZE) {
Some(len) => len, Some(len) => len,
None => painter.widget_at(child, region, [false; 2]).len(axis), None => painter
.widget_at(child, region, UiVec2::FULL_SIZE, [false; 2])
.len(axis),
}; };
cursor.px += len.px + self.gap; cursor.px += len.px + self.gap;
cursor.rel += len.rel; cursor.rel += len.rel;
@@ -44,43 +46,26 @@ impl Widget for Span {
|sum, len| sum + *len, |sum, len| sum + *len,
); );
// What is left for the shares to divide: the box less everything
// fixed, as a length of the box rather than a number of pixels.
let room = Len::rel_max() - Len::from_parts(total.rel, total.px);
// Whether anything is left over is a question in pixels: `rel(0.5)` // Whether anything is left over is a question in pixels: `rel(0.5)`
// beside 300 px is full at 600 and overfull at 400. The room to // beside 300 px is full at 600 and overfull at 400. Asked of `room`
// divide is `len * fixed - total.px`, and the length where it runs // itself, and answered back through the same expression, so the
// out is exactly the box a parent sizing itself from this answer // boundary is the drawing's own and not a second way of finding it:
// hands back -- which is why this used to need a margin either side // the three cases a rounded division needed -- the fixed parts
// of the boundary, and why it does not now: that box and this sum are // growing slower than the box, faster, or exactly with it -- are the
// whole counts of the same step, and both routes to it land on the // sign of `room.rel`, which `through` already reads. What the
// same count. What the generated oracle checks is the consequence, // generated oracle checks is the consequence, since which children
// since which children exist at all turns on this. // exist at all turns on this.
let fixed = Rel::ONE - total.rel;
let mut shares = false; let mut shares = false;
if total.leftover > Weight::ZERO { if total.leftover > Weight::ZERO {
let current = painter.px_len(axis); shares = room.to_px(painter.px_len(axis)) > Px::ZERO;
let holds = if fixed > Rel::ZERO { let holds = match shares {
// The box length the fixed parts alone fill. true => Holds::from(Px::STEP..=Px::MAX),
let full = total.px.div(fixed); false => Holds::from(Px::MIN..=Px::ZERO),
shares = current > full;
match shares {
true => Holds::from(full.next_up()..=Px::MAX),
false => Holds::from(Px::MIN..=full),
}
} else if fixed < Rel::ZERO {
// The relative parts grow faster than the box does, so here
// a shorter box is the one that leaves room.
let full = total.px.div(fixed);
shares = current < full;
match shares {
true => Holds::from(Px::MIN..=full.next_down()),
false => Holds::from(full..=Px::MAX),
}
} else {
// The relative parts take exactly the box, whatever it is, so
// the only room is what negative pixels leave.
shares = total.px < Px::ZERO;
Holds::ANY
}; };
painter.holds(axis, holds); painter.holds(axis, holds.through(room));
} }
// Across itself a span is as long as its longest child -- unless a // Across itself a span is as long as its longest child -- unless a
@@ -97,7 +82,6 @@ impl Widget for Span {
// row. // row.
let mut fixed = Len::rel_min(); let mut fixed = Len::rel_min();
let mut taken = Weight::ZERO; let mut taken = Weight::ZERO;
let room = Len::rel_max() - Len::from_parts(total.rel, total.px);
let mut start = Len::rel_min(); let mut start = Len::rel_min();
let mut ortho = LayoutLen::ZERO; let mut ortho = LayoutLen::ZERO;
for (child, len) in self.children.iter().zip(&lens) { for (child, len) in self.children.iter().zip(&lens) {
@@ -126,7 +110,12 @@ impl Widget for Span {
// Along the row this box is the child's own answer, so the answer // Along the row this box is the child's own answer, so the answer
// is not placed in it again; across it the child sits where its // is not placed in it again; across it the child sits where its
// alignment says. // alignment says.
let placed = painter.widget_at(child, region, [axis == Axis::X, axis == Axis::Y]); let placed = painter.widget_at(
child,
region,
UiVec2::FULL_SIZE,
[axis == Axis::X, axis == Axis::Y],
);
if shrinks { if shrinks {
let used = placed.len(!axis); let used = placed.len(!axis);
// Choosing between a fixed and a relative length from the // Choosing between a fixed and a relative length from the
+1 -1
View File
@@ -35,7 +35,7 @@ impl Widget for Stack {
// child is handed a box that owes nothing to its own answer, and // child is handed a box that owes nothing to its own answer, and
// where it sits in one bigger than itself is its own business. // where it sits in one bigger than itself is its own business.
match sizing == Some(i) { match sizing == Some(i) {
true => painter.widget_at(child, region, [true; 2]), true => painter.widget_at(child, region, region.size(), [true; 2]),
false => painter.widget_within(child, region), false => painter.widget_within(child, region),
}; };
} }
-10
View File
@@ -12,16 +12,6 @@ widget_trait! {
} }
} }
fn inset(self, padding: impl Into<Padding>) -> impl WidgetFn<Rsc, Inset> {
// Room taken off the inside, where `pad` adds it round the outside:
// this is as long as the box it is given and the child fills what is
// left of it.
|state| Inset {
padding: padding.into(),
inner: self.add_strong(state),
}
}
fn align(self, align: impl Into<Align>) -> impl WidgetIdFn<Rsc, WL::Widget> { fn align(self, align: impl Into<Align>) -> impl WidgetIdFn<Rsc, WL::Widget> {
// An axis left out keeps whatever it had, which is centered unless // An axis left out keeps whatever it had, which is centered unless
// something else set it. // something else set it.
+8 -25
View File
@@ -82,13 +82,11 @@ fn a_text_in_a_span_wraps_at_the_room_left_rather_than_the_whole_row() {
assert!(crowded > whole_row, "{crowded} against {whole_row}"); assert!(crowded > whole_row, "{crowded} against {whole_row}");
} }
/// Padding is outside what it pads, so a fraction under one is a fraction of /// The same reading through a pad: its inset is the whole box less the
/// the box the padding is measured from: half of a 400 px row is 200, and /// padding, so half of the inset plus the padding is half the box plus one
/// the pad is that plus both edges. Inset it instead and `rel` would mean the /// padding, not two.
/// inner box while `px` meant the outer one, which is the one thing a length
/// may not do.
#[test] #[test]
fn a_pad_is_outside_the_fraction_its_child_asked_for() { fn a_pad_reports_a_fraction_of_its_inset_as_a_fraction_of_its_box() {
let mut h = Harness::new((400, 100)); let mut h = Harness::new((400, 100));
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut h.rsc); let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut h.rsc);
let padded = (inner,).span(Dir::RIGHT).pad(10).add(&mut h.rsc); let padded = (inner,).span(Dir::RIGHT).pad(10).add(&mut h.rsc);
@@ -97,23 +95,8 @@ fn a_pad_is_outside_the_fraction_its_child_asked_for() {
// placed inside it by its own alignment, which is not what is under test. // placed inside it by its own alignment, which is not what is under test.
h.set_root((padded, tail).span(Dir::RIGHT).width(rel(1.0))); h.set_root((padded, tail).span(Dir::RIGHT).width(rel(1.0)));
assert_corners!(h, padded, (0, 0), (220, 100)); assert_corners!(h, padded, (0, 0), (210, 100));
assert_corners!(h, tail, (220, 0), (320, 100)); assert_corners!(h, tail, (210, 0), (310, 100));
}
/// The other half of the pair: an inset takes its room off the inside, so it
/// is exactly as long as the box it was given and the fraction its child
/// asked for is a fraction of what is left inside. Half of the 380 left in a
/// 400 px row is 190, and the inset is the whole 400.
#[test]
fn an_inset_is_inside_the_fraction_its_child_asked_for() {
let mut h = Harness::new((400, 100));
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut h.rsc);
let inset = (inner,).span(Dir::RIGHT).inset(10).add(&mut h.rsc);
h.set_root((inset,).span(Dir::RIGHT).width(rel(1.0)));
assert_corners!(h, inset, (0, 0), (200, 100));
assert_corners!(h, inner, (10, 0), (200, 100));
} }
#[test] #[test]
@@ -254,7 +237,7 @@ fn a_moved_subtree_takes_its_children_with_it() {
let mut h = Harness::new((400, 400)); let mut h = Harness::new((400, 400));
let first = rect(Color::RED).height(40).add(&mut h.rsc); let first = rect(Color::RED).height(40).add(&mut h.rsc);
let inner = rect(Color::BLUE).add(&mut h.rsc); let inner = rect(Color::BLUE).add(&mut h.rsc);
let row = inner.inset(10).height(40).region_node().add(&mut h.rsc); let row = inner.pad(10).height(40).region_node().add(&mut h.rsc);
// 80 of fixed rows in a 400 window, so the span takes 80 and sits in the // 80 of fixed rows in a 400 window, so the span takes 80 and sits in the
// middle of what it was given. // middle of what it was given.
h.set_root((first, row).span(Dir::DOWN)); h.set_root((first, row).span(Dir::DOWN));
@@ -297,7 +280,7 @@ fn a_box_with_a_fixed_length_can_be_stretched_on_its_other_axis() {
// impossible to take out of: recovering a fraction of a box needs a // impossible to take out of: recovering a fraction of a box needs a
// relative extent, and it has none on that axis. // relative extent, and it has none on that axis.
let inner = rect(Color::BLUE).add(&mut h.rsc); let inner = rect(Color::BLUE).add(&mut h.rsc);
let row = inner.inset(10).height(40).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 filler = rect(Color::GREEN).add(&mut h.rsc);
// This column is an item in a row, so it takes the width left for it // This column is an item in a row, so it takes the width left for it
// rather than asking for a full row-width in addition to the bar. // rather than asking for a full row-width in addition to the bar.
+110 -1
View File
@@ -462,7 +462,7 @@ fn a_change_two_levels_under_its_reader_still_reaches_it() {
let (leaf, _) = counted(&mut h, Size::px((100, 100).into()), true); let (leaf, _) = counted(&mut h, Size::px((100, 100).into()), true);
let padded = leaf.pad(10).add(&mut h.rsc); let padded = leaf.pad(10).add(&mut h.rsc);
let below = rect(Color::RED).add(&mut h.rsc); let below = rect(Color::RED).add(&mut h.rsc);
h.set_root((padded, below).span(Dir::DOWN).inset(12)); h.set_root((padded, below).span(Dir::DOWN).pad(12));
assert_corners!(h, below, (12, 132), (388, 388)); assert_corners!(h, below, (12, 132), (388, 388));
h.rsc[leaf].size = Size::px((100, 200).into()); h.rsc[leaf].size = Size::px((100, 200).into());
@@ -628,3 +628,112 @@ fn a_masked_widget_redrawn_on_its_own_sets_its_mask_again() {
h.frame(); h.frame();
assert_corners!(h, inner, (100, 0), (400, 200)); assert_corners!(h, inner, (100, 0), (400, 200));
} }
/// The two spans a subtree changes hands between, and the branch that is not
/// in the tree yet -- kept alive by the test until it is.
struct Handover {
leaf: WidgetId,
first: WeakWidget<Span>,
second: WeakWidget<Span>,
root: WeakWidget<Span>,
spare: StrongWidget,
}
/// A subtree that changes hands while its box does not move, so nothing about
/// reusing its drawing says it changed parents. `deeper` puts a span between
/// the root and `second`, so it changes depth by changing hands as well.
fn plant_handover(h: &mut Harness, moved: bool, deeper: bool, width: f32) -> Handover {
let leaf = rect(Color::RED).add(&mut h.rsc);
let sized = leaf.width(width).add(&mut h.rsc);
let holder = (sized,).span(Dir::RIGHT).add(&mut h.rsc);
let first = Span {
children: match moved {
true => Vec::new(),
false => vec![holder.add_strong(&mut h.rsc)],
},
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.add(&mut h.rsc);
let second = Span {
children: match moved {
true => vec![holder.add_strong(&mut h.rsc)],
false => Vec::new(),
},
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.add(&mut h.rsc);
let branch = match deeper {
true => (second,).span(Dir::RIGHT).add_strong(&mut h.rsc),
false => second.add_strong(&mut h.rsc),
};
let (in_tree, spare) = match moved {
true => (branch, first.add_strong(&mut h.rsc)),
false => (first.add_strong(&mut h.rsc), branch),
};
let root = Span {
children: vec![in_tree],
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.add(&mut h.rsc);
h.state.root = Some(root.add_strong(&mut h.rsc));
Handover {
leaf: sized.id(),
first,
second,
root,
spare,
}
}
/// Moves the subtree and swaps the branch it sits in for the one it left.
fn hand_over(h: &mut Harness, tree: Handover) -> WidgetId {
let holder = h.rsc[tree.first].children.remove(0);
h.rsc[tree.second].children.push(holder);
h.rsc[tree.root].children.clear();
h.rsc[tree.root].children.push(tree.spare);
h.frame();
tree.leaf
}
#[test]
fn a_subtree_that_changed_parents_is_not_undrawn_by_the_one_it_left() {
let mut warm = Harness::new((400, 200));
let tree = plant_handover(&mut warm, false, false, 40.0);
warm.frame();
let leaf = hand_over(&mut warm, tree);
let mut cold = Harness::new((400, 200));
let grown = plant_handover(&mut cold, true, false, 40.0);
cold.frame();
assert_eq!(
warm.region(&leaf),
cold.region(&grown.leaf),
"the span it left still listed it and undrew it"
);
}
#[test]
fn a_subtree_that_changed_parents_settles_at_the_depth_it_moved_to() {
let mut warm = Harness::new((400, 200));
let tree = plant_handover(&mut warm, false, true, 40.0);
warm.frame();
let leaf = hand_over(&mut warm, tree);
// After it has changed hands, so what has to reach the new parent is a
// change made under the subtree it now holds.
warm.set_len(leaf, Axis::X, LayoutLen::px(90.0));
warm.frame();
let mut cold = Harness::new((400, 200));
let grown = plant_handover(&mut cold, true, true, 90.0);
cold.frame();
assert_eq!(
warm.region(&leaf),
cold.region(&grown.leaf),
"the span it moved to is the one the change has to reach"
);
}