Compare commits

..
Author SHA1 Message Date
iris-ai cadfba05dd Keep only what a room drawing is still needed for
A span measuring a child in the room kept its whole `Size`, of which the
along axis is already in `lens` and only the across one is read again when
the drawing is placed. Keep that length alone, which also retires the
rebinding of the match's result and the one in the placing loop. The
placement comment already says what becomes of a drawing made in the room,
so the measuring pass no longer says it a second time.
2026-09-19 02:51:08 -04:00
iris-ai 38b3a81053 Pin a frame by the fraction the child declared
`size_hint` resolves a child's hint against the asking widget's frame and
pins that frame, so a later draw cannot reuse a resolution made against a
different one. It asked the *resolved* hint whether it still had a fraction,
which is false whenever the frame is itself pixels -- a slot of a row, or the
box a stack's sizing child decided -- and the pin was dropped there. Ask the
declared hint, which is what made this draw depend on the frame, and what
`ruled` in `render_state` already asks for a rule.

No generated tree distinguishes the two: the fuzzer grows no `rel` rules, and
a frame that changes almost always changes a box the other pins catch. Kept
for the reason the `frame_len` pin beside it is kept -- "these two
invalidations always coincide" is an assumption nothing states.
2026-09-19 02:51:08 -04:00
5 changed files with 65 additions and 120 deletions

No files matched your search

