Track retained layout validity explicitly

This commit is contained in:
iris-ai committed 2026-09-15 16:03:53 -04:00
1 parent 691e3eb23c
commit 29c7881c8a
25 files changed
+836 -795

No files matched your search

+4 -16
View File
@@ -26,7 +26,6 @@ use std::{
#[derive(Clone, Copy)]
pub(crate) enum Counter {
Updates,
ResizeDependents,
DrawRequests,
WidgetDraws,
PlaceCalls,
@@ -40,13 +39,9 @@ pub(crate) enum Counter {
ReuseDirty,
ReuseWrongParent,
ReuseUnslotted,
ReuseOwnResize,
ReuseDescendantResize,
ResizeChecks,
ResizeCheckChildren,
ReuseOutside,
QueuePops,
DepthReads,
EagerReaderRedraws,
LocalRedraws,
SizeChanges,
ReaderEdges,
@@ -63,7 +58,6 @@ impl Counter {
const NAMES: [&'static str; Self::COUNT] = [
"updates",
"resize dependents",
"draw requests",
"widget draws",
"place calls",
@@ -77,13 +71,9 @@ impl Counter {
"reuse: dirty",
"reuse: wrong parent",
"reuse: unslotted",
"reuse: own resize",
"reuse: descendant resize",
"resize checks",
"resize children checked",
"reuse: outside what it holds for",
"redraw queue pops",
"depth reads",
"eager reader redraws",
"local redraws",
"size changes",
"reader edges",
@@ -100,7 +90,6 @@ impl Counter {
pub(crate) enum TimerKind {
Update,
FullLayout,
ResizeMarking,
IncrementalLayout,
TextRender,
TextShape,
@@ -114,7 +103,6 @@ impl TimerKind {
const NAMES: [&'static str; Self::COUNT] = [
"update total",
"full layout",
"resize marking",
"incremental layout",
"text render",
"text shape",
@@ -255,8 +243,8 @@ pub enum ReuseOutcome {
Dirty,
WrongParent,
Unslotted,
OwnResize,
DescendantResize,
Outside,
Undrawn,
}
/// One targeted layout event. Events are retained in execution order, making
+23 -20
View File
@@ -1,23 +1,27 @@
use crate::{
LayerId, Len, MaskIdx, MoveIdx, PrimitiveHandle, Size, TextureHandle, UiRegion, WidgetId,
util::Vec2,
Holds, LayerId, Len, MaskIdx, MoveIdx, PrimitiveHandle, Size, TextureHandle, UiRegion, WidgetId,
};
/// important non rendering data for retained drawing
/// What is kept of a widget its parent has asked about. `drawn` says whether
/// it currently draws; one that does not is kept so that a change to it, or
/// under it, still reaches whoever asked.
#[derive(Debug)]
pub struct ActiveData {
pub id: WidgetId,
pub region: UiRegion,
/// What the widget said it used of `region`, the last time it drew.
/// The box its parent first asked about it in, as a part of the box the
/// parent was itself asked in. Any later box it was given was decided
/// knowing its answer, so this is where a question about it is asked
/// again -- and it is kept relative so that it follows the parent's.
pub offer: UiRegion,
/// What it answered there: the size and what that held for.
pub answer: (Size, [Holds; 2]),
/// What the widget said it used of its box, the last time it drew.
pub size: Size,
/// The pixel size of the box it drew against. `region` alone cannot say:
/// it is a fraction of a slot's box, and the same fraction of a box that
/// has since changed is a different number of pixels.
pub px: Vec2,
/// The pixel size of the box its parent first asked about it in, before
/// knowing what it came to. `px` may be a box derived from that answer,
/// and a size measured there is only the same answer asked again.
pub offered_px: Vec2,
/// The pixel lengths of its box, per axis, that its drawing and `size`
/// hold for.
pub holds: [Holds; 2],
pub drawn: bool,
pub parent: Option<WidgetId>,
/// How far down the tree it was drawn, the root being 1. Carried down a
/// draw rather than worked out by walking up, so it is right for every
@@ -28,14 +32,6 @@ pub struct ActiveData {
pub children: Vec<WidgetId>,
/// The children whose size this widget read while drawing.
pub size_deps: Vec<WidgetId>,
/// Offered pixel axes which flowed into this widget's reported size,
/// directly or through a child size it read.
pub size_box_inputs: [bool; 2],
/// Output axes read while producing `size`, distinct from the widget's
/// own box when that box has a fixed pixel length.
pub size_output_inputs: [bool; 2],
/// The output dimensions against which those dependencies were observed.
pub output_px: Vec2,
/// The slot its primitives are positioned through: its own if its parent
/// placed it, otherwise the nearest ancestor that has one.
pub move_idx: MoveIdx,
@@ -48,3 +44,10 @@ pub struct ActiveData {
pub mask: MaskIdx,
pub layer: LayerId,
}
impl ActiveData {
/// Whether its drawing and size hold for a box of these pixel lengths.
pub fn holds_at(&self, px: crate::util::Vec2) -> bool {
self.holds[0].contains(px.x) && self.holds[1].contains(px.y)
}
}
+81
View File
@@ -0,0 +1,81 @@
use crate::UiScalar;
use std::ops::RangeInclusive;
/// The lengths of a box, in pixels, that one drawing of a widget holds for:
/// give the widget any box in this range and it draws the same thing and
/// reports the same size. A widget that never reads its box in pixels holds
/// for every length; one that does holds for the one it read unless it says
/// otherwise, and a parent holds for whatever keeps every child it asked
/// about or drew inside its own range.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Holds {
pub lo: f32,
pub hi: f32,
}
/// How far outside a range a length may fall and still be inside it: a box
/// offered back to a widget at the length it reported comes back through the
/// chain a few bits off, and nothing a reader could see lives in that gap.
pub const HOLDS_EPSILON_PX: f32 = 0.05;
impl Holds {
pub const ANY: Self = Self {
lo: f32::NEG_INFINITY,
hi: f32::INFINITY,
};
pub const fn at(len: f32) -> Self {
Self { lo: len, hi: len }
}
pub fn contains(&self, len: f32) -> bool {
len >= self.lo - HOLDS_EPSILON_PX && len <= self.hi + HOLDS_EPSILON_PX
}
pub fn and(self, other: Self) -> Self {
Self {
lo: self.lo.max(other.lo),
hi: self.hi.min(other.hi),
}
}
/// What a box has to be for a part of it, `len` of the box long, to stay
/// in this range. A part with no relative extent is a fixed length: it
/// was drawn at that length and any box keeps it there.
pub fn through(self, len: UiScalar) -> Self {
if len.rel == 0.0 {
return Self::ANY;
}
let a = (self.lo - len.px) / len.rel;
let b = (self.hi - len.px) / len.rel;
Self {
lo: a.min(b),
hi: a.max(b),
}
}
}
impl From<RangeInclusive<f32>> for Holds {
fn from(range: RangeInclusive<f32>) -> Self {
Self {
lo: *range.start(),
hi: *range.end(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn through_reverses_a_range_for_a_negative_fraction() {
assert_eq!(
Holds { lo: 20.0, hi: 40.0 }.through(UiScalar::new(-0.5, 10.0)),
Holds {
lo: -60.0,
hi: -20.0
}
);
}
}
+2
View File
@@ -10,10 +10,12 @@ use crate::{
pub const CHAIN_LIMIT: u32 = 64;
mod active;
mod holds;
mod painter;
mod render_state;
pub use active::*;
pub use holds::*;
pub use painter::{Painter, PrimitiveLike};
pub use render_state::*;
+142 -128
View File
@@ -1,14 +1,18 @@
#[cfg(feature = "layout-diagnostics")]
use crate::layout_diagnostics::{self as diag, Counter};
use crate::{
Axis, Len, RenderedText, Size, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle,
UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Widget, WidgetId,
Axis, Holds, Len, RenderedText, Size, StrongWidget, TextAttrs, TextBuffer, TextData,
TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Widget, WidgetId,
render::{
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveHandle, PrimitiveInst,
PrimitiveKind, TexturePrimitive,
},
ui::render_state::DrawInfo,
util::Vec2,
};
use std::ops::RangeInclusive;
const AXES: [Axis; 2] = [Axis::X, Axis::Y];
/// makes your surfaces look pretty
pub struct Painter<'a> {
@@ -21,14 +25,21 @@ pub struct Painter<'a> {
pub(super) textures: Vec<TextureHandle>,
pub(super) primitives: Vec<PrimitiveHandle>,
pub(super) children: Vec<WidgetId>,
/// The children asked about so far, so the first box each was asked
/// about is the one recorded as its offer.
/// The children asked about so far, so the first box each was asked in
/// is the one recorded as its offer.
pub(super) offered: Vec<WidgetId>,
/// The box this widget was first asked about in, in pixels.
pub(super) offered_px: Vec2,
/// Whether this draw is in that box, which makes the questions it asks
/// the ones a cold layout asks and their answers the ones to keep.
pub(super) at_offer: bool,
/// The children whose size this widget read while drawing.
pub(super) size_deps: Vec<WidgetId>,
/// Offered pixel axes which can affect the size this draw reports.
pub(super) size_box_inputs: [bool; 2],
pub(super) size_output_inputs: [bool; 2],
/// What this draw itself read of its box in pixels, per axis: every
/// length until it reads one, then that one, unless it says otherwise.
pub(super) own: [Holds; 2],
/// What the children it asked about and drew keep it to.
pub(super) under: [Holds; 2],
/// The slot this widget's primitives are positioned through: its own if
/// its parent placed it, otherwise the nearest ancestor that has one.
pub(super) move_idx: MoveIdx,
@@ -90,14 +101,7 @@ impl<'a> Painter<'a> {
/// Draws a widget within this widget's region.
pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> {
let declared = self.declared_lens(id);
// Composing `FULL` through a box is not quite the identity in f32,
// so a child with nothing declared keeps the box it would have had.
let region = match declared.iter().any(Option::is_some) {
true => declared_box(UiRegion::FULL, declared).within(&self.region),
false => self.region,
};
self.widget_at(id, region, false, declared)
self.widget_at(id, UiRegion::FULL, false)
}
/// Draws a widget somewhere within this one.
@@ -106,9 +110,7 @@ impl<'a> Painter<'a> {
id: &'s StrongWidget<W>,
region: UiRegion,
) -> DrawResult<'s, 'a, W> {
let declared = self.declared_lens(id);
let region = declared_box(region, declared).within(&self.region);
self.widget_at(id, region, false, declared)
self.widget_at(id, region, false)
}
/// What a widget declares its lengths to be, which whoever draws it
@@ -120,7 +122,7 @@ impl<'a> Painter<'a> {
let Some(widget) = self.rsc.widgets().get_dyn(id.id()) else {
return [None; 2];
};
[Axis::X, Axis::Y].map(|axis| declared_len(widget, axis))
AXES.map(|axis| declared_len(widget, axis))
}
/// Draws a child this widget decides the box of, and may decide again
@@ -136,39 +138,75 @@ impl<'a> Painter<'a> {
) -> DrawResult<'s, 'a, W> {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::PlaceCalls);
let declared = self.declared_lens(id);
let region = declared_box(region, declared).within(&self.region);
#[cfg(feature = "layout-diagnostics")]
diag::placed(id.id(), self.id, region);
self.widget_at(id, region, true, declared)
self.widget_at(id, region, true)
}
/// Takes back a child that was drawn only to find out how long it is.
/// Its drawing is dropped and it is not one of this widget's children
/// this frame; what it answered is still something this widget asked.
pub fn undraw<W: ?Sized>(&mut self, id: &StrongWidget<W>) {
self.children.retain(|child| *child != id.id());
self.state.undraw_rec(id.id(), self.rsc);
}
/// `region` in this widget's own coordinates, and with the child's
/// declared lengths still to be taken.
fn widget_at<'s, W: ?Sized>(
&'s mut self,
id: &'s StrongWidget<W>,
region: UiRegion,
slotted: bool,
declared: [Option<Len>; 2],
) -> DrawResult<'s, 'a, W> {
let declared = self.declared_lens(id);
// Composing `FULL` through a box is not quite the identity in f32,
// so a child with nothing declared keeps the box it would have had.
let local = match declared.iter().any(Option::is_some) {
true => declared_box(region, declared),
false => region,
};
let within = match local == UiRegion::FULL {
true => self.region,
false => local.within(&self.region),
};
#[cfg(feature = "layout-diagnostics")]
if slotted {
diag::placed(id.id(), self.id, within);
}
// A child listed twice would be moved twice.
if !self.children.contains(&id.id()) {
self.children.push(id.id());
}
let first_ask = self.offer(id.id());
let offer = match first_ask {
true => local,
false => self.state.active.get(&id.id()).map_or(local, |a| a.offer),
};
let answers_offer = self.at_offer && local == offer;
let size = self.state.draw_inner(
self.layer,
id.id(),
region,
Some(self.id),
self.depth + 1,
self.move_idx,
slotted,
self.mask,
within,
DrawInfo {
layer: self.layer,
parent: Some(self.id),
depth: self.depth + 1,
parent_move: self.move_idx,
slotted,
mask: self.mask,
offer,
offered_px: self.px_within_offer(offer),
},
None,
self.rsc,
);
self.offer(id.id(), region);
if let Some(active) = self.state.active.get_mut(&id.id()) {
active.declared = declared;
let active = self.state.active.get_mut(&id.id()).unwrap();
active.declared = declared;
if answers_offer {
active.answer = (active.size, active.holds);
}
// Whatever the child's drawing holds for keeps this one to the boxes
// that give the child a length inside it.
for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) {
*under = under.and(active.holds[axis as usize].through(local.axis(axis).len()));
}
DrawResult {
child: id,
@@ -191,7 +229,7 @@ impl<'a> Painter<'a> {
Some(hint) => {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::HintHits);
self.depend_on_hint(id);
self.depend_on(id);
Some(hint)
}
None => {
@@ -202,87 +240,66 @@ impl<'a> Painter<'a> {
}
}
/// A retained child length valid under the region it is about to be
/// offered. Unlike a hint, this is contextual: it is kept only when none
/// of the offered pixel axes which produced it changed.
/// A child's length in the box it is about to be offered, if it can be
/// had without drawing it: from its hint, or from a drawing it already
/// has that holds for that box.
pub fn known_len<W: ?Sized>(
&mut self,
child: &StrongWidget<W>,
axis: Axis,
region: UiRegion,
) -> Option<Len> {
let region = region.within(&self.region);
self.offer(child.id(), region);
let declared = self.declared_lens(child);
let local = declared_box(region, declared);
let within = local.within(&self.region);
let first_ask = self.offer(child.id());
if first_ask && let Some(active) = self.state.active.get_mut(&child.id()) {
active.offer = local;
}
if let Some(hint) = self.size_hint(child, axis) {
return Some(hint);
}
self.retained_size(child, region)
.map(|size| size.axis(axis))
}
/// `region` in this widget's own coordinates.
fn retained_size<W: ?Sized>(
&mut self,
child: &StrongWidget<W>,
region: UiRegion,
) -> Option<Size> {
let (size, box_inputs, output_inputs) =
let px = self.state.px_of(self.move_idx, within);
let (size, holds) =
self.state
.retained_size(child.id(), region, self.move_idx, self.rsc.widgets())?;
.retained_size(child.id(), px, self.move_idx, self.rsc.widgets())?;
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::RetainedSizeHits);
self.depend_on_size_inputs(child, box_inputs, output_inputs);
Some(size)
self.depend_on(child);
if first_ask {
let active = self.state.active.get_mut(&child.id()).unwrap();
active.answer = (size, holds);
}
for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) {
*under = under.and(holds[axis as usize].through(local.axis(axis).len()));
}
Some(size.axis(axis))
}
/// Records the box a child was first asked about in this draw. Any later
/// box this draw gives it was decided knowing its answer, so a size the
/// child measures there is not an answer to this widget's question.
fn offer(&mut self, child: WidgetId, region: UiRegion) {
if self.offered.contains(&child) {
return;
/// Whether this is the first box a child is asked about in during a draw
/// that is itself in the box it was asked in -- the question a cold
/// layout asks, whose answer is the one to keep.
fn offer(&mut self, child: WidgetId) -> bool {
if !self.at_offer || self.offered.contains(&child) {
return false;
}
self.offered.push(child);
let px = self.state.px_of(self.move_idx, region);
if let Some(active) = self.state.active.get_mut(&child) {
active.offered_px = px;
}
true
}
/// Depends on a length the child gave without being drawn. A hint is
/// context-free, so this depends on the child but on no pixel axis.
fn depend_on_hint<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
self.depend_on_size_inputs(child, [false; 2], [false; 2]);
/// The pixel size of a part of the box this widget was asked in.
fn px_within_offer(&self, local: UiRegion) -> Vec2 {
let size = local.size();
Vec2::new(
size.x.to_px(self.offered_px.x),
size.y.to_px(self.offered_px.y),
)
}
/// Depends on a size the child produced by drawing, which carries
/// whatever the child read to produce it.
fn depend_on_drawn_size<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
let (box_inputs, output_inputs) = self
.state
.active
.get(&child.id())
.map_or(([false; 2], [false; 2]), |active| {
(active.size_box_inputs, active.size_output_inputs)
});
self.depend_on_size_inputs(child, box_inputs, output_inputs);
}
fn depend_on_size_inputs<W: ?Sized>(
&mut self,
child: &StrongWidget<W>,
box_inputs: [bool; 2],
output_inputs: [bool; 2],
) {
fn depend_on<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
if !self.size_deps.contains(&child.id()) {
self.size_deps.push(child.id());
}
for (own, child) in self.size_box_inputs.iter_mut().zip(box_inputs) {
*own |= child;
}
for (own, child) in self.size_output_inputs.iter_mut().zip(output_inputs) {
*own |= child;
}
}
pub fn render_text<'b>(
@@ -327,45 +344,42 @@ impl<'a> Painter<'a> {
self.region
}
/// The output's size in pixels. A widget that reads it draws again when
/// the output changes, since nothing else can put that right.
pub fn output_size(&mut self) -> Vec2 {
self.size_output_inputs = [true; 2];
self.state.output_size
}
/// One axis of the output in pixels. Prefer this to [`Self::output_size`]
/// when the other axis cannot affect the size this widget reports.
pub fn output_len(&mut self, axis: Axis) -> f32 {
self.size_output_inputs[axis as usize] = true;
self.state.output_size.axis(axis)
}
/// This widget's box in pixels. Resolved against the output's size and
/// the boxes it sits within, so a widget that reads it draws again when
/// the output changes.
/// This widget's box in pixels. Reading it makes the drawing one that
/// holds for this box only, until `holds` says how far it goes.
pub fn px_size(&mut self) -> Vec2 {
self.size_box_inputs = [true; 2];
let region = self.state.moves.resolve(self.move_idx, self.region);
region.size().to_px(self.state.output_size)
let px = self.state.px_of(self.move_idx, self.region);
for (own, len) in self.own.iter_mut().zip([px.x, px.y]) {
if *own == Holds::ANY {
*own = Holds::at(len);
}
}
px
}
/// One axis of this widget's box in pixels. Prefer this to
/// [`Self::px_size`] when the other axis cannot affect the reported size.
/// [`Self::px_size`] when the other axis cannot affect the drawing.
pub fn px_len(&mut self, axis: Axis) -> f32 {
self.size_box_inputs[axis as usize] = true;
self.px_len_for_draw(axis)
let len = self.state.px_of(self.move_idx, self.region).axis(axis);
let own = &mut self.own[axis as usize];
if *own == Holds::ANY {
*own = Holds::at(len);
}
len
}
/// One axis of this widget's box in pixels, for a draw whose reported
/// size does not follow from it -- a clamp or a position. Nothing records
/// the read, so a size that does depend on it would go stale.
pub fn px_len_for_draw(&self, axis: Axis) -> f32 {
let region = self.state.moves.resolve(self.move_idx, self.region);
region
.size()
.axis(axis)
.to_px(self.state.output_size.axis(axis))
/// The lengths of this widget's box on `axis` that what it is drawing
/// holds for -- the same primitives, in the same fractions and offsets
/// of the box, and the same reported size. A widget that read its
/// length in pixels holds for that one alone until it says otherwise.
pub fn holds(&mut self, axis: Axis, range: RangeInclusive<f32>) {
let holds = Holds::from(range);
debug_assert!(
holds.contains(self.state.px_of(self.move_idx, self.region).axis(axis)),
"'{}' ({:?}) says its drawing holds for lengths that leave out its own box",
self.label(),
self.id
);
self.own[axis as usize] = holds;
}
pub fn text_data(&mut self) -> &mut TextData {
@@ -405,7 +419,7 @@ impl<W: ?Sized> DrawResult<'_, '_, W> {
diag::bump(Counter::SizeReads);
diag::size_read(self.child.id(), self.painter.id, self.size);
}
self.painter.depend_on_drawn_size(self.child);
self.painter.depend_on(self.child);
self.size
}
@@ -451,7 +465,7 @@ pub(crate) fn declared_len(widget: &dyn Widget, axis: Axis) -> Option<Len> {
/// reserved the space hands back the same length, so this is the identity
/// for it.
fn declared_box(mut region: UiRegion, declared: [Option<Len>; 2]) -> UiRegion {
for (axis, len) in [Axis::X, Axis::Y].into_iter().zip(declared) {
for (axis, len) in AXES.into_iter().zip(declared) {
let Some(len) = len else { continue };
let span = region.axis_mut(axis);
span.end = span.start + UiScalar::new(len.rel, len.px);
File diff suppressed because it is too large. Load diff
-22
View File
@@ -15,20 +15,6 @@ pub use tag::*;
pub use view::*;
pub use widgets::*;
/// What may be done to a widget's drawing when the box it was given changes
/// on this axis, instead of drawing it again. Asked per axis, because wrapped
/// text reads the width it is offered and not the height.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum OnResize {
Scale,
/// Reserved: nothing reads this yet, so a widget saying it is redrawn.
/// Keeping an unchanged drawing in a bigger box needs the widget to say
/// *where* in that box it should sit, which is the alignment work.
Translate,
#[default]
Redraw,
}
pub trait Widget: Any {
/// Draws the widget, and returns what it used of the box it was given.
fn draw(&mut self, painter: &mut Painter) -> Size;
@@ -39,10 +25,6 @@ pub trait Widget: Any {
fn size_hint(&self, _axis: Axis) -> Option<Len> {
None
}
fn on_resize(&self, _axis: Axis) -> OnResize {
OnResize::default()
}
}
impl Widget for () {
@@ -54,10 +36,6 @@ impl Widget for () {
fn size_hint(&self, _axis: Axis) -> Option<Len> {
Some(Len::default())
}
fn on_resize(&self, _axis: Axis) -> OnResize {
OnResize::Scale
}
}
impl dyn Widget {