Files
iris/core/src/ui/render_state.rs
T
iris-aiandClaude Opus 5 2807a925af Make a declared length one that cannot carry a share
`declared_lens` filtered `leftover` out of both its sources and every
consumer then re-dropped it, so the rule lived in two filters and a comment.
A declaration is a `Len`: `LayoutLen::declared` states the rule once and both
sources go through it, and `Declared` replaces the bare two-element array on
`ActiveData` and in four signatures.

The two sources stay one value deliberately. A rule decides the child's box;
a hint only promises what it will report -- but `size_hint` is by contract an
exact answer with no painter context, and `hints_agree` fails a widget that
draws something else, so narrowing the box to a hint cannot change what is
drawn. Every consumer asks about the length, never which said it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 21:06:31 -04:00

1211 lines
46 KiB
Rust

#[cfg(feature = "layout-diagnostics")]
use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind};
use crate::{
ActiveData, Axis, Declared, DrawLayers, IdLike, LayoutHolds, LayoutLen, Len, MaskIdx, MoveIdx,
Moves, Painter, PixelRegion, PlaceDesc, PxVec2, Rel, Size, StrongWidget, UiRegion, UiRsc,
UiSpan, UiVec2, Weight, WidgetId, Widgets,
util::{HashMap, Vec2},
};
const AXES: [Axis; 2] = [Axis::X, Axis::Y];
/// Where a widget is drawn: what its parent decides about the draw besides
/// the boxes themselves.
#[derive(Clone, Copy)]
pub(super) struct DrawInfo {
pub layer: usize,
pub parent: Option<WidgetId>,
pub depth: usize,
pub parent_move: MoveIdx,
pub region_node: bool,
pub mask: MaskIdx,
/// What a fraction declared or reported under this widget is a fraction
/// of, as a length of the window.
pub rel_base: UiVec2,
/// The box the widget is asked in, in its parent region node's
/// coordinates.
pub region: UiRegion,
/// Where the widget is put, and where it was asked, as parts of the
/// parent's box. See [`PlaceDesc`]. The two are one ask's place until the
/// parent puts the answer somewhere else.
pub placed: PlaceDesc,
pub asked: PlaceDesc,
/// Whether the parent already asked about this widget in this draw.
pub re_asked: bool,
/// The rel base in pixels, resolved once against the window.
pub px: PxVec2,
}
/// What a widget's children are placed in: its own box, the coordinates its
/// drawing is in, and what else one ask of a child is decided from.
pub(super) struct Placing {
pub id: WidgetId,
pub region: UiRegion,
pub rel_base: UiVec2,
pub window: PxVec2,
pub depth: usize,
pub move_idx: MoveIdx,
pub mask: MaskIdx,
}
pub struct UiRenderState {
pub active: HashMap<WidgetId, ActiveData>,
pub layers: DrawLayers,
pub(super) output_size: PxVec2,
old_root: Option<WidgetId>,
/// Whether the output has changed since the last update. A frame is
/// owed for that whether or not anything has to be drawn again: every
/// fraction becomes pixels against the output, in the shader's uniform
/// as well as here.
resized: bool,
/// A widget's move slot, which outlives any one `ActiveData`: a redraw
/// replaces that while its children go on pointing at the slot.
slots: HashMap<WidgetId, MoveIdx>,
/// Widgets waiting for an ancestor to draw them, so the walk down the
/// depths does not pick one up again at its own depth.
deferred: crate::util::HashSet<WidgetId>,
/// What the walk has left to settle, deepest last. Ordered rather than
/// searched for, so finding the next one is not a pass over the marks.
pending: std::collections::BTreeSet<(usize, WidgetId)>,
pub moves: Moves,
}
impl UiRenderState {
pub fn new() -> Self {
Self {
active: Default::default(),
layers: Default::default(),
output_size: PxVec2::ZERO,
old_root: None,
slots: Default::default(),
deferred: Default::default(),
pending: Default::default(),
moves: Default::default(),
resized: false,
}
}
/// The window, in whatever the platform measures it in, onto the grid
/// everything below it is decided on. No move entry holds it: a chain
/// bottoms out in `MoveIdx::NONE`, which is the window, and the window's
/// size is applied where a fraction becomes pixels -- here in `to_px`,
/// and in the shader by its uniform. A resize therefore rewrites no
/// retained entry at all.
///
/// The root is the only widget a resize marks, and only where the new
/// output invalidates its answer or its drawing. The latter includes
/// children whose size it never read. Where either fails, the ordinary walk
/// draws the root, and each widget's own range decides how far down the
/// new length reaches.
pub fn resize(&mut self, size: impl Into<Vec2>, widgets: &mut Widgets) {
let size = PxVec2::from_f32(size.into());
if size == self.output_size {
return;
}
self.output_size = size;
self.resized = true;
let Some(root) = self.old_root else { return };
let stands = self.active.get(&root).is_some_and(|active| {
// Nothing above the root chose anything, so the box it was first
// asked about is the whole of its rel base. Both its answer and its
// drawing have to stand in the new window, since nothing above
// it will ask either again.
let answer = active
.answer
.is_some_and(|(_, holds)| holds.contains(size, active.rel_base, active.region));
answer && active.holds.contains(size, active.rel_base, active.region)
});
if !stands {
widgets.needs_redraw.insert(root);
}
}
/// The root is asked about in the output. Its own rules narrow both its
/// rel base and box; nothing above it chose a different one.
fn root_info(&self, rel_base: UiVec2, region: UiRegion) -> DrawInfo {
let px = rel_base.to_px(self.output_size);
DrawInfo {
layer: 0,
parent: None,
depth: 1,
parent_move: MoveIdx::NONE,
region_node: false,
mask: MaskIdx::NONE,
rel_base,
region,
placed: PlaceDesc::WHOLE,
asked: PlaceDesc::WHOLE,
re_asked: false,
px,
}
}
pub fn output_size(&self) -> PxVec2 {
self.output_size
}
pub fn update<'a>(&mut self, root: impl Into<Option<&'a StrongWidget>>, rsc: &mut dyn UiRsc) {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::Updates);
#[cfg(feature = "layout-diagnostics")]
let _update = diag::timer(TimerKind::Update);
// safety mechanism for memory leaks; might wanna return a result instead so user can
// decide whether to panic or not
if !rsc.widgets().waiting.is_empty() {
let widgets = rsc.widgets();
let len = widgets.waiting.len();
let all: Vec<_> = widgets
.waiting
.iter()
.map(|&w| format!("'{}' ({w:?})", widgets.label(w)))
.collect();
panic!(
"{len} widget(s) were never upgraded\n\
this is likely a memory leak; consider upgrading to strong if you plan on using it later\n\
weak widgets: {all:#?}"
);
}
let root = root.into();
if self.root_changed(root) {
self.redraw_all(root, rsc);
self.old_root = root.map(|r| r.id());
}
self.resized = false;
if rsc.widgets().has_updates() {
self.redraw_updates(rsc);
}
self.free(rsc);
}
fn redraw_all(&mut self, root: Option<&StrongWidget>, rsc: &mut dyn UiRsc) {
#[cfg(feature = "layout-diagnostics")]
let _layout = diag::timer(TimerKind::FullLayout);
self.clear(rsc);
if let Some(id) = root {
let (rel_base, region) = Self::root_layout(id.id(), rsc.widgets());
let info = self.root_info(rel_base, region);
self.draw_inner(id.id(), info, None, rsc);
}
}
/// The root's rel base and box: the window, taken in by the root's own
/// rules. Nothing above it narrowed anything or chose where it goes, so
/// its declaration is the whole of what decides either.
fn root_layout(id: WidgetId, widgets: &Widgets) -> (UiVec2, UiRegion) {
PlaceDesc::WHOLE.rel_base_and_region(
UiRegion::FULL,
UiVec2::FULL_SIZE,
widgets.declared_lens(id),
widgets.alignment(id),
)
}
pub(super) fn draw_inner(
&mut self,
id: WidgetId,
info: DrawInfo,
mut old: Option<ActiveData>,
rsc: &mut dyn UiRsc,
) -> (Size, LayoutHolds, LayoutHolds) {
let old_parent = old
.as_ref()
.or_else(|| self.active.get(&id))
.and_then(|a| a.parent);
let region = info.region;
#[cfg(feature = "layout-diagnostics")]
{
diag::bump(Counter::DrawRequests);
diag::draw_request(id, info.parent, region, info.px, info.region_node);
}
let align = rsc.widgets().alignment(id);
let declared = rsc.widgets().declared_lens(id);
// Nothing this widget measured can be dirty while it draws: layout is
// one bottom-up walk, so anything deeper has settled or deferred to
// its own parent, and a deferred one leaves that parent marked.
let stale = rsc.widgets().needs_redraw.contains(&id);
// The widget draws once, in the box it is asked in, and its answer
// is placed inside that box by re-expressing the drawing. The box the
// answer chose is never a question: nothing is drawn again in it, so
// an answer is kept only with the drawing that gave it, and both
// have to hold for the box asked about.
let reused = (!stale)
.then(|| self.retained_answer(id, region, info))
.flatten()
.and_then(|answer| {
let placed = info.placed.placement(region, answer.0, declared, align);
self.try_reuse(id, region, placed, info, rsc)
.map(|()| answer)
});
let answer = reused.unwrap_or_else(|| {
if old.is_none() {
old = self.remove(id, false, rsc);
}
let answer = self.draw_at(id, region, info, old.take(), rsc);
// Where the drawing goes: the part its parent gave it, with the
// answer placed inside that part on any axis the parent left
// open.
let placed = info.placed.placement(region, answer.0, declared, align);
if placed != region {
self.relocate(id, placed, info, rsc);
}
answer
});
let drawing_holds = self.active[&id].holds;
let active = self.active.get_mut(&id).unwrap();
// Whoever asked owns how the boxes were reached: the rel base it stated,
// and what of its own box it asked in. A local redraw asks the same
// question again from these.
active.rel_base = info.rel_base;
active.re_asked = info.re_asked;
active.answer = Some(answer);
active.asked = info.asked;
active.region = region;
active.placed = info.placed;
active.own_align = align;
// The previous parent must stop owning the subtree before it can
// undraw it, whether changing hands reused the drawing or replaced it.
active.parent = info.parent;
if old_parent != info.parent
&& let Some(old_parent) = old_parent
&& let Some(old_parent) = self.active.get_mut(&old_parent)
{
old_parent.children.retain(|child| *child != id);
}
(answer.0, answer.1, drawing_holds)
}
/// Calls a widget's `draw` and keeps what it drew in `region`.
fn draw_at(
&mut self,
id: WidgetId,
region: UiRegion,
info: DrawInfo,
old: Option<ActiveData>,
rsc: &mut dyn UiRsc,
) -> (Size, LayoutHolds) {
let rel_base = info.rel_base;
let (move_idx, region, retired_move) = match info.region_node {
// A node entry is only a translation. Its local box keeps the
// same window-unit length as the box in its parent's node.
true => (
self.move_slot(id, info.parent_move, region.as_translation()),
region.at_origin(),
None,
),
// Keep the old entry alive until every descendant has migrated.
// Reusing its index sooner could make an old parent look current.
false => (info.parent_move, region, self.slots.remove(&id)),
};
let mask_slot = old
.as_ref()
.and_then(|old| old.mask_region.map(|_| old.mask));
let old_children = old.map_or_else(Vec::new, |old| old.children);
rsc.widgets_mut().needs_redraw.remove(&id);
let px = info.px;
let window = self.output_size;
let mut painter = Painter {
state: self,
rel_base,
region,
window,
mask: info.mask,
layer: info.layer,
own_layer: info.layer,
id,
textures: Vec::new(),
primitives: Vec::new(),
mask_region: None,
mask_slot,
children: Vec::new(),
size_deps: Vec::new(),
own: LayoutHolds::ANY,
under: Vec::new(),
answer_under: LayoutHolds::ANY,
depth: info.depth,
move_idx,
rsc,
};
#[cfg(feature = "layout-diagnostics")]
{
diag::bump(Counter::WidgetDraws);
diag::draw_widget(id, painter.rsc.widgets().label(id));
}
let mut widget = painter.rsc.widgets().get_dyn_dynamic(id);
let size = widget.draw(&mut painter);
drop(widget);
#[cfg(feature = "layout-diagnostics")]
diag::size_reported(id, size);
let Painter {
state: _,
rsc: _,
rel_base: _,
region: _,
window: _,
mask,
textures,
primitives,
mask_region,
mask_slot,
own,
answer_under,
children,
size_deps,
under,
move_idx,
layer,
own_layer: _,
depth: _,
id,
} = painter;
debug_assert!(
Self::hints_agree(id, size, rsc),
"'{}' ({id:?}) drew a size its size_hint disagrees with",
rsc.widgets().label(id)
);
// A rule wins on the axis it names, and the draw answers the rest.
// Applied here so it is one place rather than every widget that could
// carry one, and so the widget under a rule never learns of it. The
// rel base is the answer where the rule gave a length outright: it was
// resolved into the rel base when the child was asked, and resolving it
// again here would take the fraction of a fraction.
let rules = rsc.widgets().size_rules(id);
let ruled = |axis: Axis, reported: LayoutLen| match rules[axis].exact() {
None => reported,
Some(len) if len.leftover == Weight::ZERO => LayoutLen {
rel: info.rel_base[axis].rel,
px: info.rel_base[axis].px,
leftover: Weight::ZERO,
},
Some(len) => len.within_len(info.rel_base[axis]),
};
let size = Size {
x: ruled(Axis::X, size.x),
y: ruled(Axis::Y, size.y),
};
// A widget that clipped its contents to its box drew nothing outside
// it, so reporting more than the box asks to be placed at a length it
// does not occupy -- and its parent would place the part it cut off.
// Overflowing is otherwise ordinary: a text too tall for the box it
// was offered reports the height it needs.
debug_assert!(
mask == info.mask
|| AXES
.into_iter()
.all(|axis| size.within_box(region, self.output_size, axis)),
"'{}' ({id:?}) clips to {px:?} and reports {size}",
rsc.widgets().label(id),
);
for c in &old_children {
if !children.contains(c) {
self.undraw_rec(*c, rsc);
}
}
if let Some(idx) = mask_slot {
rsc.ui_mut().masks.remove(idx);
}
if let Some(idx) = retired_move {
self.moves.remove(idx);
}
// A rule that is a fraction of the rel base is answered with the
// rel base's own length, so the answer is that rel base's and not just
// that many pixels of this window -- the same pin a widget that read
// its rel base took for its drawing.
let mut own_holds = own;
for axis in AXES {
let fraction = rules[axis].exact().is_some_and(|len| len.rel != Rel::ZERO);
if fraction {
own_holds[axis].rel_base = Some(info.rel_base[axis]);
}
}
let answer_holds = own_holds.and(answer_under);
let holds = under
.into_iter()
.fold(answer_holds, |holds, (_, child)| holds.and(child));
debug_assert!(
holds.contains(self.output_size, info.rel_base, region),
"'{}' ({id:?}) drew in {px:?}, outside the ranges it reported: {holds:?}",
rsc.widgets().label(id),
);
// What it asked about and did not draw is still something it asked,
// and a change there has to reach it. Asking answered whatever mark
// it had: a hint is read live, and a drawing is not kept past one.
for &dep in &size_deps {
if !children.contains(&dep) {
self.asked(
dep,
DrawInfo {
layer,
parent: Some(id),
depth: info.depth + 1,
parent_move: move_idx,
region_node: false,
mask,
rel_base: UiVec2::FULL_SIZE,
region: UiRegion::FULL,
placed: PlaceDesc::WHOLE,
asked: PlaceDesc::WHOLE,
re_asked: false,
px,
},
rsc,
);
rsc.widgets_mut().needs_redraw.remove(&dep);
}
}
let active = ActiveData {
id,
placement: region,
rel_base: info.rel_base,
placed: info.placed,
asked: info.asked,
region,
// Whoever asked writes the answer.
answer: None,
re_asked: info.re_asked,
size,
holds,
drawn: true,
parent: info.parent,
depth: info.depth,
textures,
primitives,
mask_region,
children,
size_deps,
declared: rsc.widgets().declared_lens(id),
own_align: rsc.widgets().alignment(id),
move_idx,
parent_move: info.parent_move,
mask,
parent_mask: info.mask,
layer: info.layer,
};
rsc.on_draw(&active);
self.active.insert(id, active);
(size, answer_holds)
}
/// Keeps a region node's entry across redraws because descendants retain
/// its index.
fn move_slot(&mut self, id: WidgetId, parent: MoveIdx, region: UiRegion) -> MoveIdx {
if let Some(&idx) = self.slots.get(&id) {
self.moves.set_parent(idx, parent);
self.moves.set(idx, region);
return idx;
}
let idx = self.moves.push(parent, region);
self.slots.insert(id, idx);
idx
}
/// Removes a region node only after its descendants stop naming it.
fn drop_slot(&mut self, id: WidgetId) {
if let Some(idx) = self.slots.remove(&id) {
self.moves.remove(idx);
}
}
/// The answer to an ask can be retained independently of where its
/// drawing ended up. Alignment is exactly that case: the first box is the
/// question and the smaller placed box holds the drawing. Whether the
/// answer is stale at all is its caller's question, asked once there.
fn retained_answer(
&self,
id: WidgetId,
region: UiRegion,
info: DrawInfo,
) -> Option<(Size, LayoutHolds)> {
let active = self.active.get(&id)?;
let has_region_node = active.move_idx != active.parent_move;
if !active.drawn
|| has_region_node != info.region_node
|| active.parent_move != info.parent_move
{
return None;
}
let answer = active.answer?;
answer
.1
.contains(self.output_size, info.rel_base, region)
.then_some(answer)
}
/// Keeps the retained drawing if its contract holds for `part`, the box
/// asked about, and puts it at `placed`, where the answer places it.
fn try_reuse(
&mut self,
id: WidgetId,
region: UiRegion,
placed: UiRegion,
info: DrawInfo,
rsc: &mut dyn UiRsc,
) -> Option<()> {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::ReuseAttempts);
if rsc.widgets().needs_redraw.contains(&id) {
#[cfg(feature = "layout-diagnostics")]
{
diag::bump(Counter::ReuseDirty);
diag::reuse(id, ReuseOutcome::Dirty);
}
return None;
}
let active = self.active.get(&id)?;
if !active.drawn {
#[cfg(feature = "layout-diagnostics")]
diag::reuse(id, ReuseOutcome::Undrawn);
return None;
}
let has_region_node = active.move_idx != active.parent_move;
if has_region_node != info.region_node {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::ReuseWrongNode);
return None;
}
// Drawn on another layer: the drawing sits in that layer's list and
// paints at its moment, which no amount of geometry says. A container
// that measures a child by drawing it and then draws it again where
// it belongs -- `Stack`, over its background -- asks the second time
// on a layer the first answer is not good for.
if active.layer != info.layer {
#[cfg(feature = "layout-diagnostics")]
{
diag::bump(Counter::ReuseWrongLayer);
diag::reuse(id, ReuseOutcome::WrongLayer);
}
return None;
}
if active.parent_mask != info.mask {
return None;
}
// Drawn somewhere else in the tree: its box is in coordinates it no
// longer sits in, and its slot names the wrong parent.
if active.parent_move != info.parent_move {
#[cfg(feature = "layout-diagnostics")]
{
diag::bump(Counter::ReuseWrongParent);
diag::reuse(id, ReuseOutcome::WrongParent);
}
return None;
}
// In pixels, because the box is a fraction of the window and that
// may be what changed -- an unchanged fraction of a window half the
// size is half the widget.
if !active
.holds
.contains(self.output_size, info.rel_base, region)
{
#[cfg(feature = "layout-diagnostics")]
{
// Which of the three said no, so a rel base that redraws more
// than it should says where to look. They overlap: a drawing
// can be outside two of them at once.
for axis in AXES {
let holds = active.holds[axis];
let len = region[axis].len();
let window = self.output_size[axis];
if holds.region_len.is_some_and(|pinned| pinned != len) {
diag::bump(Counter::OutsidePinnedLen);
}
if !holds.window.contains(window)
|| holds
.rel_base
.is_some_and(|pinned| pinned != info.rel_base[axis])
{
diag::bump(Counter::OutsideRelBase);
}
if !holds.region.contains(len.to_px(window)) {
diag::bump(Counter::OutsideRegion);
}
}
diag::bump(Counter::ReuseOutside);
diag::reuse(id, ReuseOutcome::Outside);
}
return None;
}
self.relocate(id, placed, info, rsc);
Some(())
}
/// Puts a retained drawing where its parent now has it, without drawing:
/// a widget with a node of its own writes that node's translation, and
/// one without re-expresses its own drawing and everything inside it.
fn relocate(&mut self, id: WidgetId, placed: UiRegion, info: DrawInfo, rsc: &mut dyn UiRsc) {
let active = &self.active[&id];
debug_assert!(
!rsc.widgets().needs_redraw.contains(&id),
"'{}' ({id:?}) placed while marked to draw",
rsc.widgets().label(id)
);
let has_region_node = active.move_idx != active.parent_move;
let local = match has_region_node {
true => placed.at_origin(),
false => placed,
};
let moved = active.placement != local;
let slot = active.move_idx;
if has_region_node {
self.moves.set(slot, placed.as_translation());
}
if moved {
self.reposition(id, local, info, rsc);
}
self.redepth(id, info.depth);
let active = self.active.get_mut(&id).unwrap();
active.rel_base = info.rel_base;
active.placed = info.placed;
#[cfg(feature = "layout-diagnostics")]
{
match (moved, has_region_node) {
(true, true) => diag::bump(Counter::ReuseMoved),
(true, false) => diag::bump(Counter::ReuseRemapped),
(false, _) => diag::bump(Counter::ReuseExact),
}
diag::reuse(
id,
if moved {
if has_region_node {
ReuseOutcome::Moved
} else {
ReuseOutcome::Remapped
}
} else {
ReuseOutcome::Exact
},
);
}
}
/// 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) {
let place = self.active[&child].placed;
self.place_in(child, at, place, rsc);
}
/// Puts a child of `at.id` in `place` of that widget's box: its answer
/// placed inside that part where the place leaves the axis open, the
/// drawing re-expressed there.
pub(super) fn place_in(
&mut self,
child: WidgetId,
at: &Placing,
place: PlaceDesc,
rsc: &mut dyn UiRsc,
) {
let active = &self.active[&child];
let (rel_base, region) = Self::ask_again(active, at, place);
let placed = place.placement(
region,
active.measured().unwrap_or(active.size),
active.declared,
active.own_align,
);
let info = DrawInfo {
layer: active.layer,
parent: Some(at.id),
depth: at.depth + 1,
parent_move: at.move_idx,
region_node: active.move_idx != active.parent_move,
mask: at.mask,
rel_base,
region,
placed: place,
asked: active.asked,
re_asked: active.re_asked,
px: rel_base.to_px(at.window),
};
self.relocate(child, placed, info, rsc);
}
/// The rel base and the box a widget already drawn is given at `place` of
/// the box its parent is being taken as. What narrowed its rel base and what
/// it declared are its own record's, so both are resolved against that
/// parent's rel base again exactly as the first ask resolved them.
fn ask_again(active: &ActiveData, at: &Placing, place: PlaceDesc) -> (UiVec2, UiRegion) {
place.rel_base_and_region(at.region, at.rel_base, active.declared, active.own_align)
}
/// Re-places everything inside a widget whose own box moved. Every child
/// is placed as a part of that box, so each one's new box is its retained
/// part re-added to the new start -- and a child whose own box then did
/// not change is not touched at all.
fn reposition(&mut self, id: WidgetId, placed: UiRegion, info: DrawInfo, rsc: &mut dyn UiRsc) {
let active = self.active.get_mut(&id).unwrap();
active.placement = placed;
for primitive in &active.primitives {
let handle = &primitive.handle;
*self.layers[handle.layer].region_mut(handle) = primitive.region.within(&placed);
}
if let Some(mask_region) = active.mask_region {
rsc.ui_mut().masks.get_mut(active.mask).region = mask_region.within(&placed);
}
let at = Placing {
id,
region: placed,
rel_base: info.rel_base,
window: self.output_size,
depth: info.depth,
move_idx: active.move_idx,
mask: active.mask,
};
let children = active.children.len();
for index in 0..children {
let child = self.active[&id].children[index];
self.place_child(child, &at, rsc);
}
}
/// A reused subtree keeps its shape, so every widget in it moves by the
/// same amount -- and where the top of it did not move, none of it did,
/// which is what makes this free in the ordinary case.
fn redepth(&mut self, id: WidgetId, depth: usize) {
let Some(active) = self.active.get_mut(&id) else {
return;
};
if active.depth == depth {
return;
}
active.depth = depth;
let children = active.children.len();
for index in 0..children {
let child = self.active[&id].children[index];
self.redepth(child, depth + 1);
}
}
fn hints_agree(id: WidgetId, size: Size, rsc: &dyn UiRsc) -> bool {
let Some(widget) = rsc.widgets().get_dyn(id) else {
return true;
};
AXES.into_iter()
.all(|axis| widget.size_hint(axis).is_none_or(|hint| hint == size[axis]))
}
/// Takes a widget's record out and frees what it drew.
fn remove(&mut self, id: WidgetId, undraw: bool, rsc: &mut dyn UiRsc) -> Option<ActiveData> {
let mut active = self.active.remove(&id);
if let Some(active) = &mut active {
for primitive in &active.primitives {
let mask = self.layers.free(&primitive.handle);
if mask != MaskIdx::NONE {
rsc.ui_mut().masks.remove(mask);
}
}
if undraw && active.mask_region.take().is_some() {
rsc.ui_mut().masks.remove(active.mask);
}
active.primitives.clear();
active.textures.clear();
rsc.ui_mut().textures.free();
if undraw && active.drawn {
rsc.on_undraw(active);
}
}
active
}
/// Stops drawing a widget and everything under it, keeping the record
/// of who asked about it so that a change to it still reaches them.
pub(super) fn undraw_rec(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) {
let Some(mut active) = self.remove(id, true, rsc) else {
return;
};
for c in std::mem::take(&mut active.children) {
self.undraw_rec(c, rsc);
}
// After the descendants, whose slots name this one as their parent.
self.drop_slot(id);
active.size_deps.clear();
active.drawn = false;
self.active.insert(id, active);
}
/// Records that `info.parent` asked about a widget it does not draw.
fn asked(&mut self, id: WidgetId, info: DrawInfo, rsc: &mut dyn UiRsc) {
if let Some(active) = self.active.get_mut(&id) {
debug_assert!(!active.drawn, "asked about a widget it drew");
active.parent = info.parent;
active.depth = info.depth;
return;
}
// Never drawn, so there is no drawing to hold anything; what its
// parent read was its hint.
let widget = rsc.widgets().get_dyn(id);
let size = Size {
x: widget
.and_then(|w| w.size_hint(Axis::X))
.unwrap_or(LayoutLen::ZERO),
y: widget
.and_then(|w| w.size_hint(Axis::Y))
.unwrap_or(LayoutLen::ZERO),
};
self.active.insert(
id,
ActiveData {
id,
placement: UiRegion::FULL,
rel_base: UiVec2::FULL_SIZE,
placed: PlaceDesc::WHOLE,
asked: PlaceDesc::WHOLE,
region: UiRegion::FULL,
answer: None,
re_asked: false,
size,
holds: LayoutHolds::ANY,
drawn: false,
parent: info.parent,
depth: info.depth,
textures: Vec::new(),
primitives: Vec::new(),
mask_region: None,
children: Vec::new(),
size_deps: Vec::new(),
move_idx: info.parent_move,
declared: Declared::NONE,
own_align: rsc.widgets().alignment(id),
parent_move: info.parent_move,
mask: info.mask,
parent_mask: info.mask,
layer: info.layer,
},
);
}
fn clear(&mut self, rsc: &mut dyn UiRsc) {
for (_, active) in self.active.drain() {
if active.drawn {
rsc.on_undraw(&active);
}
}
self.slots.clear();
self.moves.clear();
self.layers.clear();
rsc.ui_mut().masks = Default::default();
rsc.widgets_mut().needs_redraw.clear();
self.free(rsc);
}
/// Frees the widgets nothing holds any more, and the records kept of
/// them: an id is handed on to the next widget made.
fn free(&mut self, rsc: &mut dyn UiRsc) {
while let Some(id) = rsc.widgets_mut().free_next() {
rsc.on_remove(id);
self.remove(id, true, rsc);
self.drop_slot(id);
}
rsc.ui_mut().textures.free();
}
pub fn redraw_updates(&mut self, rsc: &mut dyn UiRsc) {
#[cfg(feature = "layout-diagnostics")]
let _layout = diag::timer(TimerKind::IncrementalLayout);
// Deepest first, and strictly: a widget that cannot settle where it
// is defers to its parent rather than drawing the parent from
// inside itself. It marks the parent, stays marked, and waits here
// until the walk reaches its parent's depth.
//
// What that buys is that nothing shallower is ever drawn while
// anything deeper is still dirty. A parent drawing can therefore
// trust every answer it reads without descending to check whether
// something below is about to change it -- which is the whole class
// of defect where a widget settles inside its parent's draw, clears
// its mark there, and tells nobody its answer moved.
// The queue is that set, ordered: a mark made while the walk runs
// queues itself through `mark`. What ends the walk is still the set
// being spent, not the queue, so a mark that reached it another way
// cannot be left for the next frame.
loop {
for &id in rsc.widgets().needs_redraw.iter() {
if !self.deferred.contains(&id) {
let depth = self.depth(id);
self.pending.insert((depth, id));
}
}
if self.pending.is_empty() {
break;
}
while let Some((depth, id)) = self.pending.pop_last() {
// Settled inside an ancestor's draw, or deferred to one,
// since the mark that queued it.
if self.deferred.contains(&id) || !rsc.widgets().needs_redraw.contains(&id) {
continue;
}
// A subtree that changed hands takes its descendants' depths
// with it, so an entry queued before that move names the
// depth it had under the parent it left.
let now = self.depth(id);
if now != depth {
self.pending.insert((now, id));
continue;
}
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::QueuePops);
if !self.redraw(id, rsc) {
self.deferred.insert(id);
}
}
}
self.deferred.clear();
}
/// Marks a widget for the walk to settle, and queues it at its depth.
fn mark(&mut self, id: WidgetId, widgets: &mut Widgets) {
if widgets.needs_redraw.insert(id) && !self.deferred.contains(&id) {
let depth = self.depth(id);
self.pending.insert((depth, id));
}
}
fn depth(&self, id: WidgetId) -> usize {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::DepthReads);
let depth = match self.active.get(&id) {
Some(active) if active.drawn => active.depth,
// Nothing keeps an undrawn widget's current, and it only has to
// reach whoever asked.
Some(_) => return self.walked_depth(id),
None => 1,
};
debug_assert_eq!(
depth,
self.walked_depth(id),
"a widget's kept depth is not the one its ancestry says"
);
depth
}
/// What the kept depth is checked against, and the only thing that reads
/// the ancestry to find one.
fn walked_depth(&self, id: WidgetId) -> usize {
let mut depth = 0;
let mut at = Some(id);
while let Some(id) = at {
at = self.active.get(&id).and_then(|active| active.parent);
depth += 1;
}
depth
}
pub fn root_changed<'a>(&self, root: impl Into<Option<&'a StrongWidget>>) -> bool {
root.into().map(|r| r.id()) != self.old_root
}
pub fn needs_redraw<'a>(
&self,
root: impl Into<Option<&'a StrongWidget>>,
widgets: &Widgets,
) -> bool {
self.root_changed(root) || self.resized || widgets.has_updates()
}
pub fn active_widgets(&self) -> usize {
self.active.values().filter(|active| active.drawn).count()
}
pub fn debug(&self, widgets: &Widgets, label: &str) -> impl Iterator<Item = &ActiveData> {
self.active.iter().filter_map(move |(&id, inst)| {
let l = widgets.label(id);
if l == label { Some(inst) } else { None }
})
}
pub fn debug_layers(&self) {
for ((idx, depth), draws) in self.layers.iter_depth() {
let indent = " ".repeat(depth * 2);
let counts: Vec<String> = draws
.primitives()
.iter()
.map(|l| l.as_ref().map_or(0, |l| l.instances().len()).to_string())
.collect();
println!("{indent}{idx}: [{}]", counts.join(", "));
}
}
/// Where a widget is on screen: its box composed through the boxes it
/// sits within, the same walk the vertex shader does. `None` for one that
/// is not drawn.
///
/// This is for asking where a drawing landed: hit testing, and a test
/// reading a box back. Layout decides on the lengths threaded down the
/// draw instead, and a position is not one of its inputs.
pub fn window_region(&self, id: &impl IdLike) -> Option<PixelRegion> {
let active = self.active.get(&id.id())?;
active.drawn.then(|| {
self.moves
.resolve(active.move_idx, active.placement)
.to_px(self.output_size)
})
}
/// Settles a dirty widget: asks it again where its parent asked, and
/// tells the parent if the answer changed. `false` where the question is
/// its parent's rather than its own, which leaves it marked for the
/// parent to draw when the walk reaches that depth.
pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) -> bool {
rsc.widgets_mut().needs_redraw.remove(&id);
let Some(active) = self.active.get(&id) else {
return true;
};
// Its parent resolved its declared lengths into its box and decided
// whether to draw it at all, so a change to either is the parent's
// to draw -- with the mark left on, so the parent draws it rather
// than keeping it. So is a widget the parent asked twice: its
// layout rests on an answer this widget cannot give again alone.
let declared_changed = rsc.widgets().declared_lens(id) != active.declared;
let alignment_changed = rsc.widgets().alignment(id) != active.own_align;
if let Some(parent) = active.parent
&& (declared_changed
|| alignment_changed
|| active.re_asked
|| !active.drawn
|| active.answer.is_none())
{
// Both stay marked: the parent because it has this to draw, and
// this because the parent must draw it rather than keep what it
// has. The mark comes off in `draw_at`, where the parent draws.
self.mark(id, rsc.widgets_mut());
self.mark(parent, rsc.widgets_mut());
return false;
}
if !active.drawn {
return true;
}
// Nothing above the root resolved its rules or its alignment, so its
// box is its own to work out again against the output. Every other
// widget was given one.
let Some(parent) = active.parent else {
let (rel_base, region) = Self::root_layout(id, rsc.widgets());
let info = DrawInfo {
mask: active.parent_mask,
..self.root_info(rel_base, region)
};
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::LocalRedraws);
let old = self.remove(id, false, rsc);
self.draw_inner(id, info, old, rsc);
return true;
};
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
// 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
// parent's answer put its own drawing is not a question anybody
// asked, and nothing is asked in it here either.
let parent_at = self.placing_of(parent, self.active[&parent].region);
let (rel_base, region) = Self::ask_again(active, &parent_at, active.asked);
let info = DrawInfo {
layer: active.layer,
parent: active.parent,
depth: active.depth,
parent_move: active.parent_move,
region_node: rsc.widgets().is_region_node(id),
mask: active.parent_mask,
rel_base,
region,
placed: active.asked,
asked: active.asked,
re_asked: false,
px: rel_base.to_px(self.output_size),
};
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::LocalRedraws);
let old = self.remove(id, false, rsc);
let answer = self.draw_inner(id, info, old, rsc);
let active = self.active.get_mut(&id).unwrap();
// A wider contract does not invalidate the guarantee the parent kept.
// Retain that guarantee so widening and narrowing back do not churn it.
if let Some((size, holds)) = was_answer
&& answer.0 == size
&& answer.1.covers(holds)
{
active.answer = was_answer;
}
if active.holds.covers(was_holds)
&& was_holds.contains(self.output_size, active.rel_base, active.placement)
{
active.holds = was_holds;
}
if active.answer != was_answer || active.holds != was_holds {
// The parent retains both the answer and the drawing's validity;
// even an unchanged size can narrow the range safe for a resize.
#[cfg(feature = "layout-diagnostics")]
{
diag::bump(Counter::SizeChanges);
diag::bump(Counter::ReaderEdges);
}
self.mark(parent, rsc.widgets_mut());
} else {
// The answer stands, so where the parent put it stands: the
// fresh drawing goes back there -- the same place, of the box
// the parent's answer chose rather than the one it was asked in.
let at = self.placing_of(parent, self.active[&parent].placement);
self.place_in(id, &at, was_place, rsc);
}
true
}
/// A drawn widget as the thing its children are placed within, with
/// `region` as the box their parts are of: the box it was asked in for
/// asking one of them again, the box its answer chose for placing one.
fn placing_of(&self, id: WidgetId, region: UiRegion) -> Placing {
let active = &self.active[&id];
Placing {
id,
region,
rel_base: active.rel_base,
window: self.output_size,
depth: active.depth,
move_idx: active.move_idx,
mask: active.mask,
}
}
}
/// Whether what a widget reports along `axis` is inside the box it drew in.
/// Both are lengths of the window, so the comparison is in its pixels. A
/// share is a length only to whoever divides one, so it is not a claim about
/// this box and cannot exceed it.
impl Size {
fn within_box(self, region: UiRegion, window: PxVec2, axis: Axis) -> bool {
let len = self[axis];
let window = window[axis];
len.leftover != Weight::ZERO
|| len.without_leftover().to_px(window) <= region[axis].len().to_px(window)
}
}
impl UiRegion {
/// A box in a fresh region node keeps its window-unit length and starts
/// at that node's origin.
fn at_origin(self) -> UiRegion {
let size = self.size();
UiRegion::new(
UiSpan::new(Len::ZERO, size.x),
UiSpan::new(Len::ZERO, size.y),
)
}
/// A region node changes only the origin. A full relative span anchored at
/// the box start composes as that translation in both the CPU and shader.
fn as_translation(self) -> UiRegion {
UiRegion {
x: UiSpan::new(self.x.start, self.x.start + Len::FULL),
y: UiSpan::new(self.y.start, self.y.start + Len::FULL),
}
}
}
impl Default for UiRenderState {
fn default() -> Self {
Self::new()
}
}