-15
View File
@@ -73,21 +73,6 @@ impl Holds {
} }
} }
/// What a box has to be for a part of it, this many pixels shorter, to
/// stay in this range: the range moved by that much, an end that was
/// unbounded staying so.
pub const fn longer_by(self, px: Px) -> Self {
let lo = match self.lo.raw() == Px::MIN.raw() {
true => self.lo,
false => self.lo.add(px),
};
let hi = match self.hi.raw() == Px::MAX.raw() {
true => self.hi,
false => self.hi.add(px),
};
Self { lo, hi }
}
const fn raws(lo: i64, hi: i64) -> Self { const fn raws(lo: i64, hi: i64) -> Self {
Self { Self {
lo: Px::from_raw(narrow(lo)), lo: Px::from_raw(narrow(lo)),
+37 -50
View File
@@ -267,36 +267,32 @@ impl<'a> Painter<'a> {
let widgets = self.rsc.widgets(); let widgets = self.rsc.widgets();
// A rule is the answer where there is one: it wins over whatever the // A rule is the answer where there is one: it wins over whatever the
// widget would draw, so it has to win over what the widget says too. // widget would draw, so it has to win over what the widget says too.
let hint = widgets let hint = widgets.size_rules(id.id()).axis(axis).exact().or_else(|| {
.size_rules(id.id()) widgets
.axis(axis) .get_dyn(id.id())
.exact() .and_then(|widget| widget.size_hint(axis))
.or_else(|| { });
widgets let frame = self.frame.axis(axis);
.get_dyn(id.id()) let resolved = hint.map(|hint| hint.within_len(frame));
.and_then(|widget| widget.size_hint(axis))
})
.map(|hint| hint.within_len(self.frame.axis(axis)));
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
diag::hint_read(id.id(), self.id, axis, hint); {
match hint { diag::hint_read(id.id(), self.id, axis, resolved);
Some(hint) => { diag::bump(match resolved {
#[cfg(feature = "layout-diagnostics")] Some(_) => Counter::HintHits,
diag::bump(Counter::HintHits); None => Counter::HintMisses,
self.depend_on(id); });
// A fraction was just resolved against this frame, so what }
// this draw does with it is a function of the frame's length. if let Some(hint) = hint {
if hint.rel != Rel::ZERO { self.depend_on(id);
self.frame_own_len[axis as usize] = Some(self.frame.axis(axis)); // Resolving a fraction against this frame makes this draw a
} // function of the frame's length. The fraction to ask about is
Some(hint) // the child's own: resolved against a frame of pixels, none is
} // left to see it by.
None => { if hint.rel != Rel::ZERO {
#[cfg(feature = "layout-diagnostics")] self.frame_own_len[axis as usize] = Some(frame);
diag::bump(Counter::HintMisses);
None
} }
} }
resolved
} }
fn depend_on<W: ?Sized>(&mut self, child: &StrongWidget<W>) { fn depend_on<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
@@ -593,29 +589,20 @@ impl Painter<'_> {
result.extent[n] = holds.extent[n]; result.extent[n] = holds.extent[n];
result.extent_len[n] = holds.extent_len[n]; result.extent_len[n] = holds.extent_len[n];
} }
// Its box is this widget's own less the inset. Where that is // Its box is a part of this widget's own box, in that box's
// pixels, its box is exactly that many shorter in any window, // own lengths, so what it holds for maps back through that
// so what it holds for is a range on this widget's box moved // part into a range on this widget's box. A length it pinned
// by them, and a length it pinned is this widget's length // is this widget's length less the part's pixels where the
// less them. An inset with a fraction in it is a different // part is the whole of the box less pixels, which is the one
// number of pixels in each window, and taking it off a length // shape that inverts exactly; any other part pins this
// rounds once more than taking it off pixels does: there the // widget's own length.
// child's box is a fixed expression of this one, so this (Part::Of(span), false) => {
// widget's length is pinned and the range goes on the window let part_len = span.len();
// through the child's box, the way a slot's does. result.extent[n] = holds.extent[n].through(part_len);
(Part::Inset { lead, trail }, false) => { result.extent_len[n] = holds.extent_len[n].map(|pinned| match part_len.rel {
let inset = lead + trail; Rel::ONE => pinned - Len::from_parts(Rel::ZERO, part_len.px),
match inset.rel == Rel::ZERO { _ => self.extent.axis(axis).len(),
true => { });
result.extent[n] = holds.extent[n].longer_by(inset.px);
result.extent_len[n] = holds.extent_len[n].map(|pinned| pinned + inset);
}
false => {
result.window[n] = result.window[n]
.and(holds.extent[n].through(extent.axis(axis).len()));
result.extent_len[n] = Some(self.extent.axis(axis).len());
}
}
} }
// Its box is a length this widget decided, from its own // Its box is a length this widget decided, from its own
// frame or from a sibling's answer: no length of this // frame or from a sibling's answer: no length of this
+7 -8
View File
@@ -12,13 +12,12 @@ pub enum Part {
/// here is a fraction of the window and not of the box -- the whole of a /// here is a fraction of the window and not of the box -- the whole of a
/// box is [`Self::All`], not a `rel(1.0)` span. /// box is [`Self::All`], not a `rel(1.0)` span.
From(UiSpan), From(UiSpan),
/// The box less a window length at each end, which is what a container /// A part of the box in its own coordinates, which is what a container
/// that insets one speaks -- padding, or a row asking a child in the /// that insets one speaks: taking eleven pixels off the end needs no
/// room left from its cursor. Neither end names the box's length, so a /// length, where saying the same thing in window lengths would make the
/// container can say "from here to my end" without reading how long it /// container read its own box -- and a box chosen from its own answer
/// is, and a box chosen from its own answer does not feed back into the /// then feeds back into the answer.
/// answer. Of(UiSpan),
Inset { lead: Len, trail: Len },
/// A box of this length, wherever in the parent's box the child's own /// A box of this length, wherever in the parent's box the child's own
/// alignment puts it, and that same length as its frame. Unlike `From`, /// alignment puts it, and that same length as its frame. Unlike `From`,
/// it is a length decided from above rather than a place along a /// it is a length decided from above rather than a place along a
@@ -33,7 +32,7 @@ impl Part {
match self { match self {
Self::All => extent, Self::All => extent,
Self::From(span) => UiSpan::new(extent.start + span.start, extent.start + span.end), Self::From(span) => UiSpan::new(extent.start + span.start, extent.start + span.end),
Self::Inset { lead, trail } => UiSpan::new(extent.start + lead, extent.end - trail), Self::Of(span) => span.within(&extent),
Self::Sized(len) => { Self::Sized(len) => {
let start = extent.start + (extent.len() - len).scale(align.rel()); let start = extent.start + (extent.len() - len).scale(align.rel());
UiSpan::new(start, start + len) UiSpan::new(start, start + len)
+4 -4
View File
@@ -20,10 +20,10 @@ impl Widget for Pad {
// The two stay distinct -- the box can be narrower still, where a row // The two stay distinct -- the box can be narrower still, where a row
// asked this widget in the room left, and a text wraps at that. // asked this widget in the room left, and a text wraps at that.
let inset = |lead: Px, trail: Px| { let inset = |lead: Px, trail: Px| {
Place::Within(Part::Inset { Place::Within(Part::Of(UiSpan::new(
lead: Len::from_parts(Rel::ZERO, lead), Len::from_parts(Rel::ZERO, lead),
trail: Len::from_parts(Rel::ZERO, trail), Len::from_parts(Rel::ONE, -trail),
}) )))
}; };
let place = [ let place = [
inset(self.padding.left, self.padding.right), inset(self.padding.left, self.padding.right),
+17 -43
View File
@@ -10,18 +10,13 @@ pub struct Span {
impl Widget for Span { impl Widget for Span {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
let axis = self.dir.axis; let axis = self.dir.axis;
// The room left from the cursor to the row's end, said without the // The row: this span's own box, as a length of the frame its children
// row's length: a child measured in it does not make this drawing // are laid out against. Its start is nothing's business -- a slot is
// depend on how long the row is. // a length from it -- so what this reads is the length alone.
let room_from = |cursor: Len| match self.dir.sign { let far = painter.extent_len(axis);
Sign::Pos => Part::Inset { let along = |from: Len, to: Len| match self.dir.sign {
lead: cursor, Sign::Pos => UiSpan::new(from, to),
trail: Len::ZERO, Sign::Neg => UiSpan::new(far - to, far - from),
},
Sign::Neg => Part::Inset {
lead: Len::ZERO,
trail: cursor,
},
}; };
// Across itself the child sits where its own alignment says, in the // Across itself the child sits where its own alignment says, in the
// whole of the row: a span is what contains its children there, and // whole of the row: a span is what contains its children there, and
@@ -33,28 +28,24 @@ impl Widget for Span {
// given whatever else is in it and wherever this child sits among // given whatever else is in it and wherever this child sits among
// them; what a drawn child is asked in is the room left from the // them; what a drawn child is asked in is the room left from the
// cursor, because a text has to wrap at the width actually there. // cursor, because a text has to wrap at the width actually there.
// This is the one ask a drawn fixed child gets: its slot is its
// answer, and the drawing is moved there once the shares are known.
// A hinted child is asked once, in its slot.
let mut cursor = Len::rel_min(); let mut cursor = Len::rel_min();
let mut lens = Vec::with_capacity(self.children.len()); let mut lens = Vec::with_capacity(self.children.len());
let mut measured = Vec::with_capacity(self.children.len()); let mut drawn_across = Vec::with_capacity(self.children.len());
for child in &self.children { for child in &self.children {
let size = match painter.size_hint(child, axis) { let len = match painter.size_hint(child, axis) {
Some(len) => { Some(len) => {
measured.push(None); drawn_across.push(None);
len len
} }
None => { None => {
let room = Place::Within(room_from(cursor)); let room = Place::Within(Part::From(along(cursor, far)));
let size = painter let size = painter
.widget_at(child, [None; 2], axis.pair(room, across)) .widget_at(child, [None; 2], axis.pair(room, across))
.size(); .size();
measured.push(Some(size)); drawn_across.push(Some(size.axis(!axis)));
size.axis(axis) size.axis(axis)
} }
}; };
let len = size;
cursor.px += len.px + self.gap; cursor.px += len.px + self.gap;
cursor.rel += len.rel; cursor.rel += len.rel;
lens.push(len); lens.push(len);
@@ -71,25 +62,9 @@ impl Widget for Span {
|sum, len| sum + *len, |sum, len| sum + *len,
); );
// The row: this span's own box as a length of the window, read only
// where a slot depends on it -- shares divide what is left of it,
// and a negative row counts from its end. Reading it pins the
// drawing to this length; a positive row of fixed children is not
// pinned and holds for any length its children do. Its start is
// nothing's business: a slot is a length from it.
let far = (total.leftover > Weight::ZERO || self.dir.sign == Sign::Neg)
.then(|| painter.extent_len(axis));
let along = |from: Len, to: Len| match self.dir.sign {
Sign::Pos => UiSpan::new(from, to),
Sign::Neg => {
let far = far.expect("a negative row reads its length");
UiSpan::new(far - to, far - from)
}
};
// What is left for the shares to divide: the row less everything // What is left for the shares to divide: the row less everything
// fixed, as a length of the frame rather than a number of pixels. // fixed, as a length of the frame rather than a number of pixels.
// Nothing where there are no shares, and nothing reads it there. let room = far - Len::from_parts(total.rel, total.px);
let room = far.map_or(Len::ZERO, |far| far - 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. Asked of `room` // beside 300 px is full at 600 and overfull at 400. Asked of `room`
// itself, and answered back through the same expression, so the // itself, and answered back through the same expression, so the
@@ -125,8 +100,7 @@ impl Widget for Span {
let mut taken = Weight::ZERO; let mut taken = Weight::ZERO;
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), measured) in self.children.iter().zip(&lens).zip(&measured) { for ((child, &len), &across_len) in self.children.iter().zip(&lens).zip(&drawn_across) {
let len = *len;
// A child asking for nothing but a part of what is left over, // A child asking for nothing but a part of what is left over,
// when nothing is, is not drawn at all. One that also asked for // when nothing is, is not drawn at all. One that also asked for
// pixels or a fraction keeps those and overflows. // pixels or a fraction keeps those and overflows.
@@ -156,10 +130,10 @@ impl Widget for Span {
if len.leftover > Weight::ZERO && shares { if len.leftover > Weight::ZERO && shares {
narrow[axis as usize] = Some(slot.len()); narrow[axis as usize] = Some(slot.len());
} }
let used = match (measured, narrow[axis as usize]) { let used = match (across_len, narrow[axis as usize]) {
(Some(size), None) => { (Some(across_len), None) => {
painter.place_at(child, place); painter.place_at(child, place);
size.axis(!axis) across_len
} }
_ => painter.widget_at(child, narrow, place).len(!axis), _ => painter.widget_at(child, narrow, place).len(!axis),
}; };