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

+39 -63
View File
@@ -1,6 +1,6 @@
use crate::{
DrawRegion, LayerId, LayoutHolds, LayoutLen, MaskIdx, MoveIdx, RegionAlign, RetainedPrimitive,
Size, TextureHandle, UiRegion, UiVec2, WidgetId,
LayerId, LayoutHolds, LayoutLen, MaskIdx, MoveIdx, Place, RegionAlign, RetainedPrimitive, Size,
TextureHandle, UiRegion, WidgetId,
};
/// What is kept of a widget its parent has asked about. `drawn` says whether
@@ -9,28 +9,35 @@ use crate::{
#[derive(Debug)]
pub struct ActiveData {
pub id: WidgetId,
/// The box its parent gave it, in `parent_move`'s coordinates: what it
/// was asked about, and what a fraction under it is a fraction of. A
/// local redraw asks here.
pub region: UiRegion,
/// Where its drawing sits inside that box, in the box's own coordinates.
pub placement: UiRegion,
/// The original frame in its parent widget's coordinates. Recomposition
/// and pixel-length evaluation both follow this chain.
pub given_region: UiRegion,
/// The lengths of the box its parent first asked about it in, as
/// lengths of the box the parent was itself offered. Any later box it
/// was given was decided knowing its answer, so this is the question
/// asked again -- and a chain of fractions has no frame in it, which is
/// why a region node between two widgets cannot break it.
pub offer_len: UiVec2,
pub offer_placement: [Option<crate::UiSpan>; 2],
/// Its frame in `parent_move`'s coordinates: what a fraction it declares
/// or reports is a fraction of, composed. Everything it draws sits inside
/// this by way of `extent`.
pub frame_abs: UiRegion,
/// Where its drawing goes, in the frame's own coordinates.
pub extent: UiRegion,
/// That frame in its parent's frame coordinates, before composition:
/// forwarded whole by a transparent container, narrowed by a declared
/// length or an inset. Its length is the same on every ask, which is what
/// a local redraw relies on to ask its parent's own question again.
pub frame: UiRegion,
/// What of its parent's extent the drawing was given, and what it was
/// given at the parent's first ask of it -- the question a cold layout
/// asks. A part is a length from the extent's start, so an extent that
/// moved re-places every child by re-adding that start.
pub place: [Place; 2],
pub offer_place: [Place; 2],
/// The box that ask gave it, in its frame's coordinates. Kept rather
/// than worked out again from where its parent's own box is now: a
/// parent drawn again in the box its own answer chose gives its children
/// boxes it never measured anything in, and the measurement this widget
/// answered is the one its parent's layout was built on.
pub offer_part: 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)>,
/// What the widget said it used of its box, the last time it drew.
/// What the widget said it used of its frame, the last time it drew.
pub size: Size,
/// The frame, extent and explicit placement reads that this drawing holds for.
/// The frame and extent reads that this drawing holds for.
pub holds: LayoutHolds,
pub drawn: bool,
pub parent: Option<WidgetId>,
@@ -39,30 +46,24 @@ pub struct ActiveData {
/// widget a frame visits and cannot drift while one is being drawn.
pub depth: usize,
pub textures: Vec<TextureHandle>,
/// Its primitives, each keeping the box it was written in -- in this
/// widget's extent coordinates, which is what a move recomposes from.
pub primitives: Vec<RetainedPrimitive>,
pub mask_region: Option<DrawRegion>,
/// The children whose box is a part of this widget's extent rather than
/// of its frame, and which part each was given. Moving the extent
/// re-places them through that part, so the drawing need not depend on
/// where it sits.
pub(crate) extent_children: Vec<(WidgetId, ExtentPlacement)>,
pub mask_region: Option<UiRegion>,
pub children: Vec<WidgetId>,
/// The children whose size this widget read while drawing.
pub size_deps: Vec<WidgetId>,
/// The movable region its primitives are positioned through: its own when
/// opted in, otherwise the nearest ancestor's.
pub move_idx: MoveIdx,
/// The declared lengths whoever drew this widget resolved into its box.
/// The declared lengths whoever drew this widget resolved into its frame.
/// A change to one moves a box this widget cannot fix by drawing again,
/// and comparing them is what says so.
pub declared: [Option<LayoutLen>; 2],
/// The axes along which its parent chose its box from its own answer,
/// so a local redraw asks the question its parent asked.
pub decided: [bool; 2],
/// Its alignment when it was last drawn, which a change to the property
/// is found against.
pub own_align: RegionAlign,
/// The movable region whose coordinates `region` uses.
/// The movable region whose coordinates `frame_abs` uses.
pub parent_move: MoveIdx,
/// The mask its drawing is clipped to: one it set itself, or the one it
/// inherited from whoever drew it.
@@ -76,37 +77,12 @@ pub struct ActiveData {
}
impl ActiveData {
/// 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 {
self.answer.is_some_and(|(_, holds)| {
holds.contains(
px,
UiRegion {
x: self.offer_placement[0].unwrap_or(crate::UiSpan::FULL),
y: self.offer_placement[1].unwrap_or(crate::UiSpan::FULL),
},
)
})
}
}
/// What of a container's extent a child was given: the whole of it, for a
/// wrapper whose box is its child's, or a part of it.
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum ExtentPlacement {
Inherit,
Within(UiRegion),
}
impl ExtentPlacement {
/// The child's frame in the container's frame coordinates, and the slot
/// the container chose within it.
pub fn resolve(self, extent: UiRegion) -> (UiRegion, [Option<crate::UiSpan>; 2]) {
match self {
Self::Inherit => (UiRegion::FULL, [Some(extent.x), Some(extent.y)]),
Self::Within(part) => (part.within(&extent), [None; 2]),
}
/// Whether what it answered still stands for a frame of these pixel
/// lengths. The answer was given in the box its parent first asked
/// about, which is what it is checked against -- `holds` on the record
/// is about the box the answer then chose.
pub fn answers_at(&self, px: crate::PxVec2, part: UiRegion) -> bool {
self.answer
.is_some_and(|(_, holds)| holds.contains(px, part))
}
}