Make Iris layout dependencies explicit

This commit is contained in:
iris committed 2026-09-09 22:35:03 -04:00
1 parent 2bc0ff1866
commit 3ae034a47b
25 files changed
+609 -186

No files matched your search

+5 -1
View File
@@ -11,12 +11,16 @@ pub struct ActiveData {
pub textures: Vec<TextureHandle>,
pub primitives: Vec<PrimitiveHandle>,
pub children: Vec<WidgetId>,
/// Direct children whose reported size this widget used during its
/// latest draw. Dirtiness propagates across these edges before layout
/// starts, so the resulting draw still travels only parent to child.
pub size_dependencies: Vec<WidgetId>,
/// The inherited mask, not `own_mask`.
pub mask: MaskIdx,
/// The widget's retained mask slot, or `MaskIdx::NONE`.
pub own_mask: MaskIdx,
pub layer: LayerId,
/// The last `Widget::draw` result.
/// The size recorded by the last `Widget::draw` through its painter.
pub size: Size,
/// Retained so descendants' parent links stay valid across redraws.
pub move_slot: MoveIdx,
+1 -1
View File
@@ -9,7 +9,7 @@ mod render_state;
pub use access::*;
pub use active::*;
pub use painter::Painter;
pub use painter::{DrawResult, Painter};
pub use render_state::*;
#[derive(Default)]
+92 -20
View File
@@ -25,12 +25,46 @@ pub struct Painter<'a> {
/// Previous handles, consumed in draw order and freed if left over.
pub(super) recycle: std::iter::Peekable<std::vec::IntoIter<PrimitiveHandle>>,
pub(super) children: Vec<WidgetId>,
pub(super) reuse_child_sizes: bool,
pub(super) size_dependencies: Vec<WidgetId>,
pub(super) size: Option<Size>,
/// Whether a retained child's length on each axis is still valid. A
/// child's length may change when the parent's orthogonal extent changes
/// (most importantly, wrapped text gets taller when it gets narrower),
/// but not merely because a content-sized parent grew along that same
/// axis around one of its siblings.
pub(super) reuse_child_sizes: [bool; 2],
pub layer: usize,
pub(super) id: WidgetId,
}
/// A child draw whose size has not necessarily been observed by its parent.
/// Holding this value keeps the painter borrowed, so `.size()` can only name
/// the child from the immediately preceding draw.
pub struct DrawResult<'p, 'a> {
painter: &'p mut Painter<'a>,
child: WidgetId,
}
impl DrawResult<'_, '_> {
/// Return the child's reported size and record the layout dependency.
pub fn size(self) -> Size {
if !self.painter.size_dependencies.contains(&self.child) {
self.painter.size_dependencies.push(self.child);
}
self.painter.state.active[&self.child].size
}
}
impl<'a> Painter<'a> {
/// Record the size this widget used. Every `Widget::draw` calls this
/// exactly once; parents observe it through [`DrawResult::size`].
pub fn set_size(&mut self, size: Size) {
assert!(
self.size.replace(size).is_none(),
"a widget set its size more than once during one draw"
);
}
fn primitive_at<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
self.write_primitive(primitive, region, Drawn::Yes);
}
@@ -207,15 +241,19 @@ impl<'a> Painter<'a> {
self.mask = self.own_mask;
}
/// Draws a widget within this widget's region, returning the size it
/// reported using.
pub fn widget<W: ?Sized>(&mut self, id: &StrongWidget<W>) -> Size {
/// Draw a widget within this widget's region. Reading the result's size
/// records that this widget's layout depends on the child.
pub fn widget<'p, W: ?Sized>(&'p mut self, id: &StrongWidget<W>) -> DrawResult<'p, 'a> {
self.widget_at(id, self.region)
}
/// Draws a widget somewhere within this one.
/// Useful for drawing child widgets in select areas.
pub fn widget_within<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) -> Size {
pub fn widget_within<'p, W: ?Sized>(
&'p mut self,
id: &StrongWidget<W>,
region: UiRegion,
) -> DrawResult<'p, 'a> {
self.widget_at(id, region.within(&self.region))
}
@@ -260,17 +298,29 @@ impl<'a> Painter<'a> {
}
}
pub fn known_len<W: ?Sized>(&self, id: &StrongWidget<W>, axis: Axis) -> Option<Len> {
if let Some(len) = self.rsc.widgets().get_dyn(id.id())?.size_hint(axis) {
return Some(len.fold_dp(self.density()));
pub fn known_len<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<Len> {
let len = if let Some(len) = self.rsc.widgets().get_dyn(id.id())?.size_hint(axis) {
Some(len.fold_dp(self.density()))
} else if !self.reuse_child_sizes[match axis {
Axis::X => 0,
Axis::Y => 1,
}] || self.rsc.widgets().needs_redraw.contains(&id.id())
{
None
} else {
self.state.active.get(&id.id()).map(|a| a.size.axis(axis))
};
if len.is_some() && !self.size_dependencies.contains(&id.id()) {
self.size_dependencies.push(id.id());
}
if !self.reuse_child_sizes || self.rsc.widgets().needs_redraw.contains(&id.id()) {
return None;
}
self.state.active.get(&id.id()).map(|a| a.size.axis(axis))
len
}
fn widget_at<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) -> Size {
fn widget_at<'p, W: ?Sized>(
&'p mut self,
id: &StrongWidget<W>,
region: UiRegion,
) -> DrawResult<'p, 'a> {
self.children.push(id.id());
// Passed directly rather than looked up from `self.active`: this
// widget's own `ActiveData` (which would carry its `move_slot`) is
@@ -289,19 +339,26 @@ impl<'a> Painter<'a> {
self.mask,
Retained::default(),
self.rsc,
)
);
DrawResult {
painter: self,
child: id.id(),
}
}
/// Place an already-drawn child's used area, redrawing only if its size changes.
pub fn place<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) -> Size {
pub fn place<'p, W: ?Sized>(
&'p mut self,
id: &StrongWidget<W>,
region: UiRegion,
) -> DrawResult<'p, 'a> {
let region = region.within(&self.region);
let retained = self
.state
.active
.get(&id.id())
.map(|active| (active.layer, active.mask));
if let Some(size) = self.state.place(id.id(), region, self.rsc) {
size
if self.state.place(id.id(), region, self.rsc).is_some() {
} else if let Some((layer, mask)) = retained {
self.children.push(id.id());
self.rsc.widgets_mut().needs_redraw.insert(id.id());
@@ -315,9 +372,24 @@ impl<'a> Painter<'a> {
mask,
Retained::default(),
self.rsc,
)
);
} else {
self.widget_at(id, region)
self.children.push(id.id());
let parent_move_slot = self.child_move_slot.unwrap_or(self.move_slot);
self.state.draw_inner(
self.layer,
id.id(),
region,
Some(self.id),
parent_move_slot.idx() as u32,
self.mask,
Retained::default(),
self.rsc,
);
}
DrawResult {
painter: self,
child: id.id(),
}
}
@@ -326,7 +398,7 @@ impl<'a> Painter<'a> {
id: &StrongWidget<W>,
used: Size,
within: UiRegion,
) -> Size {
) -> DrawResult<'_, 'a> {
let region = self.fit_region(used, within);
self.place(id, region)
}
+101 -64
View File
@@ -535,8 +535,16 @@ impl UiRenderState {
let move_slot = Self::move_slot_for(old_move_slot, parent_move_slot, rsc);
let inherited_mask = mask;
let reuse_child_sizes =
old_region.is_some_and(|old| Self::same_size(old, region, self.output_size));
// `Painter::layer` is a cursor widgets advance while assigning
// layers to their children. Retain the layer this widget itself was
// entered on, not wherever that cursor finishes after `draw`.
let inherited_layer = layer;
let reuse_child_sizes = old_region.map_or([false; 2], |old| {
[
Self::same_axis_size(old, region, Axis::Y, self.output_size),
Self::same_axis_size(old, region, Axis::X, self.output_size),
]
});
let mut painter = Painter {
state: self,
region,
@@ -550,6 +558,8 @@ impl UiRenderState {
primitives: Vec::new(),
recycle: recycle.into_iter().peekable(),
children: Vec::new(),
size_dependencies: Vec::new(),
size: None,
reuse_child_sizes,
rsc,
};
@@ -561,7 +571,13 @@ impl UiRenderState {
widget.size_hint(Axis::Y).map(|len| len.fold_dp(density)),
];
painter.state.draw_count += 1;
let size = widget.draw(&mut painter);
widget.draw(&mut painter);
let size = painter.size.unwrap_or_else(|| {
panic!(
"widget '{}' ({id:?}) did not set its size during draw",
painter.rsc.widgets().label(id)
)
});
debug_assert!(
size.x.dp == 0.0 && size.y.dp == 0.0,
"widget {id:?} reported an unresolved `dp` size ({size:?}); \
@@ -593,8 +609,10 @@ impl UiRenderState {
primitives,
recycle,
children,
size_dependencies,
size: _,
reuse_child_sizes: _,
layer,
layer: _,
id,
} = painter;
@@ -615,8 +633,9 @@ impl UiRenderState {
textures,
primitives,
children,
size_dependencies,
mask: inherited_mask,
layer,
layer: inherited_layer,
size,
move_slot,
child_move_slot,
@@ -690,9 +709,13 @@ impl UiRenderState {
}
fn same_size(a: UiRegion, b: UiRegion, output: Vec2) -> bool {
let a = a.size().to_abs(output);
let b = b.size().to_abs(output);
(a.x - b.x).abs() < 0.01 && (a.y - b.y).abs() < 0.01
Self::same_axis_size(a, b, Axis::X, output) && Self::same_axis_size(a, b, Axis::Y, output)
}
fn same_axis_size(mut a: UiRegion, mut b: UiRegion, axis: Axis, output: Vec2) -> bool {
let a = a.axis(axis).len().to_abs(output.axis(axis));
let b = b.axis(axis).len().to_abs(output.axis(axis));
(a - b).abs() < 0.01
}
pub(super) fn place(
@@ -920,12 +943,76 @@ impl UiRenderState {
}
pub fn redraw_updates(&mut self, rsc: &mut dyn UiRsc) {
while let Some(&id) = rsc.widgets().needs_redraw.iter().next() {
self.redraw(id, rsc);
while rsc.widgets().has_updates() {
// Expand size dependencies before drawing anything. The parent
// links are the retained widget tree already used by hit testing
// and removal; only the direct-child dependency list is new.
let pending: Vec<_> = rsc.widgets().needs_redraw.iter().copied().collect();
for mut child in pending {
for _ in 0..PARENT_CHAIN_LIMIT {
// An exact hint is the child's current answer without a
// draw. If both axes still match the retained size, no
// parent can observe a size change from this mutation.
if self.size_matches_hints(child, rsc) {
break;
}
let Some(parent) = self.active.get(&child).and_then(|active| active.parent)
else {
break;
};
let depends = self
.active
.get(&parent)
.is_some_and(|active| active.size_dependencies.contains(&child));
if !depends {
break;
}
rsc.widgets_mut().needs_redraw.insert(parent);
child = parent;
}
}
// A dirty ancestor draws its dirty descendants on the way down;
// starting those descendants separately would duplicate work.
let dirty: Vec<_> = rsc.widgets().needs_redraw.iter().copied().collect();
let mut roots = Vec::new();
for id in dirty {
let mut ancestor = self.active.get(&id).and_then(|active| active.parent);
let mut covered = false;
for _ in 0..PARENT_CHAIN_LIMIT {
let Some(parent) = ancestor else { break };
if rsc.widgets().needs_redraw.contains(&parent) {
covered = true;
break;
}
ancestor = self.active.get(&parent).and_then(|active| active.parent);
}
if !covered {
roots.push(id);
}
}
for id in roots {
self.redraw(id, rsc);
}
}
rsc.free();
}
fn size_matches_hints(&self, id: WidgetId, rsc: &dyn UiRsc) -> bool {
let Some(active) = self.active.get(&id) else {
return false;
};
let Some(widget) = rsc.widgets().get_dyn(id) else {
return false;
};
[Axis::X, Axis::Y].into_iter().all(|axis| {
widget
.size_hint(axis)
.map(|hint| hint.fold_dp(self.density))
== Some(active.size.axis(axis))
})
}
pub fn root_changed<'a>(&self, root: impl Into<Option<&'a StrongWidget>>) -> bool {
root.into().map(|r| r.id()) != self.old_root
}
@@ -1207,61 +1294,18 @@ impl UiRenderState {
/// redraws a widget that's currently active (drawn)
pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) {
self.redraw_and_settle(id, rsc);
}
/// Measure a changed branch toward the root, then revisit each widget
/// whose reported size changed on the way back down. The upward pass gives
/// every parent the new child size; the downward pass is what lets those
/// children draw inside the final boxes their parents chose. Without it a
/// newly grown subtree can retain the provisional (even inverted) region
/// it was measured in until an unrelated later update redraws it.
fn redraw_and_settle(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) {
let Some((parent, changed)) = self.redraw_once(id, rsc) else {
return;
};
if changed {
if let Some(pid) = parent {
self.redraw_and_settle(pid, rsc);
}
// The parent pass above has now placed this widget in its final
// region. Draw it once more there; unchanged descendants still
// take draw_inner's retained fast path. This is deliberately one
// redraw rather than another settling pass: feeding its size
// back into the same upward walk can alternate between the
// provisional and final regions forever (a text edit first did
// that when an Android IME committed a space), overflowing the
// native thread's stack before Rust can report a panic.
let settled_size = self.active.get(&id).map(|active| active.size);
let _ = self.redraw_once(id, rsc);
debug_assert_eq!(
self.active.get(&id).map(|active| active.size),
settled_size,
"a widget changed size after its parent settled its final region"
);
}
}
/// Redraw `id` exactly once, returning its parent and whether the size it
/// reports changed. [`Self::redraw_and_settle`] owns any propagation; in
/// particular, its final downward redraw must not start another upward
/// pass through the same branch.
fn redraw_once(
&mut self,
id: WidgetId,
rsc: &mut dyn UiRsc,
) -> Option<(Option<WidgetId>, bool)> {
rsc.widgets_mut().needs_redraw.remove(&id);
// An ancestor is drawing this widget right now, and that draw is
// about to write fresh primitives for it. Drawing it a second time
// here would leave one of the two copies on screen with nothing
// owning it -- see `draw_started`'s own doc.
if self.draw_started.contains(&id) {
return None;
return;
}
let active = self.remove(id, false, true, rsc)?;
let old_size = active.size;
let Some(active) = self.remove(id, false, true, rsc) else {
return;
};
let parent = active.parent;
// `old_move_slot` being `Some` below means the slot is reused in
// place rather than freshly parented, so this is only reached for
@@ -1285,13 +1329,6 @@ impl UiRenderState {
},
rsc,
);
// If this widget's own reported size changed, its parent's layout
// (which placed it using the old size) is now stale and needs to
// relay out too. Checked after the real draw, not before it --
// there is no query left that answers "what size would this be"
// without actually drawing (LAYOUT.md section 5).
let changed = self.active.get(&id).map(|a| a.size) != Some(old_size);
Some((parent, changed))
}
}
+3 -3
View File
@@ -16,7 +16,7 @@ pub use view::*;
pub use widgets::*;
pub trait Widget: Any {
fn draw(&mut self, painter: &mut Painter) -> Size;
fn draw(&mut self, painter: &mut Painter);
/// An exact, context-free length known without drawing or inspecting children.
fn size_hint(&self, _axis: Axis) -> Option<Len> {
@@ -45,8 +45,8 @@ pub trait Widget: Any {
}
impl Widget for () {
fn draw(&mut self, _: &mut Painter) -> Size {
Size::ZERO
fn draw(&mut self, painter: &mut Painter) {
painter.set_size(Size::ZERO);
}
fn is_size_independent(&self) -> bool {