#[cfg(feature = "layout-diagnostics")] use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind}; use crate::ui::painter::{declared_box, declared_lens, placed_box}; use crate::{ ActiveData, Axis, DrawLayers, Holds, IdLike, LayoutLen, Len, MaskIdx, MoveIdx, Moves, Painter, PixelRegion, Px, PxVec2, RegionAlign, Rel, Size, StrongWidget, UiRegion, UiRsc, UiSpan, 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 box. #[derive(Clone, Copy)] pub(super) struct DrawInfo { pub layer: usize, pub parent: Option, pub depth: usize, pub parent_move: MoveIdx, pub region_node: bool, pub mask: MaskIdx, /// The box it was first asked about in, as a part of its parent's, and /// that box in pixels. pub offer: UiRegion, pub offered_px: PxVec2, /// A container's answer for where the widget sits. `None` uses the /// widget's own property. pub align: Option, } pub struct UiRenderState { pub active: HashMap, pub layers: DrawLayers, pub(super) output_size: PxVec2, old_root: Option, /// The slot every chain bottoms out in, holding the output as a box. root_move: MoveIdx, /// Whether the output has changed since the last update. A frame is /// owed for that whether or not anything has to be drawn again. 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, /// Answers invalidated by a declared-length change below them. These are /// replaced even when retained placement means the redraw is not at the /// old offer. answer_invalid: crate::util::HashSet, /// Whether this frame contains a declared-length change, so any dirty /// dependent replaces its answer too. replace_answers: bool, 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(), answer_invalid: Default::default(), replace_answers: false, moves: Default::default(), root_move: MoveIdx::NONE, resized: false, } } /// The window as a box, so a chain bottoms out in one rather than in a /// multiplication applied after it. Composing through a box held in /// pixels leaves everything below it in pixels, which is why nothing /// downstream has to know the output's size to resolve a position. fn write_root(&mut self) { let region = UiRegion::new( UiSpan::new(Len::ZERO, Len::from_parts(Rel::ZERO, self.output_size.x)), UiSpan::new(Len::ZERO, Len::from_parts(Rel::ZERO, self.output_size.y)), ); match self.root_move == MoveIdx::NONE { true => self.root_move = self.moves.push(MoveIdx::NONE, region), false => self.moves.set(self.root_move, region), } } /// The window, in whatever the platform measures it in, onto the grid /// everything below it is decided on. pub fn resize(&mut self, size: impl Into) { let size = PxVec2::from_f32(size.into()); if size == self.output_size { return; } self.output_size = size; self.write_root(); self.resized = true; } fn root_info(&self) -> DrawInfo { DrawInfo { layer: 0, parent: None, depth: 1, parent_move: self.root_move, region_node: false, mask: MaskIdx::NONE, offer: UiRegion::FULL, offered_px: self.output_size, align: None, } } pub fn output_size(&self) -> PxVec2 { 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); // 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 let Some(root) = root && self.resized { // The output is the root's box, so a resize is that box changing // length, found the way every other box change is found. Before // anything dirty settles, so that whatever a new output draws // again is drawn once, in the box it will have. let info = self.root_info(); let region = Self::root_region(root.id(), rsc.widgets()); let answer = self.draw_inner(root.id(), region, info, None, rsc); self.active.get_mut(&root.id()).unwrap().answer = answer; } self.resized = false; if rsc.widgets().has_updates() { self.redraw_updates(rsc); } self.replace_answers = false; 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); // free all resources & cache self.write_root(); if let Some(id) = root { let info = self.root_info(); let region = Self::root_region(id.id(), rsc.widgets()); self.draw_inner(id.id(), region, info, None, rsc); } } fn root_region(id: WidgetId, widgets: &Widgets) -> UiRegion { declared_box( UiRegion::FULL, declared_lens(widgets, id), widgets.alignment(id), ) } pub(super) fn draw_inner( &mut self, id: WidgetId, region: UiRegion, info: DrawInfo, mut old: Option, rsc: &mut dyn UiRsc, ) -> (Size, [Holds; 2]) { #[cfg(feature = "layout-diagnostics")] { diag::bump(Counter::DrawRequests); diag::draw_request( id, info.parent, region, self.px_of(info.parent_move, region), info.region_node, ); } let own_align = rsc.widgets().alignment(id); let align = info.align.unwrap_or(own_align); let replace_answer = self.answer_invalid.remove(&id) || (self.replace_answers && (rsc.widgets().needs_redraw.contains(&id) || self.dirty_size_under(id, rsc.widgets()))); let retained = match replace_answer { true => None, false => self .retained_answer(id, region, info, rsc.widgets()) .or_else(|| self.try_reuse(id, region, info, rsc)), }; let answer = retained.unwrap_or_else(|| { if old.is_none() { old = self.remove(id, false, rsc); } self.draw_at(id, region, info, align, old.take(), rsc) }); let declared = declared_lens(rsc.widgets(), id); // A near-edge override means the caller already chose this box from // the child's answer. Applying the answer again would compound the // placement; it is also how the second, final ask terminates. let placed = match info.align == Some(RegionAlign::NEAR) { true => region, false => placed_box(region, answer.0, align, declared), }; let placed_info = DrawInfo { align: Some(RegionAlign::NEAR), ..info }; // The symbolic box can be unchanged while its parent slot changed // pixel size. Reuse checks the resolved box even in that case. if self.try_reuse(id, placed, placed_info, rsc).is_none() { #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::PlaceRedraws); let old = self.remove(id, false, rsc); self.draw_at(id, placed, placed_info, RegionAlign::NEAR, old, rsc); } // The answer is only reusable while both parts of the operation are: // what the widget reported in the offered box, and what it drew in // the box its report selected. Express the latter's contract back in // terms of the offered box before handing it to the parent. let drawing_holds = self.active[&id].holds; let mut settled = answer; for axis in AXES { let reported = answer.0.axis(axis); let placed_len = match reported.leftover != Weight::ZERO || declared[axis as usize].is_some() { true => Len::FULL, false => Len::from_parts(reported.rel, reported.px), }; settled.1[axis as usize] = settled.1[axis as usize].and(drawing_holds[axis as usize].through(placed_len)); } let active = self.active.get_mut(&id).unwrap(); active.offer = info.offer; active.answer = settled; active.align = align; active.align_override = info.align.is_some(); active.own_align = own_align; active.depth = info.depth; settled } /// Calls a widget's `draw` and keeps what it drew in `region`. fn draw_at( &mut self, id: WidgetId, region: UiRegion, info: DrawInfo, align: RegionAlign, old: Option, rsc: &mut dyn UiRsc, ) -> (Size, [Holds; 2]) { let (move_idx, local, retired_move) = match info.region_node { // Its box becomes its movable region, so it draws in that // region's coordinates and its box is one entry to rewrite. true => ( self.move_slot(id, info.parent_move, region), UiRegion::FULL, 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 (old_children, old_answer) = match old { Some(old) => (old.children, Some(old.answer)), None => (Vec::new(), None), }; rsc.widgets_mut().needs_redraw.remove(&id); let px = self.px_of(move_idx, local); let at_offer = same_px(px, info.offered_px); let mut painter = Painter { state: self, region: local, mask: info.mask, layer: info.layer, own_layer: info.layer, id, textures: Vec::new(), primitives: Vec::new(), children: Vec::new(), offered: Vec::new(), offered_px: info.offered_px, at_offer, size_deps: Vec::new(), own: [Holds::ANY; 2], under: [Holds::ANY; 2], 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: _, region: _, mask, textures, primitives, children, offered: _, offered_px: _, at_offer: _, size_deps, own, 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. let rules = rsc.widgets().size_rules(id); let size = Size { x: rules.x.apply(size.x), y: rules.y.apply(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| within_box(size, px, axis)), "'{}' ({id:?}) clips to {px:?} and reports {size}", rsc.widgets().label(id), ); let holds = [own[0].and(under[0]), own[1].and(under[1])]; debug_assert!( holds[0].contains(px.x) && holds[1].contains(px.y), "'{}' ({id:?}) drew in {px:?}, outside the ranges it reported: {holds:?}", rsc.widgets().label(id), ); for c in &old_children { if !children.contains(c) { self.undraw_rec(*c, rsc); } } if let Some(idx) = retired_move { self.moves.remove(idx); } // 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, offer: UiRegion::FULL, offered_px: px, align: None, }, rsc, ); rsc.widgets_mut().needs_redraw.remove(&dep); } } let active = ActiveData { id, region, offer: info.offer, // Whoever asked writes the answer, if this was the asking. answer: old_answer.unwrap_or((size, holds)), size, holds, drawn: true, parent: info.parent, depth: info.depth, textures, primitives, children, size_deps, declared: declared_lens(rsc.widgets(), id), align, align_override: info.align.is_some(), own_align: rsc.widgets().alignment(id), move_idx, parent_move: info.parent_move, mask, layer: info.layer, }; rsc.on_draw(&active); self.active.insert(id, active); (size, 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 pixel size of a region held in `slot`'s coordinates. pub(super) fn px_of(&self, slot: MoveIdx, region: UiRegion) -> PxVec2 { self.moves .resolve(slot, region) .size() .to_px(self.output_size) } /// A clean widget's retained answer, if that answer holds for a box of /// `px`. This does not move its drawing, which may already be in the box /// that answer placed it in. pub(super) fn retained_size( &self, id: WidgetId, px: PxVec2, parent_move: MoveIdx, widgets: &Widgets, ) -> Option<(Size, [Holds; 2])> { if widgets.needs_redraw.contains(&id) || self.dirty_size_under(id, widgets) { return None; } let active = self.active.get(&id)?; let (size, holds) = active.answer; let valid = active.drawn && active.parent_move == parent_move && holds[0].contains(px.x) && holds[1].contains(px.y); valid.then_some((size, holds)) } /// 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. fn retained_answer( &self, id: WidgetId, region: UiRegion, info: DrawInfo, widgets: &Widgets, ) -> Option<(Size, [Holds; 2])> { if widgets.needs_redraw.contains(&id) || self.dirty_size_under(id, widgets) { return None; } 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 px = self.px_of(info.parent_move, region); let (size, holds) = active.answer; (holds[0].contains(px.x) && holds[1].contains(px.y)).then_some((size, holds)) } /// Whether anything whose size this widget's own size was read from is /// dirty. Not needed for the answer to come right -- a changed size /// reaches its reader in any order -- but a reader that asks first /// lays out once rather than twice. fn dirty_size_under(&self, id: WidgetId, widgets: &Widgets) -> bool { self.active.get(&id).is_some_and(|active| { active.size_deps.iter().any(|child| { widgets.needs_redraw.contains(child) || self.dirty_size_under(*child, widgets) }) }) } /// The first box a widget was asked about, re-expressed in the coordinate /// space its drawing uses. Keeping the relative box and composing it /// again avoids rebuilding a shifted box from rounded pixel lengths. fn offered_region(&self, id: WidgetId) -> UiRegion { let active = &self.active[&id]; let parent_region = match active.parent.and_then(|id| self.active.get(&id)) { Some(parent) if parent.move_idx == active.parent_move => { if parent.move_idx == parent.parent_move { self.offered_region(parent.id) } else { UiRegion::FULL } } _ => UiRegion::FULL, }; let mut offered = match active.offer == UiRegion::FULL { true => parent_region, false => active.offer.within(&parent_region), }; for axis in AXES { if active.declared[axis as usize].is_some() { *offered.axis_mut(axis) = *active.region.axis(axis); } } offered } /// Reuses the actual drawing in a new box if its retained contract holds /// there. Answers retained from a different ask are handled separately. fn try_reuse( &mut self, id: WidgetId, region: UiRegion, info: DrawInfo, rsc: &mut dyn UiRsc, ) -> Option<(Size, [Holds; 2])> { #[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; } // 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 `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. if !active.holds_at(self.px_of(info.parent_move, region)) { #[cfg(feature = "layout-diagnostics")] { diag::bump(Counter::ReuseOutside); diag::reuse(id, ReuseOutcome::Outside); } return None; } let moved = active.region != region; let (answer, old_region, slot, mask) = ( (active.size, active.holds), active.region, active.move_idx, info.mask, ); if moved { if has_region_node { self.moves.set(slot, region); } else { let remap = RegionRemap::new(old_region, region)?; self.remap_subtree(id, &remap, info.parent_move, mask, rsc); } } let active = self.active.get_mut(&id).unwrap(); active.region = region; active.offer = info.offer; active.depth = info.depth; #[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 }, ); } Some(answer) } /// Re-expresses an ordinary retained subtree in a new parent region. /// An independently movable descendant needs only its own region changed; /// its contents stay in that region's coordinate space. fn remap_subtree( &mut self, id: WidgetId, remap: &RegionRemap, parent_move: MoveIdx, inherited_mask: MaskIdx, rsc: &mut dyn UiRsc, ) { let active = self.active.get_mut(&id).unwrap(); if active.move_idx != parent_move { let region = remap.apply(active.region); active.region = region; self.moves.set(active.move_idx, region); return; } for handle in &active.primitives { let region = self.layers[handle.layer].region_mut(handle); *region = remap.apply(*region); } active.region = remap.apply(active.region); let mask = active.mask; let children = active.children.len(); if mask != inherited_mask && mask != MaskIdx::NONE { let mask = rsc.ui_mut().masks.get_mut(mask); debug_assert_eq!(mask.move_idx, parent_move); mask.region = remap.apply(mask.region); } for index in 0..children { let child = self.active[&id].children[index]; self.remap_subtree(child, remap, parent_move, mask, 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)) }) } /// Takes a widget's record out and frees what it drew. 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.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, region: UiRegion::FULL, offer: UiRegion::FULL, answer: (size, [Holds::ANY; 2]), size, holds: [Holds::ANY; 2], drawn: false, parent: info.parent, depth: info.depth, textures: Vec::new(), primitives: Vec::new(), children: Vec::new(), size_deps: Vec::new(), move_idx: info.parent_move, declared: [None; 2], align: RegionAlign::default(), align_override: false, own_align: rsc.widgets().alignment(id), parent_move: info.parent_move, 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.answer_invalid.clear(); self.replace_answers = false; self.moves.clear(); self.root_move = MoveIdx::NONE; self.layers.clear(); 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); self.answer_invalid.remove(&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: a reader whose children have all settled asks each // once, where any other order has it lay out again for whatever // settles under it afterwards. Equal-depth widgets are independent, // so their order does not matter. while let Some(id) = { let dirty = rsc.widgets().needs_redraw.iter().copied(); dirty.max_by_key(|&id| self.depth(id)) } { #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::QueuePops); self.redraw(id, rsc); } } 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>) -> 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 || 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 { 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. `None` for one /// that is not drawn. pub fn window_region(&self, id: &impl IdLike) -> Option { let active = self.active.get(&id.id())?; if !active.drawn { return None; } let region = self.moves.resolve(active.parent_move, active.region); Some(region.to_px(self.output_size)) } /// Settles a dirty widget: asks it again where its parent asked, and /// tells the parent if the answer changed. pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) { rsc.widgets_mut().needs_redraw.remove(&id); let Some(active) = self.active.get(&id) else { return; }; // 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. let declared_changed = declared_lens(rsc.widgets(), id) != active.declared; let alignment_changed = rsc.widgets().alignment(id) != active.own_align; if let Some(parent) = active.parent && (declared_changed || alignment_changed || !active.drawn) { if declared_changed { self.replace_answers = true; let mut at = Some(id); while let Some(next) = at { self.answer_invalid.insert(next); rsc.widgets_mut().needs_redraw.insert(next); at = self.active[&next].parent; } } rsc.widgets_mut().needs_redraw.insert(id); self.redraw(parent, rsc); // Whatever the parent did not draw again is nothing it holds now. rsc.widgets_mut().needs_redraw.remove(&id); return; } if !active.drawn { return; } let region = active.region; let region_node = active.parent.is_some() && rsc.widgets().is_region_node(id); let asked_in = match active.parent { Some(_) => self.offered_region(id), None => Self::root_region(id, rsc.widgets()), }; let offered_px = self.px_of(active.parent_move, asked_in); let at_offer = same_px(self.px_of(active.parent_move, region), offered_px); let parent_must_place = active.parent.is_some() && (!region_node || active.align_override) && !same_pixel_region( self.moves .resolve(active.parent_move, region) .to_px(self.output_size), self.moves .resolve(active.parent_move, asked_in) .to_px(self.output_size), ); // An independently positioned region node can redraw at its offer // and move its slot to its own placement. Every other widget needs // its parent to reproduce a different final position. if let Some(parent) = active.parent && parent_must_place { rsc.widgets_mut().needs_redraw.insert(id); self.redraw(parent, rsc); rsc.widgets_mut().needs_redraw.remove(&id); return; } let info = DrawInfo { layer: active.layer, parent: active.parent, depth: active.depth, parent_move: active.parent_move, region_node, mask: active.mask, offer: active.offer, offered_px, align: active.align_override.then_some(active.align), }; let (was_answer, was) = (active.answer, (active.size, active.holds)); #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::LocalRedraws); let old = self.remove(id, false, rsc); let answer = self.draw_inner(id, asked_in, info, old, rsc); self.active.get_mut(&id).unwrap().answer = answer; let Some(parent) = info.parent else { return; }; if answer != was_answer { // Left where it was asked: the parent lays out again and chooses // its final box. #[cfg(feature = "layout-diagnostics")] { diag::bump(Counter::SizeChanges); diag::bump(Counter::ReaderEdges); } rsc.widgets_mut().needs_redraw.insert(parent); return; } if at_offer { return; } // Then in the final box its parent chose from that answer. It is kept // if it holds there; otherwise its result is the parent's business. self.draw_inner(id, region, info, None, rsc); let active = &self.active[&id]; if (active.size, active.holds) != was { rsc.widgets_mut().needs_redraw.insert(parent); } } } /// Whether what a widget reports along `axis` is inside the box it drew in. /// A share is a length only to whoever divides one, so it is not a claim /// about this box and cannot exceed it. fn within_box(size: Size, px: PxVec2, axis: Axis) -> bool { let len = size.axis(axis); let box_len = px.axis(axis); len.leftover != Weight::ZERO || box_len.mul(len.rel) + len.px <= box_len } /// The same box is the same number of steps, both of these being lengths on /// the grid rather than floats to be compared for nearness. fn same_px(a: PxVec2, b: PxVec2) -> bool { a == b } fn same_pixel_region(a: PixelRegion, b: PixelRegion) -> bool { same_px(a.top_left, b.top_left) && same_px(a.bot_right, b.bot_right) } /// A retained region rewritten from one parent box into another. A fixed /// source extent can be translated but cannot recover fractions for a resize. #[derive(Clone, Copy)] struct RegionRemap { axes: [AxisRemap; 2], } /// Moving one axis of a box into another, worked out once for the whole /// subtree that moves with it. Every part of that subtree is divided by the /// same extent and placed between the same two ends, so the ends and the /// divisor belong here rather than in each part's arithmetic. #[derive(Clone, Copy)] enum AxisRemap { /// A box that kept its length carries its parts by moving them, which is /// exact. Dividing to find the fraction each sits at and multiplying to /// place it again are two roundings, and they land a step from where /// growing the tree that way does. Translate(Len), /// A box that changed length has to re-express each part as a fraction of /// the new one, which is what a part of a box means. Scale(AxisScale), } #[derive(Clone, Copy)] struct AxisScale { /// What the fraction is measured from, and what divides it. `whole` is /// the common case of a box spanning the whole of its parent's, where /// dividing by one is the expensive way to write a subtraction. start_rel: Rel, extent: Rel, whole: bool, /// `lerp` is `a + (b - a) * fraction`, and both ends are the same for /// every part, so each is kept as its near end and its span. from_px: Px, from_px_span: Px, to_rel: Rel, to_rel_span: Rel, to_px: Px, to_px_span: Px, } impl RegionRemap { fn new(from: UiRegion, to: UiRegion) -> Option { Some(Self { axes: [AxisRemap::new(from.x, to.x)?, AxisRemap::new(from.y, to.y)?], }) } fn apply(&self, region: UiRegion) -> UiRegion { // A box that only moved carries every part of itself by the same two // amounts, and that is the common move. Asking it once for the whole // region is what lets it be eight adds in a row rather than four // sequences with a branch each -- measured, it is where the time in a // move goes. if let [AxisRemap::Translate(x), AxisRemap::Translate(y)] = self.axes { return region.translated(x, y); } UiRegion { x: self.axes[0].apply_span(region.x), y: self.axes[1].apply_span(region.y), } } } impl AxisRemap { fn new(from: UiSpan, to: UiSpan) -> Option { if from.len() == to.len() { return Some(Self::Translate(to.start - from.start)); } let extent = from.end.rel - from.start.rel; // Without a relative extent there is no fraction to re-express: a box // of fixed length cannot say where its parts sit in a different one. if extent == Rel::ZERO { return None; } Some(Self::Scale(AxisScale { start_rel: from.start.rel, extent, whole: extent == Rel::ONE, from_px: from.start.px, from_px_span: from.end.px - from.start.px, to_rel: to.start.rel, to_rel_span: to.end.rel - to.start.rel, to_px: to.start.px, to_px_span: to.end.px - to.start.px, })) } fn apply_span(&self, span: UiSpan) -> UiSpan { UiSpan { start: self.apply_scalar(span.start), end: self.apply_scalar(span.end), } } fn apply_scalar(&self, scalar: Len) -> Len { let scale = match self { Self::Translate(by) => return scalar + *by, Self::Scale(scale) => scale, }; let offset = scalar.rel - scale.start_rel; let fraction = match scale.whole { true => offset, false => offset / scale.extent, }; let from_px = scale.from_px + scale.from_px_span.scaled(fraction); let to_rel = scale.to_rel + scale.to_rel_span.scaled(fraction); let to_px = scale.to_px + scale.to_px_span.scaled(fraction); Len::from_parts(to_rel, scalar.px - from_px + to_px) } } impl Default for UiRenderState { fn default() -> Self { Self::new() } }