Compare commits

..
Author SHA1 Message Date
iris-ai 5b181bc8af Experiment: say the room from the cursor as an inset, and read the row's length only where a slot depends on it
Measured identical to its parent at every phase of the cost rig; kept as
evidence, not proposed for landing.
2026-09-19 02:19:14 -04:00
iris-ai f6242aa33c Take a span child's length from its hint, and ask it once in its slot
A share child was drawn in the measuring room and again in its slot, and
one record holding two questions made every local change under it defer
to the span. Where a rule or a hint gives the length along the span, the
first ask answers nothing the rule does not, so the child is asked once,
in its slot; the widgets that always report the whole of their box now
say so. A hint with a fraction resolves against the frame and pins it.

The dump rig prints every cold layout so a change to it shows in a diff.
2026-09-19 02:09:41 -04:00
iris-ai a888717ee9 Say window where these comments still say frame
Lengths became lengths of the window when the frame did, and `Part::From`'s
own documentation still described its spans as frame lengths -- which is
what the scroll above read them as.
2026-09-19 01:23:34 -04:00
iris-ai e8a5792dcb Place a scroll's fitting content in the viewport, not in the window
A scroll that has not been scrolled and whose content fits asked for its
content box as `Part::From(UiSpan::FULL)`. A `Part::From` span is in window
lengths, so `rel(1.0)` in one is the whole window rather than the whole box,
and the content landed in a window-tall box anchored at the viewport's
start -- 50 px low for a 300 px viewport in a 400 px window.

Saying the whole of the box as `Part::All` is the one expression that cannot
mean anything else, and it is also the place the child was already asked in,
so the placement becomes a no-op.
2026-09-19 01:23:34 -04:00
iris-aiandClaude Opus 5 a30971e4c5 Call the record's boxes what they are
The offer names are from the protocol before this one, where a widget was
drawn twice and the record had to say which drawing was the question. It
is asked once now, so offer_part is the part it was asked in, offer_place
the place it was asked at, and place where its drawing was put: part,
asked and placed. LayoutHolds::frame is a range on the window since the
frame became a length of one, and the frame's own entry is the frame_len
pin beside it, so it is window; Painter::frame_own goes with it.
answers_at had one caller and said less than the line that replaces it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 00:40:23 -04:00
16 changed files with 303 additions and 155 deletions

No files matched your search

