Make Iris layout dependencies explicit
This commit is contained in:
1 parent
2bc0ff1866
commit
3ae034a47b
25 files changed
+609
-186
No files matched your search
@@ -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
@@ -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
@@ -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
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user