`lerp` is `a + (b - a) * f`, and `b - a` is nothing often enough to be worth asking: a box with the same pixels at both ends of an axis, a span with no fraction of one, a part of a subtree whose box did not move on that axis. `Fixed::scaled` is `mul` that answers a zero receiver without widening to `i64`, rounding and narrowing back, and `lerp` uses it -- so every lerp in layout gets it rather than the two places that were about to grow their own comparison. `many` over 500 frames: 1,705,786,553 instructions to 1,657,571,216, and 638.9M cycles against 657.9M, averaged over four runs each. Checked: fmt, clippy, 105 tests, all five shrinker cases at 300 seeds, and `tabs`, `text`, `random`, `minimal` and `view` byte-identical at 1920x1200. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1161 lines
42 KiB
Rust
1161 lines
42 KiB
Rust
#[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<WidgetId>,
|
|
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<RegionAlign>,
|
|
}
|
|
|
|
pub struct UiRenderState {
|
|
pub active: HashMap<WidgetId, ActiveData>,
|
|
pub layers: DrawLayers,
|
|
pub(super) output_size: PxVec2,
|
|
|
|
old_root: Option<WidgetId>,
|
|
/// 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<WidgetId, MoveIdx>,
|
|
/// 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<WidgetId>,
|
|
/// 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<Vec2>) {
|
|
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<Option<&'a StrongWidget>>, rsc: &mut dyn UiRsc) {
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
diag::bump(Counter::Updates);
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
let _update = diag::timer(TimerKind::Update);
|
|
// safety mechanism for memory leaks; might wanna return a result instead so user can
|
|
// decide whether to panic or not
|
|
if !rsc.widgets().waiting.is_empty() {
|
|
let widgets = rsc.widgets();
|
|
let len = widgets.waiting.len();
|
|
let all: Vec<_> = widgets
|
|
.waiting
|
|
.iter()
|
|
.map(|&w| format!("'{}' ({w:?})", widgets.label(w)))
|
|
.collect();
|
|
panic!(
|
|
"{len} widget(s) were never upgraded\n\
|
|
this is likely a memory leak; consider upgrading to strong if you plan on using it later\n\
|
|
weak widgets: {all:#?}"
|
|
);
|
|
}
|
|
let root = root.into();
|
|
if self.root_changed(root) {
|
|
self.redraw_all(root, rsc);
|
|
self.old_root = root.map(|r| r.id());
|
|
} 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<ActiveData>,
|
|
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<ActiveData>,
|
|
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<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.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<Option<&'a StrongWidget>>) -> bool {
|
|
root.into().map(|r| r.id()) != self.old_root
|
|
}
|
|
|
|
pub fn needs_redraw<'a>(
|
|
&self,
|
|
root: impl Into<Option<&'a StrongWidget>>,
|
|
widgets: &Widgets,
|
|
) -> bool {
|
|
self.root_changed(root) || self.resized || widgets.has_updates()
|
|
}
|
|
|
|
pub fn active_widgets(&self) -> usize {
|
|
self.active.values().filter(|active| active.drawn).count()
|
|
}
|
|
|
|
pub fn debug(&self, widgets: &Widgets, label: &str) -> impl Iterator<Item = &ActiveData> {
|
|
self.active.iter().filter_map(move |(&id, inst)| {
|
|
let l = widgets.label(id);
|
|
if l == label { Some(inst) } else { None }
|
|
})
|
|
}
|
|
|
|
pub fn debug_layers(&self) {
|
|
for ((idx, depth), draws) in self.layers.iter_depth() {
|
|
let indent = " ".repeat(depth * 2);
|
|
let counts: Vec<String> = draws
|
|
.primitives()
|
|
.iter()
|
|
.map(|l| l.as_ref().map_or(0, |l| l.instances().len()).to_string())
|
|
.collect();
|
|
println!("{indent}{idx}: [{}]", counts.join(", "));
|
|
}
|
|
}
|
|
|
|
/// Where a widget is on screen: its box composed through the boxes it
|
|
/// sits within, which is the walk the vertex shader does. `None` for one
|
|
/// that is not drawn.
|
|
pub fn window_region(&self, id: &impl IdLike) -> Option<PixelRegion> {
|
|
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<Self> {
|
|
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<Self> {
|
|
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()
|
|
}
|
|
}
|