+9 -19
View File
@@ -19,17 +19,17 @@ pub struct ActiveData {
/// forwards the parent's frame. What it declared is kept separately in /// forwards the parent's frame. What it declared is kept separately in
/// `declared` and is a fraction of whichever of the two reached it. /// `declared` and is a fraction of whichever of the two reached it.
pub narrow: [Option<Len>; 2], pub narrow: [Option<Len>; 2],
/// Where its drawing was put, as a part of its parent's box, and where /// Where its drawing was put, and where it was asked, each as a part of
/// it was asked. The two differ where a container asks in one place and /// its parent's box. The two differ where a container asks in one place
/// places the answer in another -- a row measures from its cursor and /// and puts the answer in another -- a row measures from its cursor and
/// puts the child in its slot. A part is a length from the box's start, /// puts the child in its slot. A part is a length from the box's start,
/// so a box that moved re-places every child by re-adding that start. /// so a box that moved re-places every child by re-adding that start.
pub place: [Place; 2], pub placed: [Place; 2],
pub offer_place: [Place; 2], pub asked: [Place; 2],
/// The box it was asked in, in the parent's region-node coordinates: the /// The box it was asked in, in the parent's region-node coordinates: the
/// box its drawing was made in and the one its contract is about. Its /// box its drawing was made in and the one its contract is about. Its
/// drawing is placed elsewhere by re-expression, never by asking again. /// drawing is placed elsewhere by re-expression, never by asking again.
pub offer_part: UiRegion, pub part: UiRegion,
/// The measured answer and its dependencies. A hint-only dependency or /// The measured answer and its dependencies. A hint-only dependency or
/// a widget first encountered during placement has no measurement yet. /// a widget first encountered during placement has no measurement yet.
pub answer: Option<(Size, LayoutHolds)>, pub answer: Option<(Size, LayoutHolds)>,
@@ -82,20 +82,10 @@ pub struct ActiveData {
} }
impl ActiveData { impl ActiveData {
/// What it answered when its parent measured it, where it has been /// What it answered when its parent asked, where it has been asked at
/// measured at all. Not `size`, which is what its last drawing reported: /// all. Not `size`, which is what its last drawing reported: a drawing
/// a drawing made in the box that answer chose is answering a different /// re-expressed in the box that answer chose is not a second answer.
/// question.
pub fn measured(&self) -> Option<Size> { pub fn measured(&self) -> Option<Size> {
self.answer.map(|(size, _)| size) self.answer.map(|(size, _)| size)
} }
/// Whether what it answered still stands in this window, for the frame
/// and the box it was asked in. 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, window: crate::PxVec2, part: UiRegion) -> bool {
self.answer
.is_some_and(|(_, holds)| holds.contains(window, self.frame, part))
}
} }
+15
View File
@@ -73,6 +73,21 @@ 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)),
+6 -6
View File
@@ -20,7 +20,7 @@ const AXES: [Axis; 2] = [Axis::X, Axis::Y];
/// whatever the frame turns out to be. /// whatever the frame turns out to be.
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub struct LayoutHolds { pub struct LayoutHolds {
pub frame: [Holds; 2], pub window: [Holds; 2],
pub frame_len: [Option<Len>; 2], pub frame_len: [Option<Len>; 2],
pub extent: [Holds; 2], pub extent: [Holds; 2],
pub extent_len: [Option<Len>; 2], pub extent_len: [Option<Len>; 2],
@@ -28,7 +28,7 @@ pub struct LayoutHolds {
impl LayoutHolds { impl LayoutHolds {
pub const ANY: Self = Self { pub const ANY: Self = Self {
frame: [Holds::ANY; 2], window: [Holds::ANY; 2],
frame_len: [None; 2], frame_len: [None; 2],
extent: [Holds::ANY; 2], extent: [Holds::ANY; 2],
extent_len: [None; 2], extent_len: [None; 2],
@@ -37,7 +37,7 @@ impl LayoutHolds {
pub fn and(self, other: Self) -> Self { pub fn and(self, other: Self) -> Self {
let mut result = Self::ANY; let mut result = Self::ANY;
for n in 0..2 { for n in 0..2 {
result.frame[n] = self.frame[n].and(other.frame[n]); result.window[n] = self.window[n].and(other.window[n]);
result.extent[n] = self.extent[n].and(other.extent[n]); result.extent[n] = self.extent[n].and(other.extent[n]);
debug_assert!( debug_assert!(
self.extent_len[n].is_none() self.extent_len[n].is_none()
@@ -57,8 +57,8 @@ impl LayoutHolds {
pub fn covers(self, other: Self) -> bool { pub fn covers(self, other: Self) -> bool {
(0..2).all(|n| { (0..2).all(|n| {
self.frame[n].lo <= other.frame[n].lo self.window[n].lo <= other.window[n].lo
&& self.frame[n].hi >= other.frame[n].hi && self.window[n].hi >= other.window[n].hi
&& self.extent[n].lo <= other.extent[n].lo && self.extent[n].lo <= other.extent[n].lo
&& self.extent[n].hi >= other.extent[n].hi && self.extent[n].hi >= other.extent[n].hi
&& self.extent_len[n].is_none_or(|len| other.extent_len[n] == Some(len)) && self.extent_len[n].is_none_or(|len| other.extent_len[n] == Some(len))
@@ -70,7 +70,7 @@ impl LayoutHolds {
AXES.into_iter().all(|axis| { AXES.into_iter().all(|axis| {
let n = axis as usize; let n = axis as usize;
let len = extent.axis(axis).len(); let len = extent.axis(axis).len();
self.frame[n].contains(window.axis(axis)) self.window[n].contains(window.axis(axis))
&& self.frame_len[n].is_none_or(|pinned| pinned == frame.axis(axis)) && self.frame_len[n].is_none_or(|pinned| pinned == frame.axis(axis))
&& self.extent[n].contains(len.to_px(window.axis(axis))) && self.extent[n].contains(len.to_px(window.axis(axis)))
&& self.extent_len[n].is_none_or(|pinned| pinned == len) && self.extent_len[n].is_none_or(|pinned| pinned == len)
+61 -38
View File
@@ -42,7 +42,7 @@ pub struct Painter<'a> {
pub(super) size_deps: Vec<WidgetId>, pub(super) size_deps: Vec<WidgetId>,
/// What this draw itself read of the window in pixels, per axis: every /// What this draw itself read of the window in pixels, per axis: every
/// window until it reads one, then that one, unless it says otherwise. /// window until it reads one, then that one, unless it says otherwise.
pub(super) frame_own: [Holds; 2], pub(super) window_own: [Holds; 2],
/// Its frame's symbolic length where this draw read it, which makes the /// Its frame's symbolic length where this draw read it, which makes the
/// drawing one that holds for that frame alone. /// drawing one that holds for that frame alone.
pub(super) frame_own_len: [Option<Len>; 2], pub(super) frame_own_len: [Option<Len>; 2],
@@ -192,8 +192,8 @@ impl<'a> Painter<'a> {
mask: self.mask, mask: self.mask,
frame, frame,
part: extent, part: extent,
place, placed: place,
offer_place: place, asked: place,
narrow, narrow,
re_asked, re_asked,
px, px,
@@ -259,17 +259,24 @@ impl<'a> Painter<'a> {
declared_lens(self.rsc.widgets(), id.id()) declared_lens(self.rsc.widgets(), id.id())
} }
/// What a child says its length is without being drawn, if it can say. /// What a child says its length is without being drawn, if it can say,
/// Asking counts as reading its size. /// as the length its draw would report: a fraction in it is resolved
/// against this widget's frame, which is the frame a child asked with
/// nothing narrowed gets. Asking counts as reading its size.
pub fn size_hint<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<LayoutLen> { pub fn size_hint<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<LayoutLen> {
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.size_rules(id.id()).axis(axis).exact().or_else(|| { let hint = widgets
widgets .size_rules(id.id())
.get_dyn(id.id()) .axis(axis)
.and_then(|widget| widget.size_hint(axis)) .exact()
}); .or_else(|| {
widgets
.get_dyn(id.id())
.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); diag::hint_read(id.id(), self.id, axis, hint);
match hint { match hint {
@@ -277,6 +284,11 @@ impl<'a> Painter<'a> {
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::HintHits); diag::bump(Counter::HintHits);
self.depend_on(id); 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 hint.rel != Rel::ZERO {
self.frame_own_len[axis as usize] = Some(self.frame.axis(axis));
}
Some(hint) Some(hint)
} }
None => { None => {
@@ -429,7 +441,7 @@ impl<'a> Painter<'a> {
pub fn to_px(&mut self, len: Len, axis: Axis) -> Px { pub fn to_px(&mut self, len: Len, axis: Axis) -> Px {
let window = self.window.axis(axis); let window = self.window.axis(axis);
if len.rel != Rel::ZERO { if len.rel != Rel::ZERO {
let own = &mut self.frame_own[axis as usize]; let own = &mut self.window_own[axis as usize];
if *own == Holds::ANY { if *own == Holds::ANY {
*own = Holds::at(window); *own = Holds::at(window);
} }
@@ -437,8 +449,10 @@ impl<'a> Painter<'a> {
len.to_px(window) len.to_px(window)
} }
/// A validity range already stated about the window. Containers use /// The windows this drawing holds for, stated rather than taken: a
/// this after branching on a window-unit length. /// container that branched on a length in pixels says which side of the
/// boundary it was on, which is wider than the one window reading that
/// length pins, and replaces it.
pub fn window_holds(&mut self, axis: Axis, holds: impl Into<Holds>) { pub fn window_holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
let holds = holds.into(); let holds = holds.into();
debug_assert!( debug_assert!(
@@ -447,7 +461,7 @@ impl<'a> Painter<'a> {
self.label(), self.label(),
self.id self.id
); );
self.frame_own[axis as usize] = holds; self.window_own[axis as usize] = holds;
} }
pub fn text_data(&mut self) -> &mut TextData { pub fn text_data(&mut self) -> &mut TextData {
@@ -539,7 +553,7 @@ impl PrimitiveLike for &TextureHandle {
/// method's `impl` block is where a `Painter`'s own boxes are, so it takes /// method's `impl` block is where a `Painter`'s own boxes are, so it takes
/// only what the child was asked with. /// only what the child was asked with.
impl Painter<'_> { impl Painter<'_> {
/// Frame ranges are already ranges on the window and combine directly. /// Window ranges are already about the one unit and combine directly.
/// A frame pin becomes this widget's own frame wherever a length of it /// A frame pin becomes this widget's own frame wherever a length of it
/// is what reached the child; where only pixels did, no length of this /// is what reached the child; where only pixels did, no length of this
/// frame can change the child's and the pin stops here. /// frame can change the child's and the pin stops here.
@@ -561,9 +575,9 @@ impl Painter<'_> {
let mut result = LayoutHolds::ANY; let mut result = LayoutHolds::ANY;
for axis in AXES { for axis in AXES {
let n = axis as usize; let n = axis as usize;
// Every frame range is already a range on the window: the // Every read became pixels against the window, so a range on
// widget's own read converted through its frame exactly once. // it is already in this widget's terms.
result.frame[n] = holds.frame[n]; result.window[n] = holds.window[n];
let reaches = narrow[n].is_none() let reaches = narrow[n].is_none()
&& !matches!(place[n].part(), Part::Sized(_)) && !matches!(place[n].part(), Part::Sized(_))
&& declared[n].is_none_or(|len| len.rel != Rel::ZERO); && declared[n].is_none_or(|len| len.rel != Rel::ZERO);
@@ -579,28 +593,37 @@ 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 a part of this widget's own box, in that box's // Its box is this widget's own less the inset. Where that is
// own lengths, so what it holds for maps back through that // pixels, its box is exactly that many shorter in any window,
// part into a range on this widget's box. A length it pinned // so what it holds for is a range on this widget's box moved
// is this widget's length less the part's pixels where the // by them, and a length it pinned is this widget's length
// part is the whole of the box less pixels, which is the one // less them. An inset with a fraction in it is a different
// shape that inverts exactly; any other part pins this // number of pixels in each window, and taking it off a length
// widget's own length. // rounds once more than taking it off pixels does: there the
(Part::Of(span), false) => { // child's box is a fixed expression of this one, so this
let part_len = span.len(); // widget's length is pinned and the range goes on the window
result.extent[n] = holds.extent[n].through(part_len); // through the child's box, the way a slot's does.
result.extent_len[n] = holds.extent_len[n].map(|pinned| match part_len.rel { (Part::Inset { lead, trail }, false) => {
Rel::ONE => pinned - Len::from_parts(Rel::ZERO, part_len.px), let inset = lead + trail;
_ => self.extent.axis(axis).len(), match inset.rel == Rel::ZERO {
}); 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 part of this widget's frame, or a length of // Its box is a length this widget decided, from its own
// it decided here: a length of the frame is all that reaches // frame or from a sibling's answer: no length of this
// it, so what it holds for is a range on the frame and none // widget's box reaches it, so what it holds for is a range
// of it on this widget's own box. // on the window and none of it on that box.
_ => { _ => {
result.frame[n] = result.window[n] =
result.frame[n].and(holds.extent[n].through(extent.axis(axis).len())); result.window[n].and(holds.extent[n].through(extent.axis(axis).len()));
} }
} }
} }
+14 -11
View File
@@ -5,17 +5,20 @@ use crate::{AxisAlign, Len, PrimitiveHandle, UiRegion, UiSpan};
pub enum Part { pub enum Part {
/// The whole of it. /// The whole of it.
All, All,
/// Frame lengths from where the box starts, which is what a container /// Window lengths from where the box starts, which is what a container
/// dividing room among its children speaks: a child's report is a length /// dividing room among its children speaks: a child's report is a window
/// of the frame, so the cursor that sums those reports is one too. A /// length, so the cursor that sums those reports is one too. A moved box
/// moved box re-places every child by re-adding its start, exactly. /// re-places every child by re-adding its start, exactly. A fraction
/// 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.
From(UiSpan), From(UiSpan),
/// A part of the box in its own coordinates, which is what a container /// The box less a window length at each end, which is what a container
/// that insets one speaks: taking eleven pixels off the end needs no /// that insets one speaks -- padding, or a row asking a child in the
/// length, where saying the same thing in frame lengths would make the /// room left from its cursor. Neither end names the box's length, so a
/// container read its own box -- and a box chosen from its own answer /// container can say "from here to my end" without reading how long it
/// then feeds back into the answer. /// is, and a box chosen from its own answer does not feed back into the
Of(UiSpan), /// answer.
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
@@ -30,7 +33,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::Of(span) => span.within(&extent), Self::Inset { lead, trail } => UiSpan::new(extent.start + lead, extent.end - trail),
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)
+37 -33
View File
@@ -28,9 +28,9 @@ pub(super) struct DrawInfo {
pub part: UiRegion, pub part: UiRegion,
/// Where the widget is put, and where it was asked, as parts of the /// Where the widget is put, and where it was asked, as parts of the
/// parent's box. See [`Place`]. The two are one ask's place until the /// parent's box. See [`Place`]. The two are one ask's place until the
/// parent places the answer somewhere else. /// parent puts the answer somewhere else.
pub place: [Place; 2], pub placed: [Place; 2],
pub offer_place: [Place; 2], pub asked: [Place; 2],
/// A frame the parent decided for it on each axis, as a length of the /// A frame the parent decided for it on each axis, as a length of the
/// window, which the widget's own declaration is a fraction of. /// window, which the widget's own declaration is a fraction of.
pub narrow: [Option<Len>; 2], pub narrow: [Option<Len>; 2],
@@ -44,7 +44,7 @@ impl DrawInfo {
/// The axes where the part is the drawing's box outright, which are the /// The axes where the part is the drawing's box outright, which are the
/// axes the answer is not placed inside it again. /// axes the answer is not placed inside it again.
fn fill(&self) -> [bool; 2] { fn fill(&self) -> [bool; 2] {
self.place.map(Place::fills) self.placed.map(Place::fills)
} }
} }
@@ -120,9 +120,13 @@ impl UiRenderState {
let Some(root) = self.old_root else { return }; let Some(root) = self.old_root else { return };
let stands = self.active.get(&root).is_some_and(|active| { let stands = self.active.get(&root).is_some_and(|active| {
// Nothing above the root chose anything, so the box it was first // Nothing above the root chose anything, so the box it was first
// asked about is the whole of its frame. // asked about is the whole of its frame. Both its answer and its
active.answers_at(size, active.offer_part) // drawing have to stand in the new window, since nothing above
&& active.holds.contains(size, active.frame, active.offer_part) // it will ask either again.
let answer = active
.answer
.is_some_and(|(_, holds)| holds.contains(size, active.frame, active.part));
answer && active.holds.contains(size, active.frame, active.part)
}); });
if !stands { if !stands {
widgets.needs_redraw.insert(root); widgets.needs_redraw.insert(root);
@@ -142,8 +146,8 @@ impl UiRenderState {
mask: MaskIdx::NONE, mask: MaskIdx::NONE,
frame, frame,
part: extent, part: extent,
place: [Place::Within(Part::All); 2], placed: [Place::Within(Part::All); 2],
offer_place: [Place::Within(Part::All); 2], asked: [Place::Within(Part::All); 2],
narrow: [None; 2], narrow: [None; 2],
re_asked: false, re_asked: false,
px, px,
@@ -267,9 +271,9 @@ impl UiRenderState {
active.narrow = info.narrow; active.narrow = info.narrow;
active.re_asked = info.re_asked; active.re_asked = info.re_asked;
active.answer = Some(answer); active.answer = Some(answer);
active.offer_place = info.offer_place; active.asked = info.asked;
active.offer_part = part; active.part = part;
active.place = info.place; active.placed = info.placed;
active.own_align = align; active.own_align = align;
// A subtree can be reused whole under a different parent -- same box, // A subtree can be reused whole under a different parent -- same box,
// same layer, same region node -- and nothing in the drawing says it // same layer, same region node -- and nothing in the drawing says it
@@ -328,7 +332,7 @@ impl UiRenderState {
mask_region: None, mask_region: None,
children: Vec::new(), children: Vec::new(),
size_deps: Vec::new(), size_deps: Vec::new(),
frame_own: [Holds::ANY; 2], window_own: [Holds::ANY; 2],
frame_own_len: [None; 2], frame_own_len: [None; 2],
under: Vec::new(), under: Vec::new(),
extent_own: [Holds::ANY; 2], extent_own: [Holds::ANY; 2],
@@ -364,7 +368,7 @@ impl UiRenderState {
answer_under, answer_under,
children, children,
size_deps, size_deps,
frame_own, window_own,
frame_own_len, frame_own_len,
under, under,
move_idx, move_idx,
@@ -435,7 +439,7 @@ impl UiRenderState {
} }
}); });
let own_holds = LayoutHolds { let own_holds = LayoutHolds {
frame: frame_own, window: window_own,
frame_len, frame_len,
extent: extent_own, extent: extent_own,
extent_len, extent_len,
@@ -465,8 +469,8 @@ impl UiRenderState {
mask, mask,
frame: UiVec2::FULL_SIZE, frame: UiVec2::FULL_SIZE,
part: UiRegion::FULL, part: UiRegion::FULL,
place: [Place::Within(Part::All); 2], placed: [Place::Within(Part::All); 2],
offer_place: [Place::Within(Part::All); 2], asked: [Place::Within(Part::All); 2],
narrow: [None; 2], narrow: [None; 2],
re_asked: false, re_asked: false,
px, px,
@@ -482,9 +486,9 @@ impl UiRenderState {
extent, extent,
frame: info.frame, frame: info.frame,
narrow: info.narrow, narrow: info.narrow,
place: info.place, placed: info.placed,
offer_place: info.offer_place, asked: info.asked,
offer_part: extent, part: extent,
// Whoever asked writes the answer. // Whoever asked writes the answer.
answer: None, answer: None,
re_asked: info.re_asked, re_asked: info.re_asked,
@@ -626,7 +630,7 @@ impl UiRenderState {
if holds.extent_len[n].is_some_and(|pinned| pinned != part.axis(axis).len()) { if holds.extent_len[n].is_some_and(|pinned| pinned != part.axis(axis).len()) {
diag::bump(Counter::OutsidePinnedLen); diag::bump(Counter::OutsidePinnedLen);
} }
if !holds.frame[n].contains(self.output_size.axis(axis)) if !holds.window[n].contains(self.output_size.axis(axis))
|| holds.frame_len[n].is_some_and(|pinned| pinned != info.frame.axis(axis)) || holds.frame_len[n].is_some_and(|pinned| pinned != info.frame.axis(axis))
{ {
diag::bump(Counter::OutsideFrame); diag::bump(Counter::OutsideFrame);
@@ -672,7 +676,7 @@ impl UiRenderState {
self.redepth(id, info.depth); self.redepth(id, info.depth);
let active = self.active.get_mut(&id).unwrap(); let active = self.active.get_mut(&id).unwrap();
active.frame = info.frame; active.frame = info.frame;
active.place = info.place; active.placed = info.placed;
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
{ {
match (moved, has_region_node) { match (moved, has_region_node) {
@@ -697,7 +701,7 @@ impl UiRenderState {
/// Places one child of `at.id` where that widget's own box now has it. /// Places one child of `at.id` where that widget's own box now has it.
fn place_child(&mut self, child: WidgetId, at: &Placing, rsc: &mut dyn UiRsc) { fn place_child(&mut self, child: WidgetId, at: &Placing, rsc: &mut dyn UiRsc) {
let place = self.active[&child].place; let place = self.active[&child].placed;
self.place_in(child, at, place, rsc); self.place_in(child, at, place, rsc);
} }
@@ -729,8 +733,8 @@ impl UiRenderState {
mask: at.mask, mask: at.mask,
frame, frame,
part, part,
place, placed: place,
offer_place: active.offer_place, asked: active.asked,
narrow: active.narrow, narrow: active.narrow,
re_asked: active.re_asked, re_asked: active.re_asked,
px: frame.to_px(at.window), px: frame.to_px(at.window),
@@ -874,9 +878,9 @@ impl UiRenderState {
extent: UiRegion::FULL, extent: UiRegion::FULL,
frame: UiVec2::FULL_SIZE, frame: UiVec2::FULL_SIZE,
narrow: [None; 2], narrow: [None; 2],
place: [Place::Within(Part::All); 2], placed: [Place::Within(Part::All); 2],
offer_place: [Place::Within(Part::All); 2], asked: [Place::Within(Part::All); 2],
offer_part: UiRegion::FULL, part: UiRegion::FULL,
answer: None, answer: None,
re_asked: false, re_asked: false,
size, size,
@@ -1113,14 +1117,14 @@ impl UiRenderState {
self.draw_inner(id, info, old, rsc); self.draw_inner(id, info, old, rsc);
return true; return true;
}; };
let (was_answer, was_holds, was_place) = (active.answer, active.holds, active.place); let (was_answer, was_holds, was_place) = (active.answer, active.holds, active.placed);
// The question its parent asked, asked again: the same place of the // The question its parent asked, asked again: the same place of the
// box the parent was asked in, which is the box the parent's own // box the parent was asked in, which is the box the parent's own
// draw ran in and what its children's parts are of. Where the // draw ran in and what its children's parts are of. Where the
// parent's answer put its own drawing is not a question anybody // parent's answer put its own drawing is not a question anybody
// asked, and nothing is asked in it here either. // asked, and nothing is asked in it here either.
let asked = self.placing_of(parent, self.active[&parent].offer_part); let parent_at = self.placing_of(parent, self.active[&parent].part);
let (frame, part) = Self::ask_again(active, &asked, active.offer_place); let (frame, part) = Self::ask_again(active, &parent_at, active.asked);
let info = DrawInfo { let info = DrawInfo {
layer: active.layer, layer: active.layer,
parent: active.parent, parent: active.parent,
@@ -1130,8 +1134,8 @@ impl UiRenderState {
mask: active.parent_mask, mask: active.parent_mask,
frame, frame,
part, part,
place: active.offer_place, placed: active.asked,
offer_place: active.offer_place, asked: active.asked,
narrow: active.narrow, narrow: active.narrow,
re_asked: false, re_asked: false,
px: frame.to_px(self.output_size), px: frame.to_px(self.output_size),
+4
View File
@@ -142,6 +142,10 @@ impl Widget for Branch {
}; };
Size::LEFTOVER Size::LEFTOVER
} }
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
Some(LayoutLen::LEFTOVER)
}
} }
pub struct Spanned { pub struct Spanned {
+4
View File
@@ -15,4 +15,8 @@ impl Widget for Masked {
// draw, and the framework would place the drawing it clipped away. // draw, and the framework would place the drawing it clipped away.
Size::LEFTOVER Size::LEFTOVER
} }
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
Some(LayoutLen::LEFTOVER)
}
} }
+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::Of(UiSpan::new( Place::Within(Part::Inset {
Len::from_parts(Rel::ZERO, lead), lead: Len::from_parts(Rel::ZERO, lead),
Len::from_parts(Rel::ONE, -trail), trail: Len::from_parts(Rel::ZERO, trail),
))) })
}; };
let place = [ let place = [
inset(self.padding.left, self.padding.right), inset(self.padding.left, self.padding.right),
+12 -7
View File
@@ -44,7 +44,6 @@ impl Widget for Scroll {
// have placed the whole scroll in a box longer than it. // have placed the whole scroll in a box longer than it.
let slack = (self.container_len - self.content_len).max(Px::ZERO); let slack = (self.container_len - self.content_len).max(Px::ZERO);
let anchor = slack.mul(align.rel()); let anchor = slack.mul(align.rel());
let mut content = UiSpan::FULL;
// Content that fills the viewport and has not been scrolled is the // Content that fills the viewport and has not been scrolled is the
// viewport, and is handed back as it came. Writing the same box as // viewport, and is handed back as it came. Writing the same box as
// its own length in pixels is the same box in another form, and the // its own length in pixels is the same box in another form, and the
@@ -52,18 +51,20 @@ impl Widget for Scroll {
// one centred in `px 900`, since halving a difference is not halving // one centred in `px 900`, since halving a difference is not halving
// each part of it. // each part of it.
let moved = anchor != Px::ZERO || self.amt != Px::ZERO; let moved = anchor != Px::ZERO || self.amt != Px::ZERO;
if moved || self.content_len != self.container_len { let content = match moved || self.content_len != self.container_len {
let start = Len::from_parts(Rel::ZERO, anchor - self.amt); true => {
content = UiSpan::new(start, start.offset(self.content_len)); let start = Len::from_parts(Rel::ZERO, anchor - self.amt);
} Part::From(UiSpan::new(start, start.offset(self.content_len)))
}
false => Part::All,
};
// The viewport is the inner's frame, so a fraction it declares or // The viewport is the inner's frame, so a fraction it declares or
// reports is a fraction of what is on screen rather than of the // reports is a fraction of what is on screen rather than of the
// content box its own answer decided. Where it goes is the content // content box its own answer decided. Where it goes is the content
// box, scrolled: its drawing moved there, not made again there. // box, scrolled: its drawing moved there, not made again there.
painter.place_at( painter.place_at(
&self.inner, &self.inner,
self.axis self.axis.pair(Place::Fill(content), Place::Fill(Part::All)),
.pair(Place::Fill(Part::From(content)), Place::Fill(Part::All)),
); );
// 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
@@ -71,6 +72,10 @@ impl Widget for Scroll {
// is. // is.
Size::LEFTOVER Size::LEFTOVER
} }
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
Some(LayoutLen::LEFTOVER)
}
} }
impl Scroll { impl Scroll {
+67 -34
View File
@@ -10,38 +10,55 @@ 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 row: this span's own box, as a length of the frame its children // The room left from the cursor to the row's end, said without the
// are laid out against. Its start is nothing's business -- a slot is // row's length: a child measured in it does not make this drawing
// a length from it -- so what this reads is the length alone. // depend on how long the row is.
let far = painter.extent_len(axis); let room_from = |cursor: Len| match self.dir.sign {
let along = |from: Len, to: Len| match self.dir.sign { Sign::Pos => Part::Inset {
Sign::Pos => UiSpan::new(from, to), lead: cursor,
Sign::Neg => UiSpan::new(far - to, far - from), trail: Len::ZERO,
},
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
// nothing divides that axis. // nothing divides that axis.
let across = Place::Within(Part::All); let across = Place::Within(Part::All);
// A length for every child before their final slots are chosen. The // A length for every child before their final slots are chosen: from
// frame passes through unchanged, so `rel(0.5)` is half the area this // a hint where one says, and from drawing otherwise. The frame passes
// span was given whatever else is in it and wherever this child sits // through unchanged, so `rel(0.5)` is half the area this span was
// among them; what it is asked in is the room left from the cursor, // given whatever else is in it and wherever this child sits among
// because a text has to wrap at the width actually there. This is // them; what a drawn child is asked in is the room left from the
// the one ask a fixed child gets: its slot is its answer, and the // cursor, because a text has to wrap at the width actually there.
// drawing is moved there once the shares are known. // 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 sizes = Vec::with_capacity(self.children.len()); let mut lens = Vec::with_capacity(self.children.len());
let mut measured = Vec::with_capacity(self.children.len());
for child in &self.children { for child in &self.children {
let room = Place::Within(Part::From(along(cursor, far))); let size = match painter.size_hint(child, axis) {
let size = painter Some(len) => {
.widget_at(child, [None; 2], axis.pair(room, across)) measured.push(None);
.size(); len
let len = size.axis(axis); }
None => {
let room = Place::Within(room_from(cursor));
let size = painter
.widget_at(child, [None; 2], axis.pair(room, across))
.size();
measured.push(Some(size));
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;
sizes.push(size); lens.push(len);
} }
let lens: Vec<LayoutLen> = sizes.iter().map(|size| size.axis(axis)).collect();
let gaps = self let gaps = self
.gap .gap
@@ -54,9 +71,25 @@ 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.
let room = far - Len::from_parts(total.rel, total.px); // Nothing where there are no shares, and nothing reads it there.
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
@@ -92,8 +125,8 @@ 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, size) in self.children.iter().zip(&sizes) { for ((child, len), measured) in self.children.iter().zip(&lens).zip(&measured) {
let len = size.axis(axis); 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.
@@ -115,20 +148,20 @@ impl Widget for Span {
// answer inside again. A share is decided here and nowhere // answer inside again. A share is decided here and nowhere
// else: its slot narrows its frame, and the child is asked in // else: its slot narrows its frame, and the child is asked in
// it, since a text wraps at the width it is actually given. A // it, since a text wraps at the width it is actually given. A
// fixed child's slot is its own answer, so its drawing is put // fixed child's slot is its own answer, so a drawing made in the
// there as it is. // room is put there as it is, and one not made yet is made here.
let slot = along(from, start); let slot = along(from, start);
let place = axis.pair(Place::Fill(Part::From(slot)), across); let place = axis.pair(Place::Fill(Part::From(slot)), across);
let used = match len.leftover > Weight::ZERO && shares { let mut narrow = [None; 2];
true => { if len.leftover > Weight::ZERO && shares {
let mut narrow = [None; 2]; narrow[axis as usize] = Some(slot.len());
narrow[axis as usize] = Some(slot.len()); }
painter.widget_at(child, narrow, place).len(!axis) let used = match (measured, narrow[axis as usize]) {
} (Some(size), None) => {
false => {
painter.place_at(child, place); painter.place_at(child, place);
size.axis(!axis) size.axis(!axis)
} }
_ => painter.widget_at(child, narrow, place).len(!axis),
}; };
if shrinks { if shrinks {
// Choosing between a fixed and a relative length from the // Choosing between a fixed and a relative length from the
+9
View File
@@ -49,6 +49,15 @@ impl Widget for Stack {
} }
size size
} }
/// Without a sizing child a stack is whatever box it is given, which it
/// can say without drawing anything.
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
match self.size {
StackSize::Default => Some(LayoutLen::LEFTOVER),
StackSize::Child(_) => None,
}
}
} }
#[derive(Default, Debug)] #[derive(Default, Debug)]
+2 -2
View File
@@ -124,7 +124,7 @@ fn padding_keeps_the_frame_distinct_from_the_room_left_in_a_row() {
h.set_root((icon, padded).span(Dir::RIGHT).width(rel(1.0))); h.set_root((icon, padded).span(Dir::RIGHT).width(rel(1.0)));
let active = &h.render.active[&text.id()]; let active = &h.render.active[&text.id()];
let window = h.render.output_size().x; let window = h.render.output_size().x;
let asked = active.offer_part.x.len().to_px(window); let asked = active.part.x.len().to_px(window);
assert_eq!(active.frame.x.to_px(window), Px::from_int(868)); assert_eq!(active.frame.x.to_px(window), Px::from_int(868));
assert_eq!(asked, Px::from_int(844)); assert_eq!(asked, Px::from_int(844));
} }
@@ -171,7 +171,7 @@ fn padding_narrows_both_frame_and_box_inside_a_share() {
let active = &h.render.active[&text.id()]; let active = &h.render.active[&text.id()];
let window = h.render.output_size().x; let window = h.render.output_size().x;
assert_eq!(active.frame.x.to_px(window), Px::from_int(418)); assert_eq!(active.frame.x.to_px(window), Px::from_int(418));
assert_eq!(active.offer_part.x.len().to_px(window), Px::from_int(418)); assert_eq!(active.part.x.len().to_px(window), Px::from_int(418));
} }
#[test] #[test]
+16
View File
@@ -145,3 +145,19 @@ fn a_clipping_widget_reporting_more_than_its_box_is_caught() {
h.set_root(clipper); h.set_root(clipper);
h.frame(); h.frame();
} }
/// Content that fits sits in the viewport, not in a box of the window's
/// length anchored at the viewport's start. `Part::From` takes window
/// lengths, so a `rel(1.0)` span in one is the window, and only a scroll
/// filling the window would land right.
#[test]
fn content_that_fits_is_placed_in_the_viewport_and_not_in_the_window() {
let mut h = Harness::new((400, 400));
let head = rect(Color::RED).height(100).add(&mut h.rsc);
let inner = rect(Color::BLUE).height(50).add(&mut h.rsc);
let scroll = Scroll::new(inner.add_strong(&mut h.rsc), Axis::Y).add(&mut h.rsc);
h.set_root((head, scroll).span(Dir::DOWN));
assert_corners!(h, scroll, (0, 100), (400, 400));
assert_corners!(h, inner, (0, 225), (400, 275));
}
+42
View File
@@ -0,0 +1,42 @@
//! Prints where a cold layout puts every widget of many grown trees, so two
//! commits can be compared on cold layout alone. The warm/cold oracle cannot
//! see a change that moves cold layout, since both of its sides move; this
//! can, by diffing its output across the change:
//!
//! IRIS_DUMP_SEEDS=400 IRIS_DUMP_DEPTH=5 cargo test --release \
//! --test layout_dump -- --ignored --nocapture > /tmp/before.txt
//!
//! then the same after, and `diff` the two. A line is one widget: the seed,
//! its index in creation order, and its box in window pixels, or `-` where
//! it is not drawn.
use iris::harness::Harness;
use iris::random::{Edits, grow};
fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
std::env::var(name)
.ok()
.and_then(|value| value.parse().ok())
.unwrap_or(fallback)
}
#[test]
#[ignore = "a dump to diff across commits, not a check"]
fn every_cold_layout_is_printed() {
let seeds = env("IRIS_DUMP_SEEDS", 400_u64);
let depth = env("IRIS_DUMP_DEPTH", 5_usize);
let mut out = String::new();
for seed in 1..=seeds {
let mut harness = Harness::new((1920.0, 1200.0));
let (root, tree) = grow(&mut harness.rsc, seed, depth, &Edits::default());
harness.state.root = Some(root);
harness.frame();
for (index, id) in tree.ids.iter().enumerate() {
match harness.region(id) {
Some(region) => out.push_str(&format!("{seed} {index} {region:?}\n")),
None => out.push_str(&format!("{seed} {index} -\n")),
}
}
}
print!("{out}");
}
+1 -1
View File
@@ -407,7 +407,7 @@ fn record(id: WidgetId, h: &Harness) -> String {
let active = &h.render.active[&id]; let active = &h.render.active[&id];
format!( format!(
"frame {} ask {} box {} size {}", "frame {} ask {} box {} size {}",
active.frame, active.offer_part, active.extent, active.size, active.frame, active.part, active.extent, active.size,
) )
} }