WIP: a widget's region stays put and its placement moves in it

The protocol split: `region` is the box a parent gives a widget -- what a
fraction it declares or reports is a fraction of, and the coordinates
every region it writes composes within -- and it is the same box on the
ask that measures and the ask that places. `placement` is what of that
region the drawing takes, chosen by the parent per axis or by the
widget's own answer and alignment.

That is what stops a fraction being resolved twice: the placing ask no
longer hands the widget its own answer as its box, so nothing under it
re-resolves against a box that came from its own report. `reports_of`
and `decided` are gone, folded into the two regions; `box_of` is gone;
`declared_box` becomes `ask_box`, which gives a rule the region's length
and takes the position from the placement.

84 of 92 suite tests pass. Five text and region-node cases still diverge
warm against cold, and three count a second widget draw where a span's
measuring ask and its placing ask give different placements.
This commit is contained in:
iris-ai committed 2026-09-17 13:53:14 -04:00
1 parent e44dea34b4
commit 5fcace1bfa
11 files changed
+346 -206

No files matched your search

+8 -5
View File
@@ -13,9 +13,8 @@ impl Widget for Pad {
// it; where the box is bigger -- a share of a row, a rule over this
// widget -- the slack is the inner's to sit in, and forcing the near
// edge pinned it to a corner it had not asked for.
let inner = painter
.widget_within(&self.inner, self.padding.region())
.size();
let inside = self.padding.region_of(painter.placement());
let inner = painter.widget_within(&self.inner, inside).size();
Size {
x: LayoutLen {
px: inner.x.px + self.padding.left + self.padding.right,
@@ -53,14 +52,18 @@ impl Padding {
bottom: amt,
}
}
pub fn region(&self) -> UiRegion {
let mut region = UiRegion::FULL;
/// `region` less this padding on each side.
pub fn region_of(&self, mut region: UiRegion) -> UiRegion {
region.x.start.px += self.left;
region.y.start.px += self.top;
region.x.end.px -= self.right;
region.y.end.px -= self.bottom;
region
}
pub fn region(&self) -> UiRegion {
self.region_of(UiRegion::FULL)
}
pub fn x(amt: impl UiNum) -> Self {
let amt = Px::from_num(amt);
Self {
+6 -2
View File
@@ -15,7 +15,7 @@ impl Widget for Scroll {
// Draw in the whole container only when its scrolling-axis length is
// not already known, then draw it at the scrolled offset.
let whole = UiRegion::FULL;
let answer_len = match painter.known_len(&self.inner, self.axis, whole, whole.size()) {
let answer_len = match painter.known_len(&self.inner, self.axis, whole) {
Some(len) => len,
None => painter.widget(&self.inner).size().axis(self.axis),
};
@@ -64,7 +64,11 @@ impl Widget for Scroll {
region = region.offset(offset);
region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len);
}
painter.widget_at(&self.inner, region, region.size(), [true; 2]);
// The viewport is the inner's region, so a fraction it declares or
// reports is a fraction of what is on screen rather than of the
// content box its own answer decided. Where it is put is the content
// box, scrolled.
painter.widget_at(&self.inner, whole, [Some(region.x), Some(region.y)]);
// 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
// more. The content's length is what it scrolls through, not what it
+36 -34
View File
@@ -10,25 +10,36 @@ 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 span of the region it was given.
// Its children are laid out along it, and what they declare or report
// is a fraction of the region -- the area this span was told it has,
// which it passes on unchanged.
let own = painter.placement();
let row = *own.axis(axis);
// Across itself the span's own box is the child's region: a span is
// what contains its children there, and nothing divides that axis.
// Along it the whole region is, so a fraction means the same thing
// for every child however much of the row is left when it is asked.
let region = UiRegion::from_axis(axis, UiSpan::FULL, *own.axis(!axis));
let along = |from: Len, to: Len| match self.dir.sign {
Sign::Pos => UiSpan::new(row.start + from, row.start + to),
Sign::Neg => UiSpan::new(row.end - to, row.end - from),
};
let far = row.len();
// A length for every child before their final boxes are chosen: from
// a hint where one exists, and from drawing otherwise.
let mut cursor = Len::rel_min();
let mut lens = Vec::with_capacity(self.children.len());
for child in &self.children {
let mut span = UiSpan::new(cursor, Len::rel_max());
if self.dir.sign == Sign::Neg {
span.flip();
}
let region = UiRegion::from_axis(axis, span, UiSpan::FULL);
// Offered the room left from the cursor, because a text has to
// wrap at the width actually there, but reporting a fraction of
// the whole row: `rel(0.5)` is half the span whatever else is in
// it and wherever this child sits among them.
let len = match painter.known_len(child, axis, region, UiVec2::FULL_SIZE) {
// The whole region is the child's, 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 it is placed in is the room left
// from the cursor, because a text has to wrap at the width
// actually there.
let room = axis.pair(Some(along(cursor, far)), None);
let len = match painter.known_len(child, axis, region) {
Some(len) => len,
None => painter
.widget_at(child, region, UiVec2::FULL_SIZE, [false; 2])
.len(axis),
None => painter.widget_at(child, region, room).len(axis),
};
cursor.px += len.px + self.gap;
cursor.rel += len.rel;
@@ -46,9 +57,9 @@ impl Widget for Span {
|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);
// What is left for the shares to divide: the row less everything
// fixed, as a length of the region rather than a number of pixels.
let room = far - Len::from_parts(total.rel, total.px);
// 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
@@ -60,12 +71,12 @@ impl Widget for Span {
// exist at all turns on this.
let mut shares = false;
if total.leftover > Weight::ZERO {
shares = room.to_px(painter.px_len(axis)) > Px::ZERO;
shares = room.to_px(painter.region_px_len(axis)) > Px::ZERO;
let holds = match shares {
true => Holds::from(Px::STEP..=Px::MAX),
false => Holds::from(Px::MIN..=Px::ZERO),
};
painter.holds(axis, holds.through(room));
painter.region_holds(axis, holds.through(room));
}
// Across itself a span is as long as its longest child -- unless a
@@ -94,28 +105,19 @@ impl Widget for Span {
fixed.px += self.gap;
continue;
}
let mut span = UiSpan::FULL;
span.start = start;
let from = start;
if len.leftover > Weight::ZERO && shares {
taken += len.leftover;
}
fixed.px += len.px;
fixed.rel += len.rel;
start = shared(fixed, taken, total.leftover, room);
span.end = start;
let mut region = UiRegion::from_axis(axis, span, UiSpan::FULL);
if self.dir.sign == Sign::Neg {
region.flip(axis);
}
// 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
// alignment says.
let placed = painter.widget_at(
child,
region,
UiVec2::FULL_SIZE,
[axis == Axis::X, axis == Axis::Y],
);
// Along the row the span says where the child goes; across it the
// child sits where its own alignment says. Its region is the
// whole of what this span was given either way, which is what its
// fractions are of.
let placed =
painter.widget_at(child, region, axis.pair(Some(along(from, start)), None));
if shrinks {
let used = placed.len(!axis);
// Choosing between a fixed and a relative length from the
+20 -14
View File
@@ -13,31 +13,37 @@ impl Widget for Stack {
StackSize::Default => None,
StackSize::Child(i) => Some(i),
};
// Whichever child sizes the stack decides the box every child gets.
// The stack reports that size, so a child given a longer box would
// draw outside what the stack says it occupies.
// This stack's own box, which is `FULL` until its answer is known.
let placement = painter.placement();
// Whichever child sizes the stack keeps the stack's whole region as
// its own -- the stack is the length that child asked for, so taking
// the fraction of the stack's box again would take it twice -- and is
// put where the stack itself is put.
let size = match sizing.and_then(|i| self.children.get(i).map(|c| (i, c))) {
// On the layer that child ends up on, so the ask below is a reuse
// rather than a second drawing of it somewhere else: a retained
// drawing belongs to the layer it was made on.
Some((i, child)) => {
painter.child_layer_at(i);
painter.widget(child).size()
painter
.widget_at(
child,
UiRegion::FULL,
[Some(placement.x), Some(placement.y)],
)
.size()
}
None => Size::LEFTOVER,
};
let region = painter.box_of(size);
for (i, child) in self.children.iter().enumerate() {
if sizing == Some(i) {
continue;
}
painter.child_layer_at(i);
// The sizing child placed its own content in the box its answer
// decided, and this box was derived from that answer, so applying
// its alignment again here would place it twice. Every other
// 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.
match sizing == Some(i) {
true => painter.widget_at(child, region, region.size(), [true; 2]),
false => painter.widget_within(child, region),
};
// Every other child has the stack's own box for its region, since
// the stack is what contains it, and where it sits in one bigger
// than itself is its own business.
painter.widget_within(child, placement);
}
size
}