#[cfg(feature = "layout-diagnostics")] use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind}; use crate::ui::painter::{declared_lens, frame_and_extent, part_of, placed_extent}; use crate::{ ActiveData, Axis, DrawLayers, Holds, IdLike, LayoutHolds, LayoutLen, Len, MaskIdx, MoveIdx, Moves, Painter, Part, PixelRegion, Place, PxVec2, Size, StrongWidget, UiRegion, UiRsc, 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, pub depth: usize, pub parent_move: MoveIdx, pub region_node: bool, pub mask: MaskIdx, /// The frame in the parent widget's frame coordinates, before /// composition. Its length is the same on every ask of the widget. pub frame: UiRegion, /// That frame composed into `parent_move`'s coordinates, which is what /// the widget's own drawing is written within. pub frame_abs: UiRegion, /// The box the drawing is given, in the frame's own coordinates: the /// part of the parent's own box that `place` names, before the widget's /// answer is placed inside it. pub part: UiRegion, /// What of the parent's extent the drawing was given, and what it was /// given at the parent's first ask of it. See [`Place`]. pub place: [Place; 2], pub offer_place: [Place; 2], /// Whether this ask is the one the widget's answer is kept from: the /// first box its parent asked about, in the parent's own measuring draw. pub offer: bool, /// The frame in pixels: one multiply from the parent's own, which is /// where every pixel length in layout comes from. pub px: PxVec2, } impl DrawInfo { /// The axes where the part is the drawing's box outright, which are the /// axes the answer is not placed inside it again. fn fill(&self) -> [bool; 2] { self.place.map(Place::fills) } } /// 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. struct Placing { id: WidgetId, extent: UiRegion, /// The widget's frame in the coordinates its children compose within: /// `FULL` where it is a region node, since its box is that node. local: UiRegion, px: PxVec2, depth: usize, move_idx: MoveIdx, mask: MaskIdx, } pub struct UiRenderState { pub active: HashMap, pub layers: DrawLayers, pub(super) output_size: PxVec2, old_root: Option, /// 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, /// 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, /// 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, 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| { let px = active.frame.size().to_px(size); // Nothing above the root chose anything, so the box it was first // asked about is the whole of its frame. let offer = part_of(UiRegion::FULL, active.offer_place); active.answers_at(px, offer) && active.holds.contains(px, active.extent) }); if !stands { widgets.needs_redraw.insert(root); } } /// The root is asked about in the output: the window is where a fraction /// becomes pixels rather than a box of its own, so the root's box is the /// first length threaded down. Its own rules narrow that box, and where /// they do the narrowed box is also the offer -- nothing above it chose /// anything else. fn root_info(&self, region: UiRegion) -> DrawInfo { let px = region.size().to_px(self.output_size); DrawInfo { layer: 0, parent: None, depth: 1, parent_move: MoveIdx::NONE, region_node: false, mask: MaskIdx::NONE, frame: region, frame_abs: region, part: UiRegion::FULL, place: [Place::Within(Part::All); 2], offer_place: [Place::Within(Part::All); 2], offer: true, px, } } 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()); } 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 region = Self::root_region(id.id(), rsc.widgets()); let info = self.root_info(region); self.draw_inner(id.id(), info, None, rsc); } } /// The root's frame: the window, narrowed by the root's own rules. Its /// extent is that frame, since nothing above it chose anything else. fn root_region(id: WidgetId, widgets: &Widgets) -> UiRegion { let declared = declared_lens(widgets, id); let narrow = AXES.map(|axis| declared[axis as usize].map(|len| Len::from_parts(len.rel, len.px))); frame_and_extent( UiRegion::FULL, UiRegion::FULL, narrow, widgets.alignment(id), ) .0 } pub(super) fn draw_inner( &mut self, id: WidgetId, info: DrawInfo, mut old: Option, rsc: &mut dyn UiRsc, ) -> (Size, LayoutHolds, LayoutHolds) { let (frame, part) = (info.frame_abs, info.part); #[cfg(feature = "layout-diagnostics")] { diag::bump(Counter::DrawRequests); diag::draw_request(id, info.parent, frame, info.px, info.region_node); } let align = rsc.widgets().alignment(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); let retained = match stale { true => None, false => self .retained_answer(id, part, info) .or_else(|| self.try_reuse(id, frame, part, info, rsc)), }; let answer = retained.unwrap_or_else(|| { if old.is_none() { old = self.remove(id, false, rsc); } self.draw_at(id, part, info, old.take(), rsc) }); // Where the drawing goes, in the frame's own coordinates: the part // its parent gave it, with the answer placed inside that part on any // axis the parent left open. The frame itself does not change, so // nothing under it resolves a fraction a second time. // // From the answer it gave when its parent first asked, and not from // what a placing evaluation reported: a drawing made in the box that // answer chose is answering a different question, and placing it by // that would move the box out from under itself. let measured = match info.offer { true => answer.0, false => self.active[&id].answer.map_or(answer.0, |(size, _)| size), }; let extent = placed_extent( part, measured, declared_lens(rsc.widgets(), id), info.fill(), align, ); self.place(id, extent, info, rsc); // On axes the parent filled, measurement and drawing share an extent. // Otherwise the answer fixes the final extent as a function of the // frame, so pull that drawing's validity back through it. let drawing_holds = self.active[&id].holds; let mut settled = answer; for axis in AXES { let n = axis as usize; settled.1.frame[n] = settled.1.frame[n].and(drawing_holds.frame[n]); if info.fill()[n] { settled.1.extent[n] = settled.1.extent[n].and(drawing_holds.extent[n]); } else { settled.1.frame[n] = settled.1.frame[n] .and(drawing_holds.extent[n].through(extent.axis(axis).len())); } } let active = self.active.get_mut(&id).unwrap(); // Whoever asked owns how the boxes were reached: the frame it stated, // and what of its own box it gave the drawing. A local redraw asks // the same question again from these. active.frame_abs = frame; active.frame = info.frame; if info.offer { active.answer = Some(answer); active.offer_place = info.offer_place; active.offer_part = part; } active.place = info.place; active.own_align = align; // A subtree can be reused whole under a different parent -- same box, // same layer, same region node -- and nothing in the drawing says it // changed hands. Two things read who its parent is: a deferral, which // marks whoever has it to draw, and the old parent's list of children, // which its next draw undraws whatever is missing from. let old_parent = std::mem::replace(&mut 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, settled.1) } /// Recompose retained geometry when the evaluation still holds at this extent. fn place(&mut self, id: WidgetId, extent: UiRegion, info: DrawInfo, rsc: &mut dyn UiRsc) { if self .try_reuse(id, info.frame_abs, extent, info, rsc) .is_some() { return; } #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::PlaceRedraws); let old = self.remove(id, false, rsc); self.draw_at(id, extent, info, old, rsc); } /// Calls a widget's `draw` and keeps what it drew in `extent` of `frame`. fn draw_at( &mut self, id: WidgetId, extent: UiRegion, info: DrawInfo, old: Option, rsc: &mut dyn UiRsc, ) -> (Size, LayoutHolds) { let frame = info.frame_abs; 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, frame), 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, frame, self.slots.remove(&id)), }; let (old_children, old_answer, old_offer_part) = match old { Some(old) => (old.children, old.answer, Some(old.offer_part)), None => (Vec::new(), None, None), }; rsc.widgets_mut().needs_redraw.remove(&id); // Only evaluation at the original offer establishes the children's // offers. A placing evaluation must not overwrite that question. let px = info.px; let at_offer = info.offer; let mut painter = Painter { state: self, frame: local, extent, extent_len: [None; 2], px, mask: info.mask, layer: info.layer, own_layer: info.layer, id, textures: Vec::new(), primitives: Vec::new(), mask_region: None, children: Vec::new(), offered: Vec::new(), at_offer, size_deps: Vec::new(), own: [Holds::ANY; 2], under: LayoutHolds::ANY, extent_own: [Holds::ANY; 2], 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: _, frame: _, extent: _, px: _, mask, textures, primitives, mask_region, extent_own, extent_len, answer_under, children, offered: _, 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), ); for c in &old_children { if !children.contains(c) { self.undraw_rec(*c, rsc); } } if let Some(idx) = retired_move { self.moves.remove(idx); } let own_holds = LayoutHolds { frame: own, extent: extent_own, extent_len, }; let answer_holds = own_holds.and(answer_under); let holds = answer_holds.and(under); debug_assert!( holds.contains(px, extent), "'{}' ({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, frame: UiRegion::FULL, frame_abs: UiRegion::FULL, part: UiRegion::FULL, place: [Place::Within(Part::All); 2], offer_place: [Place::Within(Part::All); 2], offer: false, px, }, rsc, ); rsc.widgets_mut().needs_redraw.remove(&dep); } } let active = ActiveData { id, frame_abs: frame, extent, frame: info.frame, place: info.place, offer_place: info.offer_place, offer_part: old_offer_part.unwrap_or(extent), // Whoever asked writes the answer, if this was the asking. answer: old_answer, size, holds, drawn: true, parent: info.parent, depth: info.depth, textures, primitives, mask_region, children, size_deps, declared: declared_lens(rsc.widgets(), 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, part: 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(info.px, part).then_some(answer) } /// The pixel lengths of a widget's frame, which is what a local redraw /// needs to ask the question its parent asked. /// /// It is threaded down from the window a length of a box at a time, and /// this takes the same steps back up: a widget's frame is a length of its /// parent's frame, and that chain has no coordinate frame in it, so a /// region node cannot break it -- and it lands on the number a cold /// layout computes rather than near it. fn asked_px(&self, id: WidgetId) -> PxVec2 { let active = &self.active[&id]; // Nothing above the root: the window is where a fraction becomes // pixels, which is also the whole of the frame the root is given. let parent_px = match active.parent.and_then(|p| self.active.get(&p)) { Some(parent) => self.asked_px(parent.id), None => self.output_size, }; active.frame.size().to_px(parent_px) } /// 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, frame: UiRegion, extent: UiRegion, info: DrawInfo, rsc: &mut dyn UiRsc, ) -> Option<(Size, LayoutHolds)> { #[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 the frame is a fraction of its parent's and // that may be what changed -- an unchanged fraction of a box half the // size is half the widget. if !active.holds.contains(info.px, extent) { #[cfg(feature = "layout-diagnostics")] { // Which of the three said no, so a frame that redraws more // than it should says where to look. They overlap: a drawing // can be outside two of them at once. let holds = active.holds; for axis in AXES { let n = axis as usize; if holds.extent_len[n].is_some_and(|pinned| pinned != extent.axis(axis).len()) { diag::bump(Counter::OutsidePlacement); } if !holds.frame[n].contains(info.px.axis(axis)) { diag::bump(Counter::OutsideFrame); } if !holds.extent[n].contains(extent.axis(axis).len().to_px(info.px.axis(axis))) { diag::bump(Counter::OutsideExtent); } } diag::bump(Counter::ReuseOutside); diag::reuse(id, ReuseOutcome::Outside); } return None; } let extent_moved = active.extent != extent; let moved = active.frame_abs != frame; let (answer, slot) = ((active.size, active.holds), active.move_idx); if moved { if has_region_node { self.moves.set(slot, frame); } else { self.recompose_subtree(id, frame, info.parent_move, rsc); } } if extent_moved { self.reposition(id, frame, extent, info, rsc); } self.redepth(id, info.depth); let active = self.active.get_mut(&id).unwrap(); active.frame_abs = frame; active.frame = info.frame; active.place = info.place; #[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) } /// Places one child of `at.id` in the box that widget's own box gives /// it: its part of the extent, with its answer placed inside that part /// where the ask left the axis open. fn place_child(&mut self, child: WidgetId, at: &Placing, rsc: &mut dyn UiRsc) { let active = &self.active[&child]; let (frame, part) = Self::re_ask(active, at.extent, active.place); // The answer it gave, and not what its last drawing reported: a // drawing made in the box that answer chose is answering a different // question. let answer = active.answer.map_or(active.size, |(size, _)| size); let extent = placed_extent( part, answer, active.declared, active.place.map(Place::fills), 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, frame, frame_abs: frame.within(&at.local), part, place: active.place, offer_place: active.offer_place, // Putting it back where it was asked about is that ask again, so // what it answers there is the answer -- and putting it anywhere // else is not, however the box was arrived at. offer: active.place == active.offer_place, px: frame.size().to_px(at.px), }; self.place(child, extent, info, rsc); } /// The frame and the box a widget being asked again is given, from what /// it already has and where its parent's box is now. A frame's length is /// the same on every ask, so a declared length is put back where it sits /// in the part rather than resolved from its rule a second time. fn re_ask( active: &ActiveData, parent_extent: UiRegion, place: [Place; 2], ) -> (UiRegion, UiRegion) { let narrow = AXES.map(|axis| { let n = axis as usize; active.declared[n] .is_some() .then(|| active.frame.axis(axis).len()) }); frame_and_extent( active.frame, part_of(parent_extent, place), narrow, 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, frame: UiRegion, extent: UiRegion, info: DrawInfo, rsc: &mut dyn UiRsc, ) { let active = self.active.get_mut(&id).unwrap(); active.frame_abs = frame; active.extent = extent; let local = if info.region_node { UiRegion::FULL } else { frame }; for primitive in &active.primitives { let handle = &primitive.handle; *self.layers[handle.layer].region_mut(handle) = primitive.region.within(&extent).within(&local); } if let Some(mask_region) = active.mask_region { rsc.ui_mut().masks.get_mut(active.mask).region = mask_region.within(&extent).within(&local); } let at = Placing { id, extent, local, px: info.px, 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); } } /// Replays the original local compositions, including their rounding order. /// A region node terminates the walk because its contents name its slot. fn recompose_subtree( &mut self, id: WidgetId, frame: UiRegion, parent_move: MoveIdx, rsc: &mut dyn UiRsc, ) { let active = self.active.get_mut(&id).unwrap(); active.frame_abs = frame; if active.move_idx != parent_move { self.moves.set(active.move_idx, frame); return; } let extent = active.extent; for primitive in &active.primitives { let handle = &primitive.handle; *self.layers[handle.layer].region_mut(handle) = primitive.region.within(&extent).within(&frame); } if let Some(local) = active.mask_region { rsc.ui_mut().masks.get_mut(active.mask).region = local.within(&extent).within(&frame); } let children = active.children.len(); for index in 0..children { let child = self.active[&id].children[index]; let local = self.active[&child].frame; self.recompose_subtree(child, local.within(&frame), parent_move, 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 primitive in &active.primitives { let mask = self.layers.free(&primitive.handle); 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, frame_abs: UiRegion::FULL, extent: UiRegion::FULL, frame: UiRegion::FULL, place: [Place::Within(Part::All); 2], offer_place: [Place::Within(Part::All); 2], offer_part: UiRegion::FULL, answer: None, 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: [None; 2], 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.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>) -> 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, 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 { let active = self.active.get(&id.id())?; active.drawn.then(|| { let placed = active.extent.within(&active.frame_abs); self.moves .resolve(active.parent_move, placed) .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. 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 || 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 region = Self::root_region(id, rsc.widgets()); let info = DrawInfo { mask: active.parent_mask, ..self.root_info(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 px = self.asked_px(id); let (was_answer, was_holds) = (active.answer, active.holds); // The boxes its parent gave it, then and now: its frame is the same // on every ask, so the question its parent asked is the one this // asks again -- there is no box here that could be its parent's to // choose instead. let parent_extent = self.active[&parent].extent; 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, frame: active.frame, frame_abs: active.frame_abs, part: Self::re_ask(active, parent_extent, active.place).1, place: active.place, offer_place: active.offer_place, offer: false, px, }; // The ask that measured it, asked again: the box it was measured in // as its parent left it, rather than where that ask's place resolves // to now -- a parent drawn again in the box its own answer chose // gives its children boxes it never measured anything in. let offered = DrawInfo { place: info.offer_place, part: active.offer_part, offer: true, ..info }; #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::LocalRedraws); // Asked again in the box its parent gave it, which is the question // its parent asked only while that box is as long as the one it was // measured in. Any other box is a different question, so the parent // asks it, with the mark left on. Lengths and not whole boxes: what // a drawing depends on is its lengths, so the same lengths elsewhere // is one question. // // The frame is the same on every ask now, so this is about the box // the drawing goes in alone. Removing it -- asking the measuring // question here and placing the answer afterwards -- is what the // transparent-frames plan asks for next, and it does not hold yet: // seeds 104 (align) and 210 (reorder) at depth 5 settle differently // warm and cold without it. if info.part.size() != offered.part.size() { self.mark(id, rsc.widgets_mut()); self.mark(parent, rsc.widgets_mut()); return false; } let old = self.remove(id, false, rsc); // The original measurement is refreshed before the assigned slot is // restored: its lengths may differ even though the frame is // unchanged. let answer = self.draw_inner(id, offered, old, rsc); if info.place != info.offer_place || info.part != offered.part { self.draw_inner(id, info, None, 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(px, active.extent) { 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()); } true } } /// 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 } impl Default for UiRenderState { fn default() -> Self { Self::new() } }