#[cfg(feature = "layout-diagnostics")] use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind}; use crate::{ ActiveData, Axis, DrawLayers, IdLike, MaskIdx, MoveIdx, Moves, OnResize, Painter, PixelRegion, Size, StrongWidget, UiRegion, UiRsc, WidgetId, Widgets, util::{HashMap, HashSet, Vec2}, }; const AXES: [Axis; 2] = [Axis::X, Axis::Y]; const LAYOUT_EPSILON_PX: f32 = 0.05; fn pixel_len_changed(old: f32, new: f32) -> bool { (old - new).abs() > LAYOUT_EPSILON_PX } pub struct UiRenderState { pub active: HashMap, pub layers: DrawLayers, pub(super) output_size: Vec2, old_root: Option, resized: [bool; 2], /// Content/state dirtiness whose retained size cannot answer a layout /// question until that widget has drawn again. invalid_sizes: HashSet, /// Marks introduced only to traverse resize dependency paths. Unlike /// content dirtiness, these may retain an answer whose observed pixel /// axes did not change. resize_marks: HashSet, /// What has already been drawn during the pass under way, so a widget /// reached by redrawing an ancestor is not drawn again on its own /// account. Emptied when the pass ends. draw_started: HashSet, /// 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, pub moves: Moves, } impl UiRenderState { pub fn new() -> Self { Self { active: Default::default(), layers: Default::default(), output_size: Vec2::ZERO, old_root: None, resized: [false; 2], invalid_sizes: Default::default(), resize_marks: Default::default(), draw_started: Default::default(), slots: Default::default(), moves: Default::default(), } } pub fn resize(&mut self, size: impl Into) { let size = size.into(); for (axis, resized) in AXES.into_iter().zip(self.resized.iter_mut()) { *resized |= size.axis(axis) != self.output_size.axis(axis); } self.output_size = size; } pub fn output_size(&self) -> Vec2 { self.output_size } pub fn update<'a>(&mut self, root: impl Into>, rsc: &mut dyn UiRsc) { #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::Updates); #[cfg(feature = "layout-diagnostics")] let _update = diag::timer(TimerKind::Update); self.invalid_sizes.clear(); self.invalid_sizes .extend(rsc.widgets().needs_redraw.iter().copied()); self.resize_marks.clear(); // 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()); } else if self.resized.iter().any(|&resized| resized) { // A region is a fraction of the output plus an offset, resolved // against the window in the shader, so a resize moves the whole // drawing on its own. Only a widget that read pixels can be wrong. { #[cfg(feature = "layout-diagnostics")] let _marking = diag::timer(TimerKind::ResizeMarking); let dependents: Vec<_> = self .active .iter() .filter_map(|(&id, active)| { AXES.into_iter() .zip(self.resized) .any(|(axis, changed)| { changed && active.reads_output[axis as usize] && pixel_len_changed( active.output_px.axis(axis), self.output_size.axis(axis), ) }) .then_some(id) }) .collect(); for id in dependents { #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::ResizeDependents); rsc.widgets_mut().needs_redraw.insert(id); if let Some(top) = self.mark_readers(id, rsc) { rsc.widgets_mut().needs_redraw.insert(top); } } self.resize_marks.extend( rsc.widgets() .needs_redraw .iter() .filter(|id| !self.invalid_sizes.contains(id)) .copied(), ); } } if rsc.widgets().has_updates() { self.redraw_updates(rsc); } self.resized = [false; 2]; self.invalid_sizes.clear(); self.resize_marks.clear(); self.draw_started.clear(); } 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); // free all resources & cache if let Some(id) = root { self.draw_inner( 0, id.id(), UiRegion::FULL, None, MoveIdx::NONE, false, MaskIdx::NONE, None, rsc, ); } } // TODO: should prolly make a DrawInfo struct or smth for everything other than rsc #[allow(clippy::too_many_arguments)] pub(super) fn draw_inner( &mut self, layer: usize, id: WidgetId, region: UiRegion, parent: Option, parent_move: MoveIdx, slotted: bool, mask: MaskIdx, old_children: Option>, rsc: &mut dyn UiRsc, ) -> Size { #[cfg(feature = "layout-diagnostics")] { diag::bump(Counter::DrawRequests); diag::draw_request(id, parent, region, self.px_of(parent_move, region), slotted); } let mut old_children = old_children.unwrap_or_default(); if self.active.contains_key(&id) { if let Some(size) = self.try_reuse(id, region, parent_move, rsc) { return size; } // if not, then maintain resize and track old children to remove unneeded let active = self.remove(id, false, rsc).unwrap(); old_children = active.children; } // draw widget let (move_idx, local) = match slotted { // Its box becomes its slot's, so it draws in the slot's own // coordinates and the box it was given is one entry to rewrite. true => (self.move_slot(id, parent_move, region), UiRegion::FULL), false => { self.drop_slot(id); (parent_move, region) } }; let px = self.px_of(move_idx, local); rsc.widgets_mut().needs_redraw.remove(&id); self.draw_started.insert(id); let mut painter = Painter { state: self, region: local, mask, layer, id, textures: Vec::new(), primitives: Vec::new(), children: Vec::new(), size_deps: Vec::new(), size_box_inputs: [false; 2], size_output_inputs: [false; 2], reads_output: [false; 2], 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: _, region: _, mask, textures, primitives, children, size_deps, size_box_inputs, size_output_inputs, reads_output, move_idx, layer, id, } = painter; debug_assert!( Self::hints_agree(id, size, rsc), "'{}' ({id:?}) drew a size its size_hint disagrees with", rsc.widgets().label(id) ); // add to active let active = ActiveData { id, region, size, px, parent, textures, primitives, children, size_deps, size_box_inputs, size_output_inputs, output_px: self.output_size, reads_output, move_idx, parent_move, mask, layer, }; // remove old children that weren't kept for c in &old_children { if !active.children.contains(c) { self.remove_rec(*c, rsc); } } rsc.on_draw(&active); self.active.insert(id, active); self.invalid_sizes.remove(&id); self.resize_marks.remove(&id); size } /// The slot a widget's box is held in, made on its first placed draw and /// kept until it stops being drawn -- a redraw replaces its `ActiveData` /// while descendants go on naming the slot. 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 } /// Gives up a slot a widget no longer needs, because it is drawn somewhere /// that does not place it. Its descendants name it, so this is only /// reached where they are about to be drawn again. fn drop_slot(&mut self, id: WidgetId) { if let Some(idx) = self.slots.remove(&id) { self.moves.remove(idx); } } /// The pixel size of a region held in `slot`'s coordinates. fn px_of(&self, slot: MoveIdx, region: UiRegion) -> Vec2 { self.moves .resolve(slot, region) .size() .to_abs(self.output_size) } /// A clean widget's retained size, when the offered pixel axes which /// produced that answer are unchanged. This observes the old answer only; /// it does not move or otherwise reuse the widget's drawing. pub(super) fn retained_size( &self, id: WidgetId, region: UiRegion, parent_move: MoveIdx, widgets: &Widgets, ) -> Option<(Size, [bool; 2], [bool; 2])> { if self.size_is_invalid(id, widgets) || self.dirty_size_under(id, widgets) { return None; } let active = self.active.get(&id)?; if active.parent_move != parent_move { return None; } let px = self.px_of(parent_move, region); let valid_box = AXES .into_iter() .zip(active.size_box_inputs) .all(|(axis, depends)| { !depends || !pixel_len_changed(active.px.axis(axis), px.axis(axis)) }); let valid_output = AXES.into_iter() .zip(active.size_output_inputs) .all(|(axis, depends)| { !depends || !pixel_len_changed( active.output_px.axis(axis), self.output_size.axis(axis), ) }); (valid_box && valid_output).then_some(( active.size, active.size_box_inputs, active.size_output_inputs, )) } fn size_is_invalid(&self, id: WidgetId, widgets: &Widgets) -> bool { self.invalid_sizes.contains(&id) || (widgets.needs_redraw.contains(&id) && !self.resize_marks.contains(&id)) } fn dirty_size_under(&self, id: WidgetId, widgets: &Widgets) -> bool { self.active.get(&id).is_some_and(|active| { active.size_deps.iter().any(|child| { self.size_is_invalid(*child, widgets) || self.dirty_size_under(*child, widgets) }) }) } /// The drawing a widget already has, kept for a new box if the box has not /// changed in a way it depends on. fn try_reuse( &mut self, id: WidgetId, region: UiRegion, parent_move: MoveIdx, rsc: &mut dyn UiRsc, ) -> Option { #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::ReuseAttempts); // Only its own dirtiness, not anything dirty under it that could // change the size this hands back. What makes that safe is the order // `redraw_updates` settles in, and nothing else: by the time a reader // draws, everything dirty below it has been drawn and has propagated. // Draw in another order and this returns a stale size -- measured, on // seed 2 of `tests/generated.rs`. 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)?; // 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 != parent_move { #[cfg(feature = "layout-diagnostics")] { diag::bump(Counter::ReuseWrongParent); diag::reuse(id, ReuseOutcome::WrongParent); } return None; } let (size, old_region, slot, old_px) = (active.size, active.region, active.move_idx, active.px); // In pixels, because `region` is a fraction of a slot's box and that // box may be what changed -- an unchanged fraction of a box half the // size is half the widget. let px = self.px_of(parent_move, region); let mut changed = [false; 2]; for (axis, c) in AXES.into_iter().zip(changed.iter_mut()) { *c = pixel_len_changed(old_px.axis(axis), px.axis(axis)); } if !changed.iter().any(|&c| c) && old_region == region { #[cfg(feature = "layout-diagnostics")] { diag::bump(Counter::ReuseExact); diag::reuse(id, ReuseOutcome::Exact); } return Some(size); } // Only a placed widget can be given a different box without drawing // again: everything it drew is a fraction of its slot's box, so one // entry says where all of it went. if slot == parent_move { #[cfg(feature = "layout-diagnostics")] { diag::bump(Counter::ReuseUnslotted); diag::reuse(id, ReuseOutcome::Unslotted); } return None; } if changed.iter().any(|&c| c) { let widget = rsc.widgets().get_dyn(id)?; let redraws = AXES .into_iter() .zip(changed) .any(|(axis, c)| c && widget.on_resize(axis) != OnResize::Scale); // Anything under it that has to be drawn again is drawn by drawing // this, because whatever reads that widget's size sits in between // and has to lay out around what it comes to. if redraws { #[cfg(feature = "layout-diagnostics")] { diag::bump(Counter::ReuseOwnResize); diag::reuse(id, ReuseOutcome::OwnResize); } return None; } if self.redraws_under(id, changed, rsc) { #[cfg(feature = "layout-diagnostics")] { diag::bump(Counter::ReuseDescendantResize); diag::reuse(id, ReuseOutcome::DescendantResize); } return None; } } self.moves.set(slot, region); let active = self.active.get_mut(&id).unwrap(); active.region = region; #[cfg(feature = "layout-diagnostics")] { diag::bump(Counter::ReuseMoved); diag::reuse(id, ReuseOutcome::Moved); } Some(size) } /// Whether anything under `id` would have to be drawn again for the box /// it is a fraction of changing length, `changed` saying which axes of /// that box did. /// /// A part of a box with no relative extent on an axis is a fixed length, /// held as offsets from that box's start, and composing anything into it /// leaves no relative extent either. So a widget whose own box did not /// change length has no descendant whose box did, and the walk stops /// there -- an 80-wide child of a widened row is not asked at all. fn redraws_under(&self, id: WidgetId, changed: [bool; 2], rsc: &dyn UiRsc) -> bool { #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::ResizeChecks); let Some(active) = self.active.get(&id) else { return false; }; let size_deps = &active.size_deps; active.children.iter().any(|&child| { #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::ResizeCheckChildren); let Some(data) = self.active.get(&child) else { return false; }; let Some(widget) = rsc.widgets().get_dyn(child) else { return true; }; // What it drew to learn this child's size was the child in *this* // box, so a different box is a different answer -- unless the // child gave an exact one without being drawn at all. if size_deps.contains(&child) { let measured = AXES .into_iter() .zip(changed) .any(|(axis, c)| c && widget.size_hint(axis).is_none()); if measured { return true; } } let mut own = changed; for (axis, c) in AXES.into_iter().zip(own.iter_mut()) { *c &= data.region.axis(axis).len().rel != 0.0; } if !own.iter().any(|&c| c) { return false; } let redraws = AXES .into_iter() .zip(own) .any(|(axis, c)| c && widget.on_resize(axis) != OnResize::Scale); redraws || self.redraws_under(child, own, rsc) }) } 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(axis)) }) } /// NOTE: instance textures are cleared and self.textures freed fn remove(&mut self, id: WidgetId, undraw: bool, rsc: &mut dyn UiRsc) -> Option { let mut active = self.active.remove(&id); if let Some(active) = &mut active { for h in &active.primitives { let mask = self.layers.free(h); if mask != MaskIdx::NONE { rsc.ui_mut().masks.remove(mask); } } active.textures.clear(); rsc.ui_mut().textures.free(); if undraw { rsc.on_undraw(active); } } active } fn remove_rec(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) -> Option { let inst = self.remove(id, true, rsc); if let Some(inst) = &inst { for c in &inst.children { self.remove_rec(*c, rsc); } } // After the descendants, whose slots name this one as their parent. if let Some(idx) = self.slots.remove(&id) { self.moves.remove(idx); } inst } fn clear(&mut self, rsc: &mut dyn UiRsc) { for (_, active) in self.active.drain() { rsc.on_undraw(&active); } self.slots.clear(); self.moves.clear(); self.layers.clear(); self.invalid_sizes.clear(); self.resize_marks.clear(); self.draw_started.clear(); rsc.widgets_mut().needs_redraw.clear(); rsc.free(); } pub fn redraw_updates(&mut self, rsc: &mut dyn UiRsc) { #[cfg(feature = "layout-diagnostics")] let _layout = diag::timer(TimerKind::IncrementalLayout); // A reader's answer is only valid after every dirty size it reads has // settled, and taking the deepest first is what arranges that -- // `try_reuse` hands back a retained size without asking whether // anything dirty sits under it, so this order is load-bearing for the // answer and not only for the cost. Equal-depth widgets are // independent, so their order does not matter. Resize dirtiness already marks whole reader chains, so // choosing their shallowest roots coalesces descendants that share a // reader and gives each changing box its final constraints first. while let Some(id) = { let dirty = rsc.widgets().needs_redraw.iter().copied(); match self.resized.iter().any(|&resized| resized) { true => dirty.min_by_key(|&id| self.depth(id)), false => dirty.max_by_key(|&id| self.depth(id)), } } { #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::QueuePops); self.redraw(id, rsc); } rsc.free(); } fn depth(&self, id: WidgetId) -> usize { let mut depth = 0; let mut at = Some(id); while let Some(id) = at { #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::DepthSteps); at = self.active.get(&id).and_then(|active| active.parent); depth += 1; } depth } pub fn root_changed<'a>(&self, root: impl Into>) -> bool { root.into().map(|r| r.id()) != self.old_root } pub fn needs_redraw<'a>( &self, root: impl Into>, widgets: &Widgets, ) -> bool { self.root_changed(root) || self.resized.iter().any(|&resized| resized) || widgets.has_updates() } pub fn active_widgets(&self) -> usize { self.active.len() } pub fn debug(&self, widgets: &Widgets, label: &str) -> impl Iterator { 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 = 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, which is the walk the vertex shader does. pub fn window_region(&self, id: &impl IdLike) -> Option { let active = self.active.get(&id.id())?; let region = self.moves.resolve(active.parent_move, active.region); Some(region.to_px(self.output_size)) } /// redraws a widget that's currently active (drawn) pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) { self.draw_started.remove(&id); if rsc.widgets().needs_redraw.contains(&id) && !self.resize_marks.contains(&id) { self.invalid_sizes.insert(id); } // A widget can only answer whether its size changed by drawing in the // box its parent chose. If that box changed in pixels, its retained // placement is stale and the highest size reader must choose the new // box first. Otherwise the widget can draw locally, and its readers // only matter if the returned size actually changed. let box_changed = self.active.get(&id).is_some_and(|active| { let px = self.px_of(active.parent_move, active.region); AXES.into_iter() .any(|axis| pixel_len_changed(active.px.axis(axis), px.axis(axis))) }); if (self.resized.iter().any(|&resized| resized) || box_changed) && let Some(top) = self.mark_readers(id, rsc) { #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::EagerReaderRedraws); self.redraw(top, rsc); rsc.widgets_mut().needs_redraw.remove(&id); return; } rsc.widgets_mut().needs_redraw.remove(&id); if self.draw_started.contains(&id) { return; } let Some(active) = self.remove(id, false, rsc) else { return; }; #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::LocalRedraws); let old_size = active.size; let size = self.draw_inner( active.layer, id, active.region, active.parent, active.parent_move, active.move_idx != active.parent_move, active.mask, Some(active.children), rsc, ); if size != old_size { #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::SizeChanges); if let Some(parent) = self.active.get(&id).and_then(|active| active.parent) && self .active .get(&parent) .is_some_and(|active| active.size_deps.contains(&id)) { // Propagate one dependency edge at a time. If drawing the reader // does not change its own size, nothing above it can observe this. rsc.widgets_mut().needs_redraw.insert(parent); self.invalid_sizes.insert(parent); #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::ReaderEdges); } } } /// The furthest ancestor that read this widget's size, directly or through /// widgets that did the same, marking everything below it on the way. fn mark_readers(&self, id: WidgetId, rsc: &mut dyn UiRsc) -> Option { let mut top = None; let mut at = id; while let Some(active) = self.active.get(&at) && let Some(parent) = active.parent && self .active .get(&parent) .is_some_and(|p| p.size_deps.contains(&at)) { rsc.widgets_mut().needs_redraw.insert(at); top = Some(parent); at = parent; } top } } impl Default for UiRenderState { fn default() -> Self { Self::new() } }