`try_reuse` asks whether the widget in front of it is dirty and, if not, hands its parent the size it last reported. Nothing asks whether a dirty widget sits under it through the size dependencies -- which is the check `retained_size` makes, for exactly this reason, on the path that does not draw. What covers the gap is the order `redraw_updates` settles in: taking the deepest dirty widget first means that by the time a reader draws, what it reads has already drawn and propagated. Drawing in any other order returns a stale size. Measured rather than reasoned: picking whatever the dirty set yields first fails seed 2 of `tests/generated.rs` with 24 widgets wrong, a subtree keeping a 317 px width where a cold tree has 147, and the traces are identical until a `Span` reports 317 against 147 from the same child sizes -- it had reused a subtree holding a `SetSize` whose declared width had changed. So the coupling is real and was written down nowhere. Say it in both places, since a reader of either would otherwise conclude the order is about cost. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
768 lines
28 KiB
Rust
768 lines
28 KiB
Rust
#[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<WidgetId, ActiveData>,
|
|
pub layers: DrawLayers,
|
|
pub(super) output_size: Vec2,
|
|
|
|
old_root: Option<WidgetId>,
|
|
resized: [bool; 2],
|
|
/// Content/state dirtiness whose retained size cannot answer a layout
|
|
/// question until that widget has drawn again.
|
|
invalid_sizes: HashSet<WidgetId>,
|
|
/// 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<WidgetId>,
|
|
/// 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<WidgetId>,
|
|
/// A widget's move slot, which outlives any one `ActiveData`: a redraw
|
|
/// replaces that while its children go on pointing at the slot.
|
|
slots: HashMap<WidgetId, MoveIdx>,
|
|
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<Vec2>) {
|
|
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<Option<&'a StrongWidget>>, rsc: &mut dyn UiRsc) {
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
diag::bump(Counter::Updates);
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
let _update = diag::timer(TimerKind::Update);
|
|
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<WidgetId>,
|
|
parent_move: MoveIdx,
|
|
slotted: bool,
|
|
mask: MaskIdx,
|
|
old_children: Option<Vec<WidgetId>>,
|
|
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<Size> {
|
|
#[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<ActiveData> {
|
|
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<ActiveData> {
|
|
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<Option<&'a StrongWidget>>) -> bool {
|
|
root.into().map(|r| r.id()) != self.old_root
|
|
}
|
|
|
|
pub fn needs_redraw<'a>(
|
|
&self,
|
|
root: impl Into<Option<&'a StrongWidget>>,
|
|
widgets: &Widgets,
|
|
) -> bool {
|
|
self.root_changed(root)
|
|
|| self.resized.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<Item = &ActiveData> {
|
|
self.active.iter().filter_map(move |(&id, inst)| {
|
|
let l = widgets.label(id);
|
|
if l == label { Some(inst) } else { None }
|
|
})
|
|
}
|
|
|
|
pub fn debug_layers(&self) {
|
|
for ((idx, depth), draws) in self.layers.iter_depth() {
|
|
let indent = " ".repeat(depth * 2);
|
|
let counts: Vec<String> = draws
|
|
.primitives()
|
|
.iter()
|
|
.map(|l| l.as_ref().map_or(0, |l| l.instances().len()).to_string())
|
|
.collect();
|
|
println!("{indent}{idx}: [{}]", counts.join(", "));
|
|
}
|
|
}
|
|
|
|
/// Where a widget is on screen: its box composed through the boxes it
|
|
/// sits within, which is the walk the vertex shader does.
|
|
pub fn window_region(&self, id: &impl IdLike) -> Option<PixelRegion> {
|
|
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<WidgetId> {
|
|
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()
|
|
}
|
|
}
|