Give a slot to the children a container places, and nothing else
A widget's region is now held in the coordinates of the slot it draws in rather than the window's, and `Painter::place` is how a container asks for a slot: it draws a child it decides the box of and may decide again. Everything under that slot is a fraction of its box, so placing the child a second time is one entry to write whether it moved or changed length. A child drawn any other way has no slot and shares its nearest ancestor's. That is what keeps the chain short. `chain_cost` measured depth as the cost -- free to 8, +42.6% at 16 -- and a slot per widget put a transcript's glyphs past that for nothing, since almost every slot was zero. `Span`, `Aligned` and `Scroll` are the containers that re-place a child after drawing it, and `tests/layout.rs` pins that four widgets between a span and a leaf leave the leaf's chain one deep. `UiRegion::stretch`, `UiRegion::stretchable` and `UiScalar::stretch` are gone. Nothing is inverted any more: a box that changed length is written to its slot, and the descendants recompose against it in the shader. That also retires the case the guard existed for, where a fixed length has no fraction to recover -- `tests/layout.rs` now stretches a 40-tall row on its other axis, which `stretchable` refused outright. What still walks the CPU is deciding who must draw again, which no chain can answer: `mark_resized` descends from the widget whose box changed and marks anything whose own box changed length and whose drawing reads it. A part of a box with no relative extent on an axis is a fixed length, and composing into it leaves none either, so the walk stops where a length did not change -- an 80-wide child in a widened row is not redrawn though it says `Redraw`. `Span`, `Pad`, `Stack`, `Offset`, `Aligned`, `SetSize` and `LayerOffset` say `Scale`: each places in fractions and offsets of its own box and none reads the box's pixel length. `Scroll` and `MaxSize` do read pixels and stay `Redraw`. 45 tests pass, five of them new. Render verification comes after the CPU side, per the owner. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1f9dc48b80
commit
d98969158f
18 files changed
+332
-181
No files matched your search
+127
-71
@@ -4,6 +4,8 @@ use crate::{
|
||||
util::{HashMap, HashSet, Vec2, forget_ref},
|
||||
};
|
||||
|
||||
const AXES: [Axis; 2] = [Axis::X, Axis::Y];
|
||||
|
||||
pub struct UiRenderState {
|
||||
pub active: HashMap<WidgetId, ActiveData>,
|
||||
pub layers: DrawLayers,
|
||||
@@ -82,7 +84,17 @@ impl UiRenderState {
|
||||
self.clear(rsc);
|
||||
// free all resources & cache
|
||||
if let Some(id) = root {
|
||||
self.draw_inner(0, id.id(), UiRegion::FULL, None, MaskIdx::NONE, None, rsc);
|
||||
self.draw_inner(
|
||||
0,
|
||||
id.id(),
|
||||
UiRegion::FULL,
|
||||
None,
|
||||
MoveIdx::NONE,
|
||||
false,
|
||||
MaskIdx::NONE,
|
||||
None,
|
||||
rsc,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,13 +106,15 @@ impl UiRenderState {
|
||||
id: WidgetId,
|
||||
region: UiRegion,
|
||||
parent: Option<WidgetId>,
|
||||
parent_move: MoveIdx,
|
||||
slotted: bool,
|
||||
mask: MaskIdx,
|
||||
old_children: Option<Vec<WidgetId>>,
|
||||
rsc: &mut dyn UiRsc,
|
||||
) -> Size {
|
||||
let mut old_children = old_children.unwrap_or_default();
|
||||
if self.active.contains_key(&id) {
|
||||
if let Some(size) = self.try_reuse(id, region, rsc) {
|
||||
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
|
||||
@@ -109,14 +123,21 @@ impl UiRenderState {
|
||||
}
|
||||
|
||||
// draw widget
|
||||
let move_idx = self.move_slot(id, parent);
|
||||
self.moves.set(move_idx, UiRegion::FULL);
|
||||
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)
|
||||
}
|
||||
};
|
||||
rsc.widgets_mut().needs_redraw.remove(&id);
|
||||
self.draw_started.insert(id);
|
||||
|
||||
let mut painter = Painter {
|
||||
state: self,
|
||||
region,
|
||||
region: local,
|
||||
mask,
|
||||
layer,
|
||||
id,
|
||||
@@ -136,7 +157,7 @@ impl UiRenderState {
|
||||
let Painter {
|
||||
state: _,
|
||||
rsc: _,
|
||||
region,
|
||||
region: _,
|
||||
mask,
|
||||
textures,
|
||||
primitives,
|
||||
@@ -166,6 +187,7 @@ impl UiRenderState {
|
||||
size_deps,
|
||||
reads_output,
|
||||
move_idx,
|
||||
parent_move,
|
||||
mask,
|
||||
layer,
|
||||
};
|
||||
@@ -182,101 +204,133 @@ impl UiRenderState {
|
||||
size
|
||||
}
|
||||
|
||||
/// The slot a widget's drawing is positioned through, made on its first
|
||||
/// draw and kept until it stops being drawn.
|
||||
fn move_slot(&mut self, id: WidgetId, parent: Option<WidgetId>) -> MoveIdx {
|
||||
/// 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 parent = parent
|
||||
.and_then(|p| self.slots.get(&p).copied())
|
||||
.unwrap_or(MoveIdx::NONE);
|
||||
let idx = self.moves.push(parent);
|
||||
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 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, rsc: &dyn UiRsc) -> Option<Size> {
|
||||
fn try_reuse(
|
||||
&mut self,
|
||||
id: WidgetId,
|
||||
region: UiRegion,
|
||||
parent_move: MoveIdx,
|
||||
rsc: &mut dyn UiRsc,
|
||||
) -> Option<Size> {
|
||||
if rsc.widgets().needs_redraw.contains(&id) {
|
||||
return None;
|
||||
}
|
||||
let active = self.active.get(&id)?;
|
||||
let (size, old, slot) = (active.size, active.region, active.move_idx);
|
||||
// TODO: epsilon?
|
||||
if old.size() == region.size() {
|
||||
// The right shape and only somewhere else, which is one slot to
|
||||
// write however much is under it. Both boxes are in the
|
||||
// coordinates its parent drew, so the chain above applies alike.
|
||||
let moved =
|
||||
region.to_px(self.output_size).top_left - old.to_px(self.output_size).top_left;
|
||||
self.moves.set(slot, UiRegion::FULL.offset(moved));
|
||||
return Some(size);
|
||||
}
|
||||
if !self.reusable(id, region, rsc) || !old.stretchable() {
|
||||
// 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 {
|
||||
return None;
|
||||
}
|
||||
// Its drawing stands, re-expressed as the same fractions of the box.
|
||||
self.stretch(id, old, region);
|
||||
let (size, old, slot) = (active.size, active.region, active.move_idx);
|
||||
if old == region {
|
||||
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 {
|
||||
return None;
|
||||
}
|
||||
let mut changed = [false; 2];
|
||||
for (axis, c) in AXES.into_iter().zip(changed.iter_mut()) {
|
||||
*c = region.axis(axis).len() != old.axis(axis).len();
|
||||
}
|
||||
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);
|
||||
if redraws {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
self.moves.set(slot, region);
|
||||
self.active.get_mut(&id).unwrap().region = region;
|
||||
if changed.iter().any(|&c| c) {
|
||||
self.mark_resized(id, changed, rsc);
|
||||
}
|
||||
Some(size)
|
||||
}
|
||||
|
||||
/// Whether the widget can keep the drawing it has and be given `region`
|
||||
/// instead, asked one axis at a time: a change on an axis it does not
|
||||
/// depend on costs nothing, whatever it depends on elsewhere.
|
||||
fn reusable(&self, id: WidgetId, region: UiRegion, rsc: &dyn UiRsc) -> bool {
|
||||
/// Marks every descendant whose drawing cannot survive 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.
|
||||
fn mark_resized(&mut self, id: WidgetId, changed: [bool; 2], rsc: &mut dyn UiRsc) {
|
||||
let Some(active) = self.active.get(&id) else {
|
||||
return false;
|
||||
return;
|
||||
};
|
||||
let Some(widget) = rsc.widgets().get_dyn(id) else {
|
||||
return false;
|
||||
};
|
||||
[Axis::X, Axis::Y].into_iter().all(|axis| {
|
||||
let offered = region.axis(axis).len();
|
||||
let had = active.region.axis(axis).len();
|
||||
match widget.on_resize(axis) {
|
||||
OnResize::Scale => true,
|
||||
// `Translate` is not acted on yet, and cannot be until a
|
||||
// drawing can sit somewhere other than its box. `region` is
|
||||
// both the box a widget was given and the box its primitives
|
||||
// are in, and `mov` remaps from it -- so carrying a drawing at
|
||||
// its old size while the box grows makes the next move stretch
|
||||
// it. The offset chain is what separates the two.
|
||||
OnResize::Translate | OnResize::Redraw => offered == had,
|
||||
// SAFETY: children cannot be recursive
|
||||
let children = unsafe { forget_ref(&active.children) };
|
||||
for &child in children {
|
||||
let Some(data) = self.active.get(&child) else {
|
||||
continue;
|
||||
};
|
||||
let region = data.region;
|
||||
let mut own = changed;
|
||||
for (axis, c) in AXES.into_iter().zip(own.iter_mut()) {
|
||||
*c &= region.axis(axis).len().rel != 0.0;
|
||||
}
|
||||
})
|
||||
if !own.iter().any(|&c| c) {
|
||||
continue;
|
||||
}
|
||||
let redraws = match rsc.widgets().get_dyn(child) {
|
||||
Some(widget) => AXES
|
||||
.into_iter()
|
||||
.zip(own)
|
||||
.any(|(axis, c)| c && widget.on_resize(axis) != OnResize::Scale),
|
||||
None => true,
|
||||
};
|
||||
match redraws {
|
||||
true => {
|
||||
rsc.widgets_mut().needs_redraw.insert(child);
|
||||
}
|
||||
false => self.mark_resized(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;
|
||||
};
|
||||
[Axis::X, Axis::Y].into_iter().all(|axis| {
|
||||
AXES.into_iter().all(|axis| {
|
||||
widget
|
||||
.size_hint(axis)
|
||||
.is_none_or(|hint| hint == size.axis(axis))
|
||||
})
|
||||
}
|
||||
|
||||
/// Rewrites a subtree's regions as the same fractions of a new box, for a
|
||||
/// change of length that a slot cannot express. Every region it rewrites
|
||||
/// is a region some slot was a delta from, so those go back to zero.
|
||||
fn stretch(&mut self, id: WidgetId, from: UiRegion, to: UiRegion) {
|
||||
let active = self.active.get_mut(&id).unwrap();
|
||||
for h in &active.primitives {
|
||||
let region = self.layers[h.layer].region_mut(h);
|
||||
*region = region.stretch(&from, &to);
|
||||
}
|
||||
active.region = active.region.stretch(&from, &to);
|
||||
self.moves.set(active.move_idx, UiRegion::FULL);
|
||||
// SAFETY: children cannot be recursive
|
||||
let children = unsafe { forget_ref(&active.children) };
|
||||
for child in children {
|
||||
self.stretch(*child, from, to);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
@@ -363,11 +417,11 @@ impl UiRenderState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a widget is on screen: the box it drew against plus whatever the
|
||||
/// chain above has moved since, which is the walk the vertex shader does.
|
||||
/// 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.move_idx, active.region);
|
||||
let region = self.moves.resolve(active.parent_move, active.region);
|
||||
Some(region.to_px(self.output_size))
|
||||
}
|
||||
|
||||
@@ -400,6 +454,8 @@ impl UiRenderState {
|
||||
id,
|
||||
active.region,
|
||||
active.parent,
|
||||
active.parent_move,
|
||||
active.move_idx != active.parent_move,
|
||||
active.mask,
|
||||
Some(active.children),
|
||||
rsc,
|
||||
|
||||
Reference in new issue
Block a user