Measure container children without intermediate placement

This commit is contained in:
iris-ai committed 2026-09-17 15:53:24 -04:00
1 parent c330ecec2b
commit 7601aa2a5d
6 files changed
+77 -34

No files matched your search

+20 -16
View File
@@ -139,7 +139,7 @@ impl<'a> Painter<'a> {
/// around one child wants, since its box is the child's. /// around one child wants, since its box is the child's.
pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> { pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> {
let own = self.placement; let own = self.placement;
self.widget_at_inner(id, UiRegion::FULL, [Some(own.x), Some(own.y)], true) self.widget_at_inner(id, UiRegion::FULL, [Some(own.x), Some(own.y)], true, false)
} }
/// What a widget's rules declare its lengths to be, which whoever draws /// What a widget's rules declare its lengths to be, which whoever draws
@@ -191,7 +191,7 @@ impl<'a> Painter<'a> {
region: UiRegion, region: UiRegion,
placement: [Option<UiSpan>; 2], placement: [Option<UiSpan>; 2],
) -> DrawResult<'s, 'a, W> { ) -> DrawResult<'s, 'a, W> {
self.widget_at_inner(id, region, placement, false) self.widget_at_inner(id, region, placement, false, false)
} }
fn widget_at_inner<'s, W: ?Sized>( fn widget_at_inner<'s, W: ?Sized>(
@@ -200,6 +200,7 @@ impl<'a> Painter<'a> {
region: UiRegion, region: UiRegion,
placement: [Option<UiSpan>; 2], placement: [Option<UiSpan>; 2],
inherited: bool, inherited: bool,
measuring: bool,
) -> DrawResult<'s, 'a, W> { ) -> DrawResult<'s, 'a, W> {
if inherited { if inherited {
if !self.inherited_children.contains(&id.id()) { if !self.inherited_children.contains(&id.id()) {
@@ -266,6 +267,7 @@ impl<'a> Painter<'a> {
placement, placement,
}, },
None, None,
measuring,
self.rsc, self.rsc,
); );
// Whatever the child's answer holds for keeps this one to the boxes // Whatever the child's answer holds for keeps this one to the boxes
@@ -321,32 +323,34 @@ impl<'a> Painter<'a> {
} }
} }
/// A child's length in the region it is about to be offered, if it can /// Measures a child's length from its hint, a retained answer, or `draw`.
/// be had without drawing it: from its hint, or from a drawing it already /// A fresh draw evaluates the offer without placing its answer. The caller
/// has that holds for that box. /// must later place or undraw the child.
pub fn known_len<W: ?Sized>( pub fn measure_len<W: ?Sized>(
&mut self, &mut self,
child: &StrongWidget<W>, child: &StrongWidget<W>,
axis: Axis, axis: Axis,
region: UiRegion, region: UiRegion,
placement: [Option<UiSpan>; 2], placement: [Option<UiSpan>; 2],
) -> Option<LayoutLen> { ) -> LayoutLen {
let offered = placement;
let declared = self.declared_lens(child); let declared = self.declared_lens(child);
let align = self.rsc.widgets().alignment(child.id()); let align = self.rsc.widgets().alignment(child.id());
let (local, placement) = ask_box(region, declared, align, placement); let (local, placement) = ask_box(region, declared, align, placement);
let first_ask = self.at_offer && !self.offered.contains(&child.id()); let first_ask = self.at_offer && !self.offered.contains(&child.id());
if let Some(hint) = self.size_hint(child, axis) { if let Some(hint) = self.size_hint(child, axis) {
return Some(hint); return hint;
} }
let px = local.size().to_px(self.px); let px = local.size().to_px(self.px);
let (size, holds) = self.state.retained_size( let retained =
child.id(), self.state
px, .retained_size(child.id(), px, placement, self.move_idx, self.rsc.widgets());
placement, let Some((size, holds)) = retained else {
self.move_idx, return self
self.rsc.widgets(), .widget_at_inner(child, region, offered, false, true)
)?; .len(axis);
};
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::RetainedSizeHits); diag::bump(Counter::RetainedSizeHits);
self.depend_on(child); self.depend_on(child);
@@ -364,7 +368,7 @@ impl<'a> Painter<'a> {
for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) { for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) {
*under = under.and(holds[axis as usize].through(local.axis(axis).len())); *under = under.and(holds[axis as usize].through(local.axis(axis).len()));
} }
Some(in_parent_frame(size, local.size(), declared).axis(axis)) in_parent_frame(size, local.size(), declared).axis(axis)
} }
/// Whether this is the first box a child is asked about in during a draw /// Whether this is the first box a child is asked about in during a draw
+11 -6
View File
@@ -193,7 +193,7 @@ impl UiRenderState {
if let Some(id) = root { if let Some(id) = root {
let region = Self::root_region(id.id(), rsc.widgets()); let region = Self::root_region(id.id(), rsc.widgets());
let info = self.root_info(region); let info = self.root_info(region);
self.draw_inner(id.id(), region, info, None, rsc); self.draw_inner(id.id(), region, info, None, false, rsc);
} }
} }
@@ -213,6 +213,7 @@ impl UiRenderState {
region: UiRegion, region: UiRegion,
info: DrawInfo, info: DrawInfo,
mut old: Option<ActiveData>, mut old: Option<ActiveData>,
measuring: bool,
rsc: &mut dyn UiRsc, rsc: &mut dyn UiRsc,
) -> (Size, LayoutHolds) { ) -> (Size, LayoutHolds) {
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
@@ -239,16 +240,20 @@ impl UiRenderState {
self.draw_at(id, region, info.offered_placement(), info, old.take(), rsc) self.draw_at(id, region, info.offered_placement(), info, old.take(), rsc)
}); });
let declared = declared_lens(rsc.widgets(), id);
// Where the drawing goes, in the region's own coordinates: what the // Where the drawing goes, in the region's own coordinates: what the
// parent chose, and on any axis it left open, what the answer took of // parent chose, and on any axis it left open, what the answer took of
// the region placed by the widget's alignment. The region itself does // the region placed by the widget's alignment. The region itself does
// not change, so nothing under it resolves a fraction a second time. // not change, so nothing under it resolves a fraction a second time.
let placement = if measuring {
info.offered_placement()
} else {
let declared = declared_lens(rsc.widgets(), id);
let lens = placed_lens(answer.0, declared, info.decided()); let lens = placed_lens(answer.0, declared, info.decided());
let own = placed_box(UiRegion::FULL, lens, align); let own = placed_box(UiRegion::FULL, lens, align);
let placement = UiRegion { UiRegion {
x: info.placement[0].unwrap_or(own.x), x: info.placement[0].unwrap_or(own.x),
y: info.placement[1].unwrap_or(own.y), y: info.placement[1].unwrap_or(own.y),
}
}; };
self.place(id, region, placement, info, rsc); self.place(id, region, placement, info, rsc);
@@ -1137,7 +1142,7 @@ impl UiRenderState {
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::LocalRedraws); diag::bump(Counter::LocalRedraws);
let old = self.remove(id, false, rsc); let old = self.remove(id, false, rsc);
self.draw_inner(id, region, info, old, rsc); self.draw_inner(id, region, info, old, false, rsc);
return true; return true;
}; };
let (given_px, offered_px) = self.asked_px(id); let (given_px, offered_px) = self.asked_px(id);
@@ -1179,9 +1184,9 @@ impl UiRenderState {
placement: info.offer_placement, placement: info.offer_placement,
..info ..info
}; };
let answer = self.draw_inner(id, given, offered, old, rsc); let answer = self.draw_inner(id, given, offered, old, false, rsc);
if info.placement != offered.placement { if info.placement != offered.placement {
self.draw_inner(id, given, info, None, rsc); self.draw_inner(id, given, info, None, false, rsc);
} }
if Some(answer) != was_answer { if Some(answer) != was_answer {
// Its parent chose its box knowing the old answer, so it lays out // Its parent chose its box knowing the old answer, so it lays out
+1 -4
View File
@@ -17,10 +17,7 @@ impl Widget for Scroll {
let whole = UiRegion::FULL; let whole = UiRegion::FULL;
let own = painter.placement(); let own = painter.placement();
let answer_len = let answer_len =
match painter.known_len(&self.inner, self.axis, whole, [Some(own.x), Some(own.y)]) { painter.measure_len(&self.inner, self.axis, whole, [Some(own.x), Some(own.y)]);
Some(len) => len,
None => painter.widget(&self.inner).size().axis(self.axis),
};
let content = answer_len.apply_leftover(); let content = answer_len.apply_leftover();
self.container_len = container_len; self.container_len = container_len;
self.content_len = content.to_px(container_len); self.content_len = content.to_px(container_len);
+1 -4
View File
@@ -37,10 +37,7 @@ impl Widget for Span {
// from the cursor, because a text has to wrap at the width // from the cursor, because a text has to wrap at the width
// actually there. // actually there.
let room = axis.pair(Some(along(cursor, far)), None); let room = axis.pair(Some(along(cursor, far)), None);
let len = match painter.known_len(child, axis, region, room) { let len = painter.measure_len(child, axis, region, room);
Some(len) => len,
None => painter.widget_at(child, region, room).len(axis),
};
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);
+37
View File
@@ -914,3 +914,40 @@ fn resizing_a_fixed_frame_recomposes_its_contents_without_drawing_them() {
assert_eq!(mask(&warm, leaf.id()), mask(&cold, other.id())); assert_eq!(mask(&warm, leaf.id()), mask(&cold, other.id()));
} }
} }
#[test]
fn a_span_does_not_place_its_measurement_before_assigning_the_childs_slot() {
struct MeasuredBox(Rc<Cell<usize>>);
impl Widget for MeasuredBox {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.0.set(self.0.get() + 1);
painter.px_size();
painter.primitive(RectPrimitive::color(Color::BLUE));
Size::from((100, 50))
}
}
let mut h = Harness::new((400, 200));
let draws = Rc::new(Cell::new(0));
let leaf = MeasuredBox(draws.clone()).add(&mut h.rsc);
h.set_root((leaf,).span(Dir::RIGHT).width(rel(1.0)).height(rel(1.0)));
assert_eq!(draws.get(), 3);
assert_corners!(h, leaf, (0, 75), (100, 125));
assert_eq!(
primitive_bounds(&h, leaf.id()),
vec![h.region(&leaf.id()).unwrap()]
);
h.frame();
assert_eq!(draws.get(), 3);
h.resize((600, 300));
h.frame();
assert_eq!(draws.get(), 6);
assert_corners!(h, leaf, (0, 125), (100, 175));
assert_eq!(
primitive_bounds(&h, leaf.id()),
vec![h.region(&leaf.id()).unwrap()]
);
}
+7 -4
View File
@@ -5,11 +5,11 @@
//! cargo test --release --features layout-diagnostics \ //! cargo test --release --features layout-diagnostics \
//! --test layout_diagnostics -- --ignored --nocapture //! --test layout_diagnostics -- --ignored --nocapture
//! //!
//! Uninstrumented hardware totals for one phase: //! Build the uninstrumented test with `cargo test --release --test
//! layout_diagnostics --no-run`, then run the emitted executable directly:
//! //!
//! IRIS_PHASE=resize IRIS_FRAMES=1000 perf stat \ //! IRIS_PHASE=resize IRIS_FRAMES=10000 perf stat -r 7 \
//! -e cycles:u,instructions:u cargo test --release \ //! -e cycles:u,instructions:u /path/to/layout_diagnostics --ignored --nocapture
//! --test layout_diagnostics -- --ignored --nocapture
//! //!
//! `IRIS_PHASE` is `cold`, `repaint`, `many`, `size`, `scroll`, `resize`, or //! `IRIS_PHASE` is `cold`, `repaint`, `many`, `size`, `scroll`, `resize`, or
//! `all`. `IRIS_SEED`, `IRIS_DEPTH`, and `IRIS_FRAMES` select the load, and //! `all`. `IRIS_SEED`, `IRIS_DEPTH`, and `IRIS_FRAMES` select the load, and
@@ -134,6 +134,9 @@ fn report(label: &str, mut elapsed: Vec<f64>, _harness: &Harness) {
{ {
let diagnostics = iris::core::layout_diagnostics::take(); let diagnostics = iris::core::layout_diagnostics::take();
print!("{}", diagnostics.per_frame(frames)); print!("{}", diagnostics.per_frame(frames));
for event in diagnostics.traces() {
println!(" {event:?}");
}
for callsite in diagnostics.hot_text().iter().take(3) { for callsite in diagnostics.hot_text().iter().take(3) {
let mut ancestry = Vec::new(); let mut ancestry = Vec::new();
let mut id = Some(callsite.id); let mut id = Some(callsite.id);