Lay out in a frame that passes through and a box placed in it

A widget is asked in two boxes rather than one. Its frame is what a fraction
it declares or reports is a fraction of, and it passes through a span, a
stack and a scroll unchanged, so `rel(0.5)` is half the same area however
many containers sit between: a frame is narrowed only by what is decided
above the widget -- a declared length, the root. Its extent is where the
drawing goes, given as a `Place` per axis: a part of the parent's own box,
measured in frame lengths from where that box starts, which the child either
fills or has its answer placed inside.

What that buys is that nothing under a container depends on where the
container sits. A container reads `extent_len` for the length it divides and
nothing about the start, so moving it re-places its children by re-adding
that start and draws nobody again; and a fraction is resolved once, against
the frame, rather than once per box it is composed through -- a stack sized
by a child that reports `rel(0.5)` no longer takes half of half.

`Place` replaces `DrawRegion`, `ExtentPlacement`, `widget_within`,
`measure_len`, `region()`, `placement()` and `box_of`. Primitives and masks
are written in the widget's own box's coordinates alone, so the drawing has
one reference rather than two. The placement pin goes with them: reading the
extent's length pins that length symbolically, and pins compose only where a
child's box is its parent's own.

Placing an answer waits for the end of the parent's draw or for the next ask
of that child in it, so a span child is one drawing and one move rather than
two moves.

`Pad` is transparent: its padding goes around what it pads and its child
keeps the outer frame, which is where `Outset` was going anyway. A fraction
under a pad is now a fraction of the frame rather than of the inset box.

Checked: fmt, clippy with -D warnings, 109 suite tests and 20 core tests in
debug, the 11 generated cases, and the shrinker at 400 trees of depth 5 over
all fifteen cases -- which still finds seed 108 under `reorder`, where a
wrapping text measured in one box and drawn in another settles differently
warm than cold. `redraw` therefore keeps the baseline's deferral for a box
that is not as long as the one the widget was measured in; the plan's step
6 is not done, and the next commit message or the handoff says why.
This commit is contained in:
iris-ai committed 2026-09-18 00:40:59 -04:00
1 parent 34cafb6edc
commit 1956be3f3d
19 files changed
+989 -943

No files matched your search

+37
View File
@@ -0,0 +1,37 @@
use crate::{PrimitiveHandle, UiRegion, UiSpan};
/// Where a child goes along one axis, as a part of this widget's extent.
/// Spans are frame lengths from the extent's start, so a span's slot is
/// `from..start` and a moved extent re-places every child by re-adding its
/// start, exactly. `None` is the whole extent.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Place {
/// The child's answer, aligned inside the part by the child's alignment.
Within(Option<UiSpan>),
/// Exactly the part; the answer is not placed inside it again.
Fill(Option<UiSpan>),
}
impl Place {
/// The part, where the caller named one rather than giving the whole
/// extent.
pub(crate) fn span(self) -> Option<UiSpan> {
match self {
Self::Within(span) | Self::Fill(span) => span,
}
}
/// Whether the part is the drawing's box outright, rather than the box
/// the answer is placed inside.
pub(crate) fn fills(self) -> bool {
matches!(self, Self::Fill(_))
}
}
/// A primitive as it was written: its box in the widget's extent
/// coordinates, which is what a move of that extent re-composes from.
#[derive(Debug)]
pub struct RetainedPrimitive {
pub handle: PrimitiveHandle,
pub region: UiRegion,
}