Compare commits

..
Author SHA1 Message Date
iris-ai 2ed5503717 Recompose retained frames exactly and preserve text width validity
Keep each widget's original local frame and replay the same composition
order on reuse. Remove inverse region remapping, including its fixed-frame
fallback that forced otherwise valid subtrees to draw again.

Require exact pixel-region equality in the shared generated oracle. Check
primitive and mask geometry as well as draw reuse when fixed frames resize.
Publish text's retained line-break range, with no upper bound when there
are no soft breaks, and cover widening, explicit newlines, and empty text.

Compared with efb416b, the depth-8 diagnostic rig performs 7-9% fewer widget
evaluations in the affected phases. Uninstrumented release runs use 3.5%
fewer instructions for size changes and 5.0% fewer for resize. Repaint and
scroll use 0.7% and 0.6% more instructions. Container updates remain substantially more expensive than the e44dea3 baseline;
this is still an experimental continuation, not a production replacement.
2026-09-17 15:11:03 -04:00
iris-ai efb416bbc3 Retain frame and extent dependencies independently
Keep the original measurement placement separate from the assigned slot.
Validate frame and extent lengths before reusing an answer or drawing, and
represent hint-only records as having no measured answer.

Retain primitive and mask coordinates with their frame/extent reference.
Forwarded children follow a reused wrapper's placement without rerunning
valid draw bodies. Keep the single Widget::draw API.

Restore the eight failing suite cases from the region/placement prototype,
with regressions for mixed coordinate references, a changed inherited
extent, the sizing-stack fraction, and an undrawn share becoming visible.

This remains experimental: nested container updates do substantially more
work than e44dea3 despite restoring the leaf and wrapper reuse guarantees.
Do not merge it as a performance improvement.
2026-09-17 14:50:09 -04:00
iris-ai 5fcace1bfa WIP: a widget's region stays put and its placement moves in it
The protocol split: `region` is the box a parent gives a widget -- what a
fraction it declares or reports is a fraction of, and the coordinates
every region it writes composes within -- and it is the same box on the
ask that measures and the ask that places. `placement` is what of that
region the drawing takes, chosen by the parent per axis or by the
widget's own answer and alignment.

That is what stops a fraction being resolved twice: the placing ask no
longer hands the widget its own answer as its box, so nothing under it
re-resolves against a box that came from its own report. `reports_of`
and `decided` are gone, folded into the two regions; `box_of` is gone;
`declared_box` becomes `ask_box`, which gives a rule the region's length
and takes the position from the placement.

84 of 92 suite tests pass. Five text and region-node cases still diverge
warm against cold, and three count a second widget draw where a span's
measuring ask and its placing ask give different placements.
2026-09-17 13:53:14 -04:00
iris-ai e44dea34b4 Record which widget is drawing a subtree that changed hands
A subtree can be reused whole under a different parent -- same box, same
layer, same region node, clean -- and nothing in the drawing says it
moved. Two things read who its parent is, and both were wrong after one
of these.

The old parent still listed it as a child, and a parent's next draw
undraws whatever is missing from that list: two spans under one root,
with the root swapping which of them it holds, drew the subtree under
the new span and then erased it when the old one drew. The move is
recorded on both sides where `draw_inner` already writes what the ask
decided, rather than guarded at each reader.

Its depth was also the one it had under the old parent, which is what
the settling walk orders by, so a change made under it afterwards
settled at the wrong point in the frame. `try_reuse` re-walks the
subtree's depths, and only where the top of it moved, which is what
makes that free in the ordinary case.

Two tests: one shape where the subtree's box does not move and the span
it left erases it, one where it changes depth and the change made under
it has to reach the span it moved to. Each fails without one half.
2026-09-17 13:05:15 -04:00
iris-ai a0693acc56 Let a resize settle through the walk, and drop the stale-answer guard
A resize drew the root outside `redraw_updates`, top-down over a tree
with dirty widgets still in it, which is the one entry point
`dirty_size_under` was guarding: since `a92c6ac` settles a frame strictly
bottom-up, no fuzzer could tell whether that guard still did anything
anywhere else. Closing the entry point retires the guard rather than
keeping a check for a hole reasoned rather than measured.

The root is marked instead, and only where the new output falls outside
what its answer holds for. That range is the intersection of everything
under it, so admitting the new output says the whole tree still stands,
and nothing above the root moved -- the window is no entry to rewrite.
Marking it unconditionally would have cost the root its own `Holds`: a
leaf root that scales with its box was drawn again on every resize.

`dirty_size_under` goes at both call sites. `resize` takes `Widgets`
because a mark is what it now leaves behind.
2026-09-17 13:00:32 -04:00
iris-ai 25e456e0b5 Say what the fuzzers can no longer tell about the stale-answer guard
Dropping `dirty_size_under` from it now passes every run there is. It stays
for the one entry the bottom-up ordering does not reach -- `update` draws
the root for a resize before `redraw_updates` runs -- which is a hole
reasoned rather than measured, and the note says which.
2026-09-17 05:12:44 -04:00
iris-ai 53b00c68e9 Find a span's leftover boundary through the inverse it already has
The decision used a rounded division, `total.px.div(fixed)`, where the room
the children get is a floored multiply, so the boundary and the drawing it
guards were two expressions for one length and disagreed at the edge of it.
`room` is that length as a `Len`, `room.to_px` is the multiply, and
`Holds::through` is its exact preimage -- so ask `room` whether anything is
left and hand the answer back through the same expression.

The three branches go with the division. They were the sign of `1 - rel`:
the fixed parts growing slower than the box, faster, or exactly with it, and
`through` reads that sign already. Forty lines become twelve, one `div`
leaves layout, and the boundary is the drawing's own.

Green on the suite, the shrinker at 400 seeds of depth 5, the oracle at 1000
seeds of depth 6 and 120 in debug, and 2000 seeds at depth 4 over all
fifteen cases. `tabs`, `view`, `minimal` and `random` byte-identical.
2026-09-17 05:02:45 -04:00
22 changed files with 1176 additions and 619 deletions

No files matched your search

+11
View File
@@ -7,6 +7,17 @@ pub enum Axis {
Y, Y,
} }
impl Axis {
/// A per-axis pair with `aligned` on this axis and `ortho` on the other,
/// which is what `from_axis` does for a vector.
pub fn pair<T>(self, aligned: T, ortho: T) -> [T; 2] {
match self {
Self::X => [aligned, ortho],
Self::Y => [ortho, aligned],
}
}
}
impl std::ops::Not for Axis { impl std::ops::Not for Axis {
type Output = Self; type Output = Self;
+17
View File
@@ -176,6 +176,23 @@ impl TextBuffer {
self.layout_key.as_ref()?.max_width self.layout_key.as_ref()?.max_width
} }
/// Widths covered by the current line breaks, including a wider shaping
/// retained when a later draw requested a narrower box.
pub fn width_holds(&self) -> crate::Holds {
let Some(width) = self.wrap_width() else {
return crate::Holds::ANY;
};
let width = Px::from_f32(width);
let soft_wrapped = self.layout.lines().any(|line| {
matches!(
line.break_reason(),
parley::layout::BreakReason::Regular | parley::layout::BreakReason::Emergency
)
});
let upper = if soft_wrapped { width } else { Px::MAX };
crate::Holds::from(Px::ceil_from_f32(self.layout.width()).min(width)..=upper)
}
pub fn size(&self) -> Vec2 { pub fn size(&self) -> Vec2 {
Vec2::new(self.layout.width(), self.layout.height()) Vec2::new(self.layout.width(), self.layout.height())
} }
+2 -1
View File
@@ -106,7 +106,8 @@ impl UiRenderNode {
self.active.push(i); self.active.push(i);
for change in draws.apply_free() { for change in draws.apply_free() {
if let Some(inst) = ui_render.active.get_mut(&change.id) { if let Some(inst) = ui_render.active.get_mut(&change.id) {
for h in &mut inst.primitives { for primitive in &mut inst.primitives {
let h = &mut primitive.handle;
if h.layer == i && h.kind == change.kind && h.inst_idx == change.old { if h.layer == i && h.kind == change.kind && h.inst_idx == change.old {
h.inst_idx = change.new; h.inst_idx = change.new;
break; break;
+32 -20
View File
@@ -1,6 +1,6 @@
use crate::{ use crate::{
Holds, LayerId, LayoutLen, MaskIdx, MoveIdx, PrimitiveHandle, RegionAlign, Size, TextureHandle, DrawRegion, LayerId, LayoutHolds, LayoutLen, MaskIdx, MoveIdx, RegionAlign, RetainedPrimitive,
UiRegion, UiVec2, WidgetId, Size, TextureHandle, UiRegion, UiVec2, WidgetId,
}; };
/// What is kept of a widget its parent has asked about. `drawn` says whether /// What is kept of a widget its parent has asked about. `drawn` says whether
@@ -9,29 +9,29 @@ use crate::{
#[derive(Debug)] #[derive(Debug)]
pub struct ActiveData { pub struct ActiveData {
pub id: WidgetId, pub id: WidgetId,
/// The box its drawing is in, in `parent_move`'s coordinates. /// The box its parent gave it, in `parent_move`'s coordinates: what it
/// was asked about, and what a fraction under it is a fraction of. A
/// local redraw asks here.
pub region: UiRegion, pub region: UiRegion,
/// The box its parent gave it, in the same coordinates: what it was /// Where its drawing sits inside that box, in the box's own coordinates.
/// asked about, before its own answer placed its drawing inside it. pub placement: UiRegion,
/// `region` is that placement, and a local redraw asks here. /// The original frame in its parent widget's coordinates. Recomposition
pub given: UiRegion, /// and pixel-length evaluation both follow this chain.
/// The same box as lengths of its parent's box, which is the one route pub given_region: UiRegion,
/// to a box in pixels: a draw threads these down a level at a time, and
/// [`crate::UiRenderState::redraw`] takes the same steps back up.
pub given_len: UiVec2,
/// The lengths of the box its parent first asked about it in, as /// The lengths of the box its parent first asked about it in, as
/// lengths of the box the parent was itself offered. Any later box it /// lengths of the box the parent was itself offered. Any later box it
/// was given was decided knowing its answer, so this is the question /// was given was decided knowing its answer, so this is the question
/// asked again -- and a chain of fractions has no frame in it, which is /// asked again -- and a chain of fractions has no frame in it, which is
/// why a region node between two widgets cannot break it. /// why a region node between two widgets cannot break it.
pub offer_len: UiVec2, pub offer_len: UiVec2,
/// What it answered there: the size and what that held for. pub offer_placement: [Option<crate::UiSpan>; 2],
pub answer: (Size, [Holds; 2]), /// The measured answer and its dependencies. A hint-only dependency or
/// a widget first encountered during placement has no measurement yet.
pub answer: Option<(Size, LayoutHolds)>,
/// What the widget said it used of its box, the last time it drew. /// What the widget said it used of its box, the last time it drew.
pub size: Size, pub size: Size,
/// The pixel lengths of `region`, per axis, that its drawing and `size` /// The frame, extent and explicit placement reads that this drawing holds for.
/// hold for. pub holds: LayoutHolds,
pub holds: [Holds; 2],
pub drawn: bool, pub drawn: bool,
pub parent: Option<WidgetId>, pub parent: Option<WidgetId>,
/// How far down the tree it was drawn, the root being 1. Carried down a /// How far down the tree it was drawn, the root being 1. Carried down a
@@ -39,7 +39,9 @@ pub struct ActiveData {
/// widget a frame visits and cannot drift while one is being drawn. /// widget a frame visits and cannot drift while one is being drawn.
pub depth: usize, pub depth: usize,
pub textures: Vec<TextureHandle>, pub textures: Vec<TextureHandle>,
pub primitives: Vec<PrimitiveHandle>, pub primitives: Vec<RetainedPrimitive>,
pub mask_region: Option<DrawRegion>,
pub inherited_children: Vec<WidgetId>,
pub children: Vec<WidgetId>, pub children: Vec<WidgetId>,
/// The children whose size this widget read while drawing. /// The children whose size this widget read while drawing.
pub size_deps: Vec<WidgetId>, pub size_deps: Vec<WidgetId>,
@@ -70,8 +72,18 @@ pub struct ActiveData {
} }
impl ActiveData { impl ActiveData {
/// Whether its drawing and size hold for a box of these pixel lengths. /// Whether what it answered still stands for a box of these pixel
pub fn holds_at(&self, px: crate::PxVec2) -> bool { /// lengths -- the box it was asked in, where `holds` is about the box its
self.holds[0].contains(px.x) && self.holds[1].contains(px.y) /// answer then chose.
pub fn answers_at(&self, px: crate::PxVec2) -> bool {
self.answer.is_some_and(|(_, holds)| {
holds.contains(
px,
UiRegion {
x: self.offer_placement[0].unwrap_or(crate::UiSpan::FULL),
y: self.offer_placement[1].unwrap_or(crate::UiSpan::FULL),
},
)
})
} }
} }
+36
View File
@@ -0,0 +1,36 @@
use crate::{PrimitiveHandle, UiRegion};
/// Retains which box geometry follows when only the extent changes.
#[derive(Clone, Copy, Debug)]
pub enum DrawRegion {
Frame(UiRegion),
Extent(UiRegion),
}
impl DrawRegion {
pub(crate) fn resolve(self, frame: UiRegion, extent: UiRegion) -> UiRegion {
match self {
Self::Frame(local) => local.within(&frame),
Self::Extent(local) => local.within(&extent).within(&frame),
}
}
pub(crate) fn map(self, f: impl FnOnce(UiRegion) -> UiRegion) -> Self {
match self {
Self::Frame(local) => Self::Frame(f(local)),
Self::Extent(local) => Self::Extent(f(local)),
}
}
}
impl From<UiRegion> for DrawRegion {
fn from(region: UiRegion) -> Self {
Self::Frame(region)
}
}
#[derive(Debug)]
pub struct RetainedPrimitive {
pub handle: PrimitiveHandle,
pub region: DrawRegion,
}
+33
View File
@@ -0,0 +1,33 @@
use crate::{Axis, Holds, PxVec2, UiRegion};
/// Dependencies of one evaluation, before the frame and extent are composed.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LayoutHolds {
pub frame: [Holds; 2],
pub extent: [Holds; 2],
pub placement: Option<UiRegion>,
}
impl LayoutHolds {
pub const ANY: Self = Self {
frame: [Holds::ANY; 2],
extent: [Holds::ANY; 2],
placement: None,
};
pub fn contains(self, px: PxVec2, placement: UiRegion) -> bool {
self.placement.is_none_or(|old| old == placement)
&& [Axis::X, Axis::Y].into_iter().all(|axis| {
self.frame[axis as usize].contains(px.axis(axis))
&& self.extent[axis as usize]
.contains(placement.axis(axis).len().to_px(px.axis(axis)))
})
}
pub fn in_frame(self, placement: UiRegion) -> [Holds; 2] {
[Axis::X, Axis::Y].map(|axis| {
self.frame[axis as usize]
.and(self.extent[axis as usize].through(placement.axis(axis).len()))
})
}
}
+4
View File
@@ -10,12 +10,16 @@ use crate::{
pub const CHAIN_LIMIT: u32 = 64; pub const CHAIN_LIMIT: u32 = 64;
mod active; mod active;
mod draw_region;
mod holds; mod holds;
mod layout_holds;
mod painter; mod painter;
mod render_state; mod render_state;
pub use active::*; pub use active::*;
pub use draw_region::*;
pub use holds::*; pub use holds::*;
pub use layout_holds::*;
pub use painter::{Painter, PrimitiveLike}; pub use painter::{Painter, PrimitiveLike};
pub use render_state::*; pub use render_state::*;
+248 -136
View File
@@ -1,12 +1,12 @@
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
use crate::layout_diagnostics::{self as diag, Counter}; use crate::layout_diagnostics::{self as diag, Counter};
use crate::{ use crate::{
Axis, Holds, LayoutLen, Len, Px, PxVec2, RegionAlign, RenderedText, Size, StrongWidget, Axis, DrawRegion, Holds, LayoutLen, Len, Px, PxVec2, RegionAlign, RenderedText,
TextAttrs, TextBuffer, TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiVec2, Weight, RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle,
WidgetId, Widgets, UiRegion, UiRenderState, UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets,
render::{ render::{
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveHandle, PrimitiveInst, GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveInst, PrimitiveKind,
PrimitiveKind, TexturePrimitive, TexturePrimitive,
}, },
ui::render_state::DrawInfo, ui::render_state::DrawInfo,
}; };
@@ -17,8 +17,20 @@ pub struct Painter<'a> {
pub(super) state: &'a mut UiRenderState, pub(super) state: &'a mut UiRenderState,
pub(super) rsc: &'a mut dyn UiRsc, pub(super) rsc: &'a mut dyn UiRsc,
/// This widget's box, in the coordinates of `move_idx`. /// The box its parent gave it, in the coordinates of `move_idx`: what a
/// fraction of this widget's area is a fraction of, and what every region
/// it writes composes within. The same box on the ask that measures and
/// the ask that places, which is what keeps a fraction under it from
/// being resolved twice.
pub(super) region: UiRegion, pub(super) region: UiRegion,
/// Where this widget's drawing sits inside that box, in the box's own
/// coordinates: `FULL` while its answer is not yet known, and the box
/// its answer or its parent chose once one of them has.
pub(super) placement: UiRegion,
/// Whether this draw read its placement, which makes the drawing one
/// that holds for that placement alone -- the way reading a length in
/// pixels makes it hold for that length.
pub(super) reads_placement: bool,
/// That box in pixels, which its children's are a length of: threaded /// That box in pixels, which its children's are a length of: threaded
/// down from the box this widget was given rather than composed back up /// down from the box this widget was given rather than composed back up
/// the chain, so every length in layout is one multiply from its /// the chain, so every length in layout is one multiply from its
@@ -26,7 +38,11 @@ pub struct Painter<'a> {
pub(super) px: PxVec2, pub(super) px: PxVec2,
pub(super) mask: MaskIdx, pub(super) mask: MaskIdx,
pub(super) textures: Vec<TextureHandle>, pub(super) textures: Vec<TextureHandle>,
pub(super) primitives: Vec<PrimitiveHandle>, pub(super) primitives: Vec<RetainedPrimitive>,
pub(super) mask_region: Option<DrawRegion>,
pub(super) inherited_children: Vec<WidgetId>,
pub(super) extent_own: [Holds; 2],
pub(super) extent_under: [Holds; 2],
pub(super) children: Vec<WidgetId>, pub(super) children: Vec<WidgetId>,
/// The children asked about so far, so the first box each was asked in /// The children asked about so far, so the first box each was asked in
/// is the one recorded as its offer. /// is the one recorded as its offer.
@@ -57,13 +73,13 @@ pub struct Painter<'a> {
} }
impl<'a> Painter<'a> { impl<'a> Painter<'a> {
fn primitive_at<P: Primitive>(&mut self, primitive: P, region: UiRegion) { fn primitive_at<P: Primitive>(&mut self, primitive: P, region: DrawRegion) {
let kind = self.rsc.ui_mut().primitives.kind::<P>(); let kind = self.rsc.ui_mut().primitives.kind::<P>();
self.write(kind, primitive, region); self.write(kind, primitive, region);
} }
/// Takes the kind, for a caller writing many of one primitive. /// Takes the kind, for a caller writing many of one primitive.
fn write<P: Primitive>(&mut self, kind: PrimitiveKind<P>, primitive: P, region: UiRegion) { fn write<P: Primitive>(&mut self, kind: PrimitiveKind<P>, primitive: P, region: DrawRegion) {
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::PrimitiveWrites); diag::bump(Counter::PrimitiveWrites);
let h = self.state.layers.write( let h = self.state.layers.write(
@@ -72,15 +88,15 @@ impl<'a> Painter<'a> {
kind, kind,
id: self.id, id: self.id,
primitive, primitive,
region, region: region.resolve(self.region, self.placement),
mask_idx: self.mask, mask_idx: self.mask,
move_idx: self.move_idx, move_idx: self.move_idx,
}, },
); );
self.push_primitive(h); self.push_primitive(RetainedPrimitive { handle: h, region });
} }
fn push_primitive(&mut self, h: PrimitiveHandle) { fn push_primitive(&mut self, h: RetainedPrimitive) {
if self.mask != MaskIdx::NONE { if self.mask != MaskIdx::NONE {
// TODO: I have no clue if this works at all :joy: // TODO: I have no clue if this works at all :joy:
self.rsc.ui_mut().masks.push_ref(self.mask); self.rsc.ui_mut().masks.push_ref(self.mask);
@@ -88,28 +104,42 @@ impl<'a> Painter<'a> {
self.primitives.push(h); self.primitives.push(h);
} }
/// Writes a primitive to be rendered /// Writes a primitive over the whole of this widget's own box.
pub fn primitive(&mut self, primitive: impl PrimitiveLike) { pub fn primitive(&mut self, primitive: impl PrimitiveLike) {
let at = DrawRegion::Extent(UiRegion::FULL);
let primitive = primitive.into_primitive(self); let primitive = primitive.into_primitive(self);
self.primitive_at(primitive, self.region) self.primitive_at(primitive, at)
} }
pub fn primitive_within(&mut self, primitive: impl PrimitiveLike, region: UiRegion) { /// Writes in the frame by default. `DrawRegion::Extent` keeps the local
/// geometry attached to this widget's box without reading its placement.
pub fn primitive_within(
&mut self,
primitive: impl PrimitiveLike,
region: impl Into<DrawRegion>,
) {
let primitive = primitive.into_primitive(self); let primitive = primitive.into_primitive(self);
self.primitive_at(primitive, region.within(&self.region)); self.primitive_at(primitive, region.into());
} }
pub fn set_mask(&mut self, region: UiRegion) { /// Sets a mask in the selected frame or extent coordinates.
pub fn set_mask(&mut self, region: impl Into<DrawRegion>) {
let region = region.into();
self.mask_region = Some(region);
assert!(self.mask == MaskIdx::NONE); assert!(self.mask == MaskIdx::NONE);
self.mask = self.rsc.ui_mut().masks.push(Mask { self.mask = self.rsc.ui_mut().masks.push(Mask {
region, region: region.resolve(self.region, self.placement),
move_idx: self.move_idx, move_idx: self.move_idx,
}); });
} }
/// Draws a widget within this widget's region. /// Draws a widget in the whole of this widget's own box: it gets the
/// same region -- the same area for its fractions to be of -- and is put
/// where this widget was put. What a container that is only a wrapper
/// around one child wants, since its box is the child's.
pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> { pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> {
self.widget_within(id, UiRegion::FULL) let own = self.placement;
self.widget_at_inner(id, UiRegion::FULL, [Some(own.x), Some(own.y)], true)
} }
/// What a widget's rules declare its lengths to be, which whoever draws /// What a widget's rules declare its lengths to be, which whoever draws
@@ -125,65 +155,63 @@ impl<'a> Painter<'a> {
/// this frame; what it answered is still something this widget asked. /// this frame; what it answered is still something this widget asked.
pub fn undraw<W: ?Sized>(&mut self, id: &StrongWidget<W>) { pub fn undraw<W: ?Sized>(&mut self, id: &StrongWidget<W>) {
self.children.retain(|child| *child != id.id()); self.children.retain(|child| *child != id.id());
self.inherited_children.retain(|child| *child != id.id());
self.state.undraw_rec(id.id(), self.rsc); self.state.undraw_rec(id.id(), self.rsc);
} }
/// Draws a widget somewhere within this one. `region` is in this widget's /// Draws a widget somewhere within this one. `region` is in this widget's
/// own coordinates, and the child's declared lengths are still to be /// own region, and is the child's own region: what its declared lengths
/// taken from it. Where the child's drawing sits inside what it is given /// and its report are fractions of. Where its drawing sits inside that is
/// is the child's alignment, applied where the child is drawn, so a /// its own answer placed by its alignment.
/// container positions a child either by handing it a box of exactly its
/// length or by leaving it room and letting its alignment decide.
pub fn widget_within<'s, W: ?Sized>( pub fn widget_within<'s, W: ?Sized>(
&'s mut self, &'s mut self,
id: &'s StrongWidget<W>, id: &'s StrongWidget<W>,
region: UiRegion, region: UiRegion,
) -> DrawResult<'s, 'a, W> { ) -> DrawResult<'s, 'a, W> {
self.widget_at(id, region, region.size(), [false; 2]) self.widget_at(id, region, [None; 2])
} }
/// Draws a widget in `region`, saying what the answer means. /// Draws a widget in `region`, saying where in it the drawing goes.
/// ///
/// `reports_of` is what a fraction the child reports is a fraction of, as /// `region` is the child's own area: what a fraction it declares or
/// lengths of this widget's own box. It is the box the child was given /// reports is a fraction of, and the coordinates the regions it writes
/// wherever that box is the child's whole area -- a pad's inset, a stack /// compose within. It is the same box on the ask that measures and the
/// child, a scroll's content -- and a span passes its own extent along /// ask that places, which is what stops a fraction under it being
/// the row instead: it offers each child the room left from its cursor, /// resolved twice.
/// because a text has to wrap at the width actually there, while
/// `rel(0.5)` still means half the span wherever the child sits in it.
/// ///
/// A `decided` axis is one where this box was chosen from the widget's /// `placement` is what of that region the child's drawing takes, per
/// own answer. On those the answer is not placed inside the box again: it /// axis, wherever this widget is choosing. `None` leaves the axis to the
/// already is the box, and a fraction taken of it a second time would /// child's own answer and alignment, which is what
/// shrink it twice. A container uses that where it hands back exactly /// [`Self::widget_within`] passes. A span passes the whole row as the
/// what a child asked for -- a span placing a child at the length it /// region, so `rel(0.5)` is half the row wherever the child sits in it,
/// reported, a scroll giving its content the content's own length. /// and places the child by passing the slot along its axis.
pub fn widget_at<'s, W: ?Sized>( pub fn widget_at<'s, W: ?Sized>(
&'s mut self, &'s mut self,
id: &'s StrongWidget<W>, id: &'s StrongWidget<W>,
region: UiRegion, region: UiRegion,
reports_of: UiVec2, placement: [Option<UiSpan>; 2],
decided: [bool; 2],
) -> DrawResult<'s, 'a, W> { ) -> DrawResult<'s, 'a, W> {
self.widget_at_inner(id, region, placement, false)
}
fn widget_at_inner<'s, W: ?Sized>(
&'s mut self,
id: &'s StrongWidget<W>,
region: UiRegion,
placement: [Option<UiSpan>; 2],
inherited: bool,
) -> DrawResult<'s, 'a, W> {
if inherited {
if !self.inherited_children.contains(&id.id()) {
self.inherited_children.push(id.id());
}
} else {
self.inherited_children.retain(|child| *child != id.id());
}
let region_node = self.rsc.widgets().is_region_node(id.id()); let region_node = self.rsc.widgets().is_region_node(id.id());
let declared = self.declared_lens(id); let declared = self.declared_lens(id);
let align = self.rsc.widgets().alignment(id.id()); let align = self.rsc.widgets().alignment(id.id());
// A rule this box was already chosen from is not resolved into it a let (local, placement) = ask_box(region, declared, align, placement);
// second time. The box is that rule's length already, so resolving
// it again takes the fraction twice -- a widget declaring half of a
// stack, in the stack its own answer made half a row, is a quarter
// of the row. Pixels survive it, being the same length wherever they
// are taken from, which is why only a share ever shrank.
let resolve = AXES.map(|axis| match decided[axis as usize] {
true => None,
false => declared[axis as usize],
});
// 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 resolve.iter().any(Option::is_some) {
true => declared_box(region, resolve, align),
false => region,
};
let within = match local == UiRegion::FULL { let within = match local == UiRegion::FULL {
true => self.region, true => self.region,
false => local.within(&self.region), false => local.within(&self.region),
@@ -207,11 +235,16 @@ impl<'a> Painter<'a> {
.get(&id.id()) .get(&id.id())
.map_or(given_len, |a| a.offer_len), .map_or(given_len, |a| a.offer_len),
}; };
let offer_placement = if first_ask {
placement
} else {
self.state
.active
.get(&id.id())
.map_or(placement, |a| a.offer_placement)
};
let px = given_len.to_px(self.px); let px = given_len.to_px(self.px);
let offered_px = offer_len.to_px(self.offered_px); let offered_px = offer_len.to_px(self.offered_px);
// Whether this ask is the child's offer question, which is a question
// about lengths: the same lengths somewhere else is the same question.
let answers_offer = self.at_offer && px == offered_px;
// The answer and what it holds for, both about the box asked in. The // The answer and what it holds for, both about the box asked in. The
// child's record may say something else once its drawing has been // child's record may say something else once its drawing has been
// placed: a drawing made again in its placed box holds for that box. // placed: a drawing made again in its placed box holds for that box.
@@ -225,27 +258,38 @@ impl<'a> Painter<'a> {
parent_move: self.move_idx, parent_move: self.move_idx,
region_node, region_node,
mask: self.mask, mask: self.mask,
given_len, given_region: local,
offer_len, offer_len,
offer_placement,
px, px,
offered_px, offered_px,
decided, placement,
}, },
None, None,
self.rsc, self.rsc,
); );
if answers_offer {
self.state.active.get_mut(&id.id()).unwrap().answer = (size, holds);
}
// Whatever the child's answer holds for keeps this one to the boxes // Whatever the child's answer holds for keeps this one to the boxes
// that give the child a length inside it. // that give the child a length inside it.
for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) { for axis in AXES {
*under = under.and(holds[axis as usize].through(local.axis(axis).len())); let n = axis as usize;
let frame = holds.frame[n].through(local.axis(axis).len());
self.under[n] = self.under[n].and(frame);
if inherited && declared[n].is_none() {
self.extent_under[n] = self.extent_under[n].and(holds.extent[n]);
self.reads_placement |= holds.placement.is_some();
} else {
let extent = placement[n].unwrap_or(UiSpan::FULL).len();
self.under[n] = self.under[n].and(
holds.extent[n]
.through(extent)
.through(local.axis(axis).len()),
);
}
} }
DrawResult { DrawResult {
child: id, child: id,
painter: self, painter: self,
size: in_parent_frame(size, reports_of, declared), size: in_parent_frame(size, local.size(), declared),
} }
} }
@@ -277,42 +321,50 @@ impl<'a> Painter<'a> {
} }
} }
/// A child's length in the box it is about to be offered, if it can be /// A child's length in the region it is about to be offered, if it can
/// had without drawing it: from its hint, or from a drawing it already /// be had without drawing it: from its hint, or from a drawing it already
/// has that holds for that box. `reports_of` is what a fraction in the /// has that holds for that box.
/// answer is a fraction of, as it is for [`Self::widget_at`].
pub fn known_len<W: ?Sized>( pub fn known_len<W: ?Sized>(
&mut self, &mut self,
child: &StrongWidget<W>, child: &StrongWidget<W>,
axis: Axis, axis: Axis,
region: UiRegion, region: UiRegion,
reports_of: UiVec2, placement: [Option<UiSpan>; 2],
) -> Option<LayoutLen> { ) -> Option<LayoutLen> {
let declared = self.declared_lens(child); let declared = self.declared_lens(child);
let align = self.rsc.widgets().alignment(child.id()); let align = self.rsc.widgets().alignment(child.id());
let local = declared_box(region, declared, align); let (local, placement) = ask_box(region, declared, align, placement);
let first_ask = self.offer(child.id()); let first_ask = self.at_offer && !self.offered.contains(&child.id());
if first_ask && let Some(active) = self.state.active.get_mut(&child.id()) {
active.offer_len = local.size();
}
if let Some(hint) = self.size_hint(child, axis) { if let Some(hint) = self.size_hint(child, axis) {
return Some(hint); return Some(hint);
} }
let px = local.size().to_px(self.px); let px = local.size().to_px(self.px);
let (size, holds) = let (size, holds) = self.state.retained_size(
self.state child.id(),
.retained_size(child.id(), px, self.move_idx, self.rsc.widgets())?; px,
placement,
self.move_idx,
self.rsc.widgets(),
)?;
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::RetainedSizeHits); diag::bump(Counter::RetainedSizeHits);
self.depend_on(child); self.depend_on(child);
if first_ask { if first_ask {
self.offered.push(child.id());
let active = self.state.active.get_mut(&child.id()).unwrap(); let active = self.state.active.get_mut(&child.id()).unwrap();
active.answer = (size, holds); active.offer_len = local.size();
active.offer_placement = placement;
} }
let placement = UiRegion {
x: placement[0].unwrap_or(UiSpan::FULL),
y: placement[1].unwrap_or(UiSpan::FULL),
};
let holds = holds.in_frame(placement);
for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) { for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) {
*under = under.and(holds[axis as usize].through(local.axis(axis).len())); *under = under.and(holds[axis as usize].through(local.axis(axis).len()));
} }
Some(in_parent_frame(size, reports_of, declared).axis(axis)) Some(in_parent_frame(size, local.size(), declared).axis(axis))
} }
/// Whether this is the first box a child is asked about in during a draw /// Whether this is the first box a child is asked about in during a draw
@@ -344,20 +396,24 @@ impl<'a> Painter<'a> {
ui.text.render(buffer, attrs, width) ui.text.render(buffer, attrs, width)
} }
/// Writes glyphs in the selected frame or extent coordinates.
// TODO: merge the text methods into the primitive ones. // TODO: merge the text methods into the primitive ones.
pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) { pub fn glyphs(&mut self, text: &RenderedText, origin: impl Into<DrawRegion>) {
let origin = origin.into();
let kind = self.rsc.ui_mut().primitives.kind::<GlyphPrimitive>(); let kind = self.rsc.ui_mut().primitives.kind::<GlyphPrimitive>();
for glyph in text.glyphs.iter() { for glyph in text.glyphs.iter() {
let mut region = origin; let region = origin.map(|mut region| {
region.x.end = region.x.start; region.x.end = region.x.start;
region.y.end = region.y.start; region.y.end = region.y.start;
let mut region = region.offset(UiVec2::from_px(glyph.offset)); let mut region = region.offset(UiVec2::from_px(glyph.offset));
let size = PxVec2::new( let size = PxVec2::new(
Px::from_int(glyph.entry.width as i32), Px::from_int(glyph.entry.width as i32),
Px::from_int(glyph.entry.height as i32), Px::from_int(glyph.entry.height as i32),
); );
region.x.end = region.x.start.offset(size.x); region.x.end = region.x.start.offset(size.x);
region.y.end = region.y.start.offset(size.y); region.y.end = region.y.start.offset(size.y);
region
});
self.write( self.write(
kind, kind,
GlyphPrimitive { GlyphPrimitive {
@@ -372,12 +428,26 @@ impl<'a> Painter<'a> {
} }
} }
/// This widget's box, in the coordinates its own primitives are written /// The box this widget's parent gave it, in the coordinates its own
/// in -- so a region composed `within` it may be drawn directly. /// primitives are written in -- so a region composed `within` it may be
/// drawn directly. Its own box is [`Self::placement`] of this one.
pub fn region(&self) -> UiRegion { pub fn region(&self) -> UiRegion {
self.region self.region
} }
/// Where this widget's drawing goes inside the box it was given, in that
/// box's coordinates: what its own answer took of it, or what its parent
/// chose for it. `FULL` on the ask that measures, since nothing has been
/// placed yet.
///
/// Reading it is what says the drawing depends on it, so a widget that
/// positions its own content reads it and is drawn again once its box is
/// known, and one that fills whatever it is given never is.
pub fn placement(&mut self) -> UiRegion {
self.reads_placement = true;
self.placement
}
/// Where this widget sits in a box longer than the length it takes. A /// Where this widget sits in a box longer than the length it takes. A
/// widget that positions its own content reads it to place that content /// widget that positions its own content reads it to place that content
/// the way the box around it would have placed the widget. /// the way the box around it would have placed the widget.
@@ -403,20 +473,52 @@ impl<'a> Painter<'a> {
.is_some() .is_some()
} }
/// This widget's box in pixels. Reading it makes the drawing one that /// The part of this widget's box that something of `size` takes, at the
/// holds for this box only, until `holds` says how far it goes. /// near edge. A container that reports one child's size gives every child
pub fn px_size(&mut self) -> PxVec2 { /// this, so what it draws is inside what it says it occupies.
for (own, len) in self.own.iter_mut().zip([self.px.x, self.px.y]) { pub fn box_of(&self, size: Size) -> UiRegion {
if *own == Holds::ANY { let lens = placed_lens(size, [None; 2], [false; 2]);
*own = Holds::at(len); placed_box(UiRegion::FULL, lens, RegionAlign::NEAR)
}
}
self.px
} }
/// One axis of this widget's box in pixels. Prefer this to /// This widget's own 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) -> PxVec2 {
PxVec2::new(self.px_len(Axis::X), self.px_len(Axis::Y))
}
/// One axis of this widget's own box in pixels. Prefer this to
/// [`Self::px_size`] when the other axis cannot affect the drawing. /// [`Self::px_size`] when the other axis cannot affect the drawing.
pub fn px_len(&mut self, axis: Axis) -> Px { pub fn px_len(&mut self, axis: Axis) -> Px {
let part = self.placement.axis(axis).len();
let len = part.to_px(self.px.axis(axis));
let own = &mut self.extent_own[axis as usize];
if *own == Holds::ANY {
*own = Holds::at(len);
}
len
}
/// The lengths of this widget's own 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, holds: impl Into<Holds>) {
let part = self.placement.axis(axis).len();
let holds = holds.into();
debug_assert!(
holds.contains(part.to_px(self.px.axis(axis))),
"'{}' ({:?}) says its drawing holds for lengths that leave out its own box",
self.label(),
self.id
);
self.extent_own[axis as usize] = holds;
}
/// One axis of the box this widget's parent gave it, in pixels -- what a
/// fraction of its area resolves against, and so what a container divides
/// among its children. Its own box is a part of this one.
pub fn region_px_len(&mut self, axis: Axis) -> Px {
let len = self.px.axis(axis); let len = self.px.axis(axis);
let own = &mut self.own[axis as usize]; let own = &mut self.own[axis as usize];
if *own == Holds::ANY { if *own == Holds::ANY {
@@ -425,15 +527,14 @@ impl<'a> Painter<'a> {
len len
} }
/// The lengths of this widget's box on `axis` that what it is drawing /// [`Self::holds`] stated about the region rather than about this
/// holds for -- the same primitives, in the same fractions and offsets /// widget's own box, for a container whose drawing turns on the box it
/// of the box, and the same reported size. A widget that read its /// was given rather than on the part of it it took.
/// length in pixels holds for that one alone until it says otherwise. pub fn region_holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
pub fn holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
let holds = holds.into(); let holds = holds.into();
debug_assert!( debug_assert!(
holds.contains(self.px.axis(axis)), holds.contains(self.px.axis(axis)),
"'{}' ({:?}) says its drawing holds for lengths that leave out its own box", "'{}' ({:?}) says its drawing holds for lengths that leave out its region",
self.label(), self.label(),
self.id self.id
); );
@@ -523,19 +624,16 @@ impl PrimitiveLike for &TextureHandle {
} }
} }
/// A child's answer as lengths of the parent's own box. A widget reports a /// A child's answer as lengths of the parent's own region. A widget reports
/// fraction, and `reports_of` is the length that fraction is of: the box the /// a fraction of its own region, and `of` is that region as a length of this
/// child was given wherever that is the child's whole area, and the parent's /// one. Pixels come through untouched, being that many pixels wherever they
/// own extent wherever the box is a positional remainder, as a span's is /// end up. A declared axis is already the parent's: it resolved the rule in
/// after an earlier child. Pixels come through untouched either way, being /// its own region, and the rule is what the report says.
/// that many pixels wherever they end up. A declared axis is already the fn in_parent_frame(size: Size, of: UiVec2, declared: [Option<LayoutLen>; 2]) -> Size {
/// parent's: it resolved the rule in its own box, and the rule is what the
/// report says.
fn in_parent_frame(size: Size, reports_of: UiVec2, declared: [Option<LayoutLen>; 2]) -> Size {
let mut size = size; let mut size = size;
for (axis, declared) in AXES.into_iter().zip(declared) { for (axis, declared) in AXES.into_iter().zip(declared) {
if declared.is_none() { if declared.is_none() {
*size.axis_mut(axis) = size.axis(axis).within_len(reports_of.axis(axis)); *size.axis_mut(axis) = size.axis(axis).within_len(of.axis(axis));
} }
} }
size size
@@ -564,9 +662,9 @@ pub(crate) fn declared_lens(widgets: &Widgets, id: WidgetId) -> [Option<LayoutLe
/// Whether what a widget reported along an axis is the whole of the box it /// Whether what a widget reported along an axis is the whole of the box it
/// is in rather than a part to be placed inside it. A share fills, because a /// is in rather than a part to be placed inside it. A share fills, because a
/// share is a length only to whoever divides one, and whoever did is the one /// share is a length only to whoever divides one, and whoever did is the one
/// that handed down this box. A declared axis does too: `declared_box` /// that handed down this box. A declared axis does too: the rule already gave
/// already placed it, in the parent's box, and the rule's length is what the /// the region its length, and the rule's length is what the widget reports
/// widget reports there. And an axis the parent decided from the answer is /// there. And an axis the parent decided from the answer is
/// the answer already. /// the answer already.
pub(crate) fn fills(reported: LayoutLen, declared: Option<LayoutLen>, decided: bool) -> bool { pub(crate) fn fills(reported: LayoutLen, declared: Option<LayoutLen>, decided: bool) -> bool {
reported.leftover != Weight::ZERO || declared.is_some() || decided reported.leftover != Weight::ZERO || declared.is_some() || decided
@@ -614,21 +712,35 @@ pub(crate) fn placed_box(region: UiRegion, lens: UiVec2, align: RegionAlign) ->
placed placed
} }
/// Takes a widget's declared lengths in the box `region` is given in, since a /// A child's own region and where in it its drawing goes, from the box it is
/// fraction of a length means a fraction of that one, and puts what is left /// offered, the lengths its rules declare, and what its parent chose.
/// over on the side its alignment says. A caller that already reserved the ///
/// space hands back the same length, so this is the identity for it. /// A rule gives the region its length outright -- that is what makes a rule
pub(crate) fn declared_box( /// win, and it is why a widget under one never learns of it -- and the region
/// then sits where its parent placed it, or where its alignment says if its
/// parent left the axis open. With no rule the region is the whole of what
/// was offered, since that is the area a fraction under it is a fraction of,
/// and what the parent chose is where in it the drawing goes. So the two
/// coordinate spaces are the same one wherever a placement survives.
pub(crate) fn ask_box(
mut region: UiRegion, mut region: UiRegion,
declared: [Option<LayoutLen>; 2], declared: [Option<LayoutLen>; 2],
align: RegionAlign, align: RegionAlign,
) -> UiRegion { placement: [Option<UiSpan>; 2],
for (axis, len) in AXES.into_iter().zip(declared) { ) -> (UiRegion, [Option<UiSpan>; 2]) {
let Some(len) = len else { continue }; let mut placed = [None; 2];
for (axis, (len, chosen)) in AXES.into_iter().zip(declared.into_iter().zip(placement)) {
let Some(len) = len else {
placed[axis as usize] = chosen;
continue;
};
let span = region.axis_mut(axis); let span = region.axis_mut(axis);
let len = Len::from_parts(len.rel, len.px); let len = Len::from_parts(len.rel, len.px);
span.start += (span.len() - len).scale(align.axis(axis).rel()); span.start = match chosen {
Some(chosen) => chosen.start,
None => span.start + (span.len() - len).scale(align.axis(axis).rel()),
};
span.end = span.start + len; span.end = span.start + len;
} }
region (region, placed)
} }
+318 -272
View File
@@ -1,9 +1,9 @@
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind}; use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind};
use crate::ui::painter::{declared_box, declared_lens, placed_box, placed_lens}; use crate::ui::painter::{ask_box, declared_lens, placed_box, placed_lens};
use crate::{ use crate::{
ActiveData, Axis, DrawLayers, Holds, IdLike, LayoutLen, Len, MaskIdx, MoveIdx, Moves, Painter, ActiveData, Axis, DrawLayers, Holds, IdLike, LayoutHolds, LayoutLen, MaskIdx, MoveIdx, Moves,
PixelRegion, Px, PxVec2, Rel, Size, StrongWidget, UiRegion, UiRsc, UiSpan, UiVec2, Weight, Painter, PixelRegion, PxVec2, Size, StrongWidget, UiRegion, UiRsc, UiSpan, UiVec2, Weight,
WidgetId, Widgets, WidgetId, Widgets,
util::{HashMap, Vec2}, util::{HashMap, Vec2},
}; };
@@ -20,20 +20,36 @@ pub(super) struct DrawInfo {
pub parent_move: MoveIdx, pub parent_move: MoveIdx,
pub region_node: bool, pub region_node: bool,
pub mask: MaskIdx, pub mask: MaskIdx,
/// The box its parent gave it, as lengths of the parent's own box, and /// The frame in the parent widget's coordinates, before composition.
/// the lengths of the box it was first asked about in the same form. pub given_region: UiRegion,
/// Both describe the box the *parent* stated, so the second, placing ask /// The original offer's lengths relative to the parent's own offer.
/// carries them unchanged while its own region is the placement inside.
pub given_len: UiVec2,
pub offer_len: UiVec2, pub offer_len: UiVec2,
pub offer_placement: [Option<UiSpan>; 2],
/// This ask's box in pixels, and the offer's: one multiply from the /// This ask's box in pixels, and the offer's: one multiply from the
/// parent's own, which is where every pixel length in layout comes from. /// parent's own, which is where every pixel length in layout comes from.
pub px: PxVec2, pub px: PxVec2,
pub offered_px: PxVec2, pub offered_px: PxVec2,
/// The axes along which the parent chose this box from the widget's own /// What of that region the parent chose to put the drawing in, per axis.
/// answer, so the answer is not placed inside it again. See /// `None` leaves the axis to the widget's own answer and its alignment.
/// [`Painter::widget_at`]. /// See [`Painter::widget_at`].
pub decided: [bool; 2], pub placement: [Option<UiSpan>; 2],
}
impl DrawInfo {
/// The axes the parent chose the placement on, which are the axes the
/// answer is not placed inside its region again.
fn decided(&self) -> [bool; 2] {
self.placement.map(|span| span.is_some())
}
/// The placement to draw in before the answer is known: what the parent
/// chose, and the whole region on any axis it left open.
fn offered_placement(&self) -> UiRegion {
UiRegion {
x: self.placement[0].unwrap_or(UiSpan::FULL),
y: self.placement[1].unwrap_or(UiSpan::FULL),
}
}
} }
pub struct UiRenderState { pub struct UiRenderState {
@@ -43,7 +59,9 @@ pub struct UiRenderState {
old_root: Option<WidgetId>, old_root: Option<WidgetId>,
/// Whether the output has changed since the last update. A frame is /// Whether the output has changed since the last update. A frame is
/// owed for that whether or not anything has to be drawn again. /// owed for that whether or not anything has to be drawn again: every
/// fraction becomes pixels against the output, in the shader's uniform
/// as well as here.
resized: bool, resized: bool,
/// A widget's move slot, which outlives any one `ActiveData`: a redraw /// A widget's move slot, which outlives any one `ActiveData`: a redraw
/// replaces that while its children go on pointing at the slot. /// replaces that while its children go on pointing at the slot.
@@ -83,13 +101,28 @@ impl UiRenderState {
/// size is applied where a fraction becomes pixels -- here in `to_px`, /// size is applied where a fraction becomes pixels -- here in `to_px`,
/// and in the shader by its uniform. A resize therefore rewrites no /// and in the shader by its uniform. A resize therefore rewrites no
/// retained entry at all. /// retained entry at all.
pub fn resize(&mut self, size: impl Into<Vec2>) { ///
/// The root is the only widget a resize marks, and only where the new
/// output falls outside what its answer holds for: that range is the
/// intersection of everything under it, so admitting the new output says
/// the whole tree still stands. Where it does not, the ordinary walk
/// draws the root, and each widget's own range decides how far down the
/// new length reaches.
pub fn resize(&mut self, size: impl Into<Vec2>, widgets: &mut Widgets) {
let size = PxVec2::from_f32(size.into()); let size = PxVec2::from_f32(size.into());
if size == self.output_size { if size == self.output_size {
return; return;
} }
self.output_size = size; self.output_size = size;
self.resized = true; self.resized = true;
let Some(root) = self.old_root else { return };
let stands = self
.active
.get(&root)
.is_some_and(|active| active.answers_at(active.given_region.size().to_px(size)));
if !stands {
widgets.needs_redraw.insert(root);
}
} }
/// The root is asked about in the output: the window is where a fraction /// The root is asked about in the output: the window is where a fraction
@@ -106,11 +139,12 @@ impl UiRenderState {
parent_move: MoveIdx::NONE, parent_move: MoveIdx::NONE,
region_node: false, region_node: false,
mask: MaskIdx::NONE, mask: MaskIdx::NONE,
given_len: region.size(), given_region: region,
offer_len: UiVec2::FULL_SIZE, offer_len: UiVec2::FULL_SIZE,
offer_placement: [None; 2],
px, px,
offered_px: px, offered_px: px,
decided: [false; 2], placement: [None; 2],
} }
} }
@@ -143,17 +177,6 @@ impl UiRenderState {
if self.root_changed(root) { if self.root_changed(root) {
self.redraw_all(root, rsc); self.redraw_all(root, rsc);
self.old_root = root.map(|r| r.id()); 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 region = Self::root_region(root.id(), rsc.widgets());
let info = self.root_info(region);
let answer = self.draw_inner(root.id(), region, info, None, rsc);
self.active.get_mut(&root.id()).unwrap().answer = answer;
} }
self.resized = false; self.resized = false;
if rsc.widgets().has_updates() { if rsc.widgets().has_updates() {
@@ -175,11 +198,13 @@ impl UiRenderState {
} }
fn root_region(id: WidgetId, widgets: &Widgets) -> UiRegion { fn root_region(id: WidgetId, widgets: &Widgets) -> UiRegion {
declared_box( ask_box(
UiRegion::FULL, UiRegion::FULL,
declared_lens(widgets, id), declared_lens(widgets, id),
widgets.alignment(id), widgets.alignment(id),
[None; 2],
) )
.0
} }
pub(super) fn draw_inner( pub(super) fn draw_inner(
@@ -189,83 +214,108 @@ impl UiRenderState {
info: DrawInfo, info: DrawInfo,
mut old: Option<ActiveData>, mut old: Option<ActiveData>,
rsc: &mut dyn UiRsc, rsc: &mut dyn UiRsc,
) -> (Size, [Holds; 2]) { ) -> (Size, LayoutHolds) {
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
{ {
diag::bump(Counter::DrawRequests); diag::bump(Counter::DrawRequests);
diag::draw_request(id, info.parent, region, info.px, info.region_node); diag::draw_request(id, info.parent, region, info.px, info.region_node);
} }
let align = rsc.widgets().alignment(id); let align = rsc.widgets().alignment(id);
// Nothing this widget has is an answer while something it measured // Nothing this widget measured can be dirty while it draws: layout is
// is dirty: settling that changes what it would report, and a widget // one bottom-up walk, so anything deeper has settled or deferred to
// settled inside its parent's draw tells nobody -- the comparison // its own parent, and a deferred one leaves that parent marked.
// that marks a reader is in `redraw`, which is not what asked here. let stale = rsc.widgets().needs_redraw.contains(&id);
// Both retained routes are an answer, so the question is asked once
// rather than by each of them.
let stale =
rsc.widgets().needs_redraw.contains(&id) || self.dirty_size_under(id, rsc.widgets());
let replace_answer = self.answer_invalid.remove(&id) || (self.replace_answers && stale); let replace_answer = self.answer_invalid.remove(&id) || (self.replace_answers && stale);
let retained = match replace_answer || stale { let retained = match replace_answer || stale {
true => None, true => None,
false => self false => self
.retained_answer(id, info) .retained_answer(id, info)
.or_else(|| self.try_reuse(id, region, info, rsc)), .or_else(|| self.try_reuse(id, region, info.offered_placement(), info, rsc)),
}; };
let answer = retained.unwrap_or_else(|| { let answer = retained.unwrap_or_else(|| {
if old.is_none() { if old.is_none() {
old = self.remove(id, false, rsc); old = self.remove(id, false, rsc);
} }
self.draw_at(id, region, info, old.take(), rsc) self.draw_at(id, region, info.offered_placement(), info, old.take(), rsc)
}); });
let declared = declared_lens(rsc.widgets(), id); let declared = declared_lens(rsc.widgets(), id);
// The second, final ask is in a box chosen from the answer on both // Where the drawing goes, in the region's own coordinates: what the
// axes, which is also what makes it terminate. // parent chose, and on any axis it left open, what the answer took of
let lens = placed_lens(answer.0, declared, info.decided); // the region placed by the widget's alignment. The region itself does
let placed = placed_box(region, lens, align); // not change, so nothing under it resolves a fraction a second time.
let placed_info = DrawInfo { let lens = placed_lens(answer.0, declared, info.decided());
px: lens.to_px(info.px), let own = placed_box(UiRegion::FULL, lens, align);
decided: [true; 2], let placement = UiRegion {
..info x: info.placement[0].unwrap_or(own.x),
y: info.placement[1].unwrap_or(own.y),
}; };
self.place(id, placed, placed_info, rsc); self.place(id, region, placement, info, rsc);
// The answer is only reusable while both parts of the operation are: // On axes chosen by the parent, measurement and drawing share an
// what the widget reported in the box it was asked in, and what it // extent. Otherwise the answer fixes the final extent as a function
// drew in the box its report selected. Express the latter's contract // of the frame, so pull that drawing's validity back through it.
// back in terms of the box asked in before handing it to the parent.
let drawing_holds = self.active[&id].holds; let drawing_holds = self.active[&id].holds;
let mut settled = answer; let mut settled = answer;
for axis in AXES { for axis in AXES {
settled.1[axis as usize] = let n = axis as usize;
settled.1[axis as usize].and(drawing_holds[axis as usize].through(lens.axis(axis))); settled.1.frame[n] = settled.1.frame[n].and(drawing_holds.frame[n]);
if info.placement[n].is_some() {
settled.1.extent[n] = settled.1.extent[n].and(drawing_holds.extent[n]);
} else {
settled.1.frame[n] = settled.1.frame[n]
.and(drawing_holds.extent[n].through(placement.axis(axis).len()));
}
}
if drawing_holds.placement.is_some() && info.placement.iter().any(Option::is_some) {
settled.1.placement = Some(info.offered_placement());
} }
let active = self.active.get_mut(&id).unwrap(); let active = self.active.get_mut(&id).unwrap();
// Whoever asked owns how the box was reached: the box it stated, and // Whoever asked owns how the box was reached: the box it stated, and
// what of that box the answer then took. A local redraw asks the // what of that box the answer then took. A local redraw asks the
// same question again from these. // same question again from these.
active.given = region; active.region = region;
active.given_len = info.given_len; active.given_region = info.given_region;
active.offer_len = info.offer_len; active.offer_len = info.offer_len;
active.answer = settled; if info.placement == info.offer_placement && info.px == info.offered_px {
active.decided = info.decided; active.answer = Some(settled);
active.offer_placement = info.offer_placement;
}
active.decided = info.decided();
active.own_align = align; active.own_align = align;
active.depth = info.depth; // A subtree can be reused whole under a different parent -- same box,
// same layer, same region node -- and nothing in the drawing says it
// changed hands. Two things read who its parent is: a deferral, which
// marks whoever has it to draw, and the old parent's list of children,
// which its next draw undraws whatever is missing from.
let old_parent = std::mem::replace(&mut active.parent, info.parent);
if old_parent != info.parent
&& let Some(old_parent) = old_parent
&& let Some(old_parent) = self.active.get_mut(&old_parent)
{
old_parent.children.retain(|child| *child != id);
old_parent.inherited_children.retain(|child| *child != id);
}
settled settled
} }
/// Draws a widget in the final box its answer chose, reusing the drawing /// Recompose retained geometry when the evaluation still holds at this extent.
/// already there where its retained contract holds for that box. The fn place(
/// symbolic box can be unchanged while the box it sits in changed pixel &mut self,
/// length, so what reuse checks is the box in pixels. id: WidgetId,
fn place(&mut self, id: WidgetId, placed: UiRegion, info: DrawInfo, rsc: &mut dyn UiRsc) { region: UiRegion,
if self.try_reuse(id, placed, info, rsc).is_none() { placement: UiRegion,
#[cfg(feature = "layout-diagnostics")] info: DrawInfo,
diag::bump(Counter::PlaceRedraws); rsc: &mut dyn UiRsc,
let old = self.remove(id, false, rsc); ) {
self.draw_at(id, placed, info, old, rsc); if self.try_reuse(id, region, placement, info, rsc).is_some() {
return;
} }
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::PlaceRedraws);
let old = self.remove(id, false, rsc);
self.draw_at(id, region, placement, info, old, rsc);
} }
/// Calls a widget's `draw` and keeps what it drew in `region`. /// Calls a widget's `draw` and keeps what it drew in `region`.
@@ -273,10 +323,11 @@ impl UiRenderState {
&mut self, &mut self,
id: WidgetId, id: WidgetId,
region: UiRegion, region: UiRegion,
placement: UiRegion,
info: DrawInfo, info: DrawInfo,
old: Option<ActiveData>, old: Option<ActiveData>,
rsc: &mut dyn UiRsc, rsc: &mut dyn UiRsc,
) -> (Size, [Holds; 2]) { ) -> (Size, LayoutHolds) {
let (move_idx, local, retired_move) = match info.region_node { let (move_idx, local, retired_move) = match info.region_node {
// Its box becomes its movable region, so it draws in that // Its box becomes its movable region, so it draws in that
// region's coordinates and its box is one entry to rewrite. // region's coordinates and its box is one entry to rewrite.
@@ -290,20 +341,25 @@ impl UiRenderState {
false => (info.parent_move, region, self.slots.remove(&id)), false => (info.parent_move, region, self.slots.remove(&id)),
}; };
let (old_children, old_answer) = match old { let (old_children, old_answer) = match old {
Some(old) => (old.children, Some(old.answer)), Some(old) => (old.children, old.answer),
None => (Vec::new(), None), None => (Vec::new(), None),
}; };
rsc.widgets_mut().needs_redraw.remove(&id); rsc.widgets_mut().needs_redraw.remove(&id);
// A box of the offered lengths asks the offer's question wherever it // Only evaluation at the original offer establishes the children's
// sits, since what a drawing depends on is its lengths -- and // offers. A placing evaluation must not overwrite that question.
// equality is the comparison, these being counts of a step rather
// than floats to be compared for nearness.
let px = info.px; let px = info.px;
let at_offer = px == info.offered_px; let at_offer = px == info.offered_px
&& placement
== UiRegion {
x: info.offer_placement[0].unwrap_or(UiSpan::FULL),
y: info.offer_placement[1].unwrap_or(UiSpan::FULL),
};
let mut painter = Painter { let mut painter = Painter {
state: self, state: self,
region: local, region: local,
placement,
reads_placement: false,
px, px,
mask: info.mask, mask: info.mask,
layer: info.layer, layer: info.layer,
@@ -311,6 +367,8 @@ impl UiRenderState {
id, id,
textures: Vec::new(), textures: Vec::new(),
primitives: Vec::new(), primitives: Vec::new(),
mask_region: None,
inherited_children: Vec::new(),
children: Vec::new(), children: Vec::new(),
offered: Vec::new(), offered: Vec::new(),
offered_px: info.offered_px, offered_px: info.offered_px,
@@ -318,6 +376,8 @@ impl UiRenderState {
size_deps: Vec::new(), size_deps: Vec::new(),
own: [Holds::ANY; 2], own: [Holds::ANY; 2],
under: [Holds::ANY; 2], under: [Holds::ANY; 2],
extent_own: [Holds::ANY; 2],
extent_under: [Holds::ANY; 2],
depth: info.depth, depth: info.depth,
move_idx, move_idx,
rsc, rsc,
@@ -338,10 +398,16 @@ impl UiRenderState {
state: _, state: _,
rsc: _, rsc: _,
region: _, region: _,
placement: _,
reads_placement,
px: _, px: _,
mask, mask,
textures, textures,
primitives, primitives,
mask_region,
inherited_children,
extent_own,
extent_under,
children, children,
offered: _, offered: _,
offered_px: _, offered_px: _,
@@ -379,9 +445,16 @@ impl UiRenderState {
"'{}' ({id:?}) clips to {px:?} and reports {size}", "'{}' ({id:?}) clips to {px:?} and reports {size}",
rsc.widgets().label(id), rsc.widgets().label(id),
); );
let holds = [own[0].and(under[0]), own[1].and(under[1])]; let holds = LayoutHolds {
frame: [own[0].and(under[0]), own[1].and(under[1])],
extent: [
extent_own[0].and(extent_under[0]),
extent_own[1].and(extent_under[1]),
],
placement: reads_placement.then_some(placement),
};
debug_assert!( debug_assert!(
holds[0].contains(px.x) && holds[1].contains(px.y), holds.contains(px, placement),
"'{}' ({id:?}) drew in {px:?}, outside the ranges it reported: {holds:?}", "'{}' ({id:?}) drew in {px:?}, outside the ranges it reported: {holds:?}",
rsc.widgets().label(id), rsc.widgets().label(id),
); );
@@ -408,11 +481,12 @@ impl UiRenderState {
parent_move: move_idx, parent_move: move_idx,
region_node: false, region_node: false,
mask, mask,
given_len: UiVec2::FULL_SIZE, given_region: UiRegion::FULL,
offer_len: UiVec2::FULL_SIZE, offer_len: UiVec2::FULL_SIZE,
offer_placement: [None; 2],
px, px,
offered_px: px, offered_px: px,
decided: [false; 2], placement: [None; 2],
}, },
rsc, rsc,
); );
@@ -423,14 +497,12 @@ impl UiRenderState {
let active = ActiveData { let active = ActiveData {
id, id,
region, region,
// The box a placing ask draws in is a part of the one its parent placement,
// gave, which `draw_inner` writes back over these once the given_region: info.given_region,
// placement is done.
given: region,
given_len: info.given_len,
offer_len: info.offer_len, offer_len: info.offer_len,
offer_placement: info.offer_placement,
// Whoever asked writes the answer, if this was the asking. // Whoever asked writes the answer, if this was the asking.
answer: old_answer.unwrap_or((size, holds)), answer: old_answer,
size, size,
holds, holds,
drawn: true, drawn: true,
@@ -438,10 +510,12 @@ impl UiRenderState {
depth: info.depth, depth: info.depth,
textures, textures,
primitives, primitives,
mask_region,
inherited_children,
children, children,
size_deps, size_deps,
declared: declared_lens(rsc.widgets(), id), declared: declared_lens(rsc.widgets(), id),
decided: info.decided, decided: info.decided(),
own_align: rsc.widgets().alignment(id), own_align: rsc.widgets().alignment(id),
move_idx, move_idx,
parent_move: info.parent_move, parent_move: info.parent_move,
@@ -481,18 +555,24 @@ impl UiRenderState {
&self, &self,
id: WidgetId, id: WidgetId,
px: PxVec2, px: PxVec2,
placement: [Option<UiSpan>; 2],
parent_move: MoveIdx, parent_move: MoveIdx,
widgets: &Widgets, widgets: &Widgets,
) -> Option<(Size, [Holds; 2])> { ) -> Option<(Size, LayoutHolds)> {
if widgets.needs_redraw.contains(&id) || self.dirty_size_under(id, widgets) { if widgets.needs_redraw.contains(&id) {
return None; return None;
} }
let active = self.active.get(&id)?; let active = self.active.get(&id)?;
let (size, holds) = active.answer; let (size, holds) = active.answer?;
let valid = active.drawn let valid = active.drawn
&& active.parent_move == parent_move && active.parent_move == parent_move
&& holds[0].contains(px.x) && holds.contains(
&& holds[1].contains(px.y); px,
UiRegion {
x: placement[0].unwrap_or(UiSpan::FULL),
y: placement[1].unwrap_or(UiSpan::FULL),
},
);
valid.then_some((size, holds)) valid.then_some((size, holds))
} }
@@ -500,7 +580,7 @@ impl UiRenderState {
/// drawing ended up. Alignment is exactly that case: the first box is the /// drawing ended up. Alignment is exactly that case: the first box is the
/// question and the smaller placed box holds the drawing. Whether the /// question and the smaller placed box holds the drawing. Whether the
/// answer is stale at all is its caller's question, asked once there. /// answer is stale at all is its caller's question, asked once there.
fn retained_answer(&self, id: WidgetId, info: DrawInfo) -> Option<(Size, [Holds; 2])> { fn retained_answer(&self, id: WidgetId, info: DrawInfo) -> Option<(Size, LayoutHolds)> {
let active = self.active.get(&id)?; let active = self.active.get(&id)?;
let has_region_node = active.move_idx != active.parent_move; let has_region_node = active.move_idx != active.parent_move;
if !active.drawn if !active.drawn
@@ -509,22 +589,11 @@ impl UiRenderState {
{ {
return None; return None;
} }
let (size, holds) = active.answer; let answer = active.answer?;
(holds[0].contains(info.px.x) && holds[1].contains(info.px.y)).then_some((size, holds)) answer
} .1
.contains(info.px, info.offered_placement())
/// Whether anything whose size this widget's own size was read from is .then_some(answer)
/// dirty, which makes what it would answer not yet known. It also keeps
/// a reader that asks first from laying out twice, which is all it was
/// here for while a changed size was thought to reach its reader in any
/// order; it does not, where the change settles inside the reader's own
/// draw.
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 pixel lengths of the box a widget was given and of the box it was /// The pixel lengths of the box a widget was given and of the box it was
@@ -542,14 +611,10 @@ impl UiRenderState {
// Nothing above the root: the window is where a fraction becomes // Nothing above the root: the window is where a fraction becomes
// pixels, which is also the whole of the box the root is given. // pixels, which is also the whole of the box the root is given.
let (parent_px, parent_offer) = match active.parent.and_then(|p| self.active.get(&p)) { let (parent_px, parent_offer) = match active.parent.and_then(|p| self.active.get(&p)) {
Some(parent) => { Some(parent) => self.asked_px(parent.id),
let (given, offer) = self.asked_px(parent.id);
let lens = placed_lens(parent.answer.0, parent.declared, parent.decided);
(lens.to_px(given), offer)
}
None => (self.output_size, self.output_size), None => (self.output_size, self.output_size),
}; };
let px = active.given_len.to_px(parent_px); let px = active.given_region.size().to_px(parent_px);
let mut offered = active.offer_len.to_px(parent_offer); let mut offered = active.offer_len.to_px(parent_offer);
for axis in AXES { for axis in AXES {
// A declared length is resolved by whoever drew the widget, in // A declared length is resolved by whoever drew the widget, in
@@ -568,9 +633,10 @@ impl UiRenderState {
&mut self, &mut self,
id: WidgetId, id: WidgetId,
region: UiRegion, region: UiRegion,
placement: UiRegion,
info: DrawInfo, info: DrawInfo,
rsc: &mut dyn UiRsc, rsc: &mut dyn UiRsc,
) -> Option<(Size, [Holds; 2])> { ) -> Option<(Size, LayoutHolds)> {
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::ReuseAttempts); diag::bump(Counter::ReuseAttempts);
if rsc.widgets().needs_redraw.contains(&id) { if rsc.widgets().needs_redraw.contains(&id) {
@@ -619,7 +685,7 @@ impl UiRenderState {
// In pixels, because `region` is a fraction of the box its parent // In pixels, because `region` is a fraction of the box its parent
// drew in and that box may be what changed -- an unchanged fraction // drew in and that box may be what changed -- an unchanged fraction
// of a box half the size is half the widget. // of a box half the size is half the widget.
if !active.holds_at(info.px) { if !active.holds.contains(info.px, placement) {
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
{ {
diag::bump(Counter::ReuseOutside); diag::bump(Counter::ReuseOutside);
@@ -627,23 +693,24 @@ impl UiRenderState {
} }
return None; return None;
} }
let extent_moved = active.placement != placement;
let moved = active.region != region; let moved = active.region != region;
let (answer, old_region, slot) = let (answer, slot) = ((active.size, active.holds), active.move_idx);
((active.size, active.holds), active.region, active.move_idx);
if moved { if moved {
if has_region_node { if has_region_node {
self.moves.set(slot, region); self.moves.set(slot, region);
} else { } else {
let remap = RegionRemap::new(old_region, region)?; self.recompose_subtree(id, region, info.parent_move, rsc);
self.remap_subtree(id, &remap, info.parent_move, rsc);
} }
} }
if extent_moved {
self.reposition(id, region, placement, info, rsc);
}
self.redepth(id, info.depth);
let active = self.active.get_mut(&id).unwrap(); let active = self.active.get_mut(&id).unwrap();
active.region = region; active.region = region;
active.given = region; active.given_region = info.given_region;
active.given_len = info.given_len;
active.offer_len = info.offer_len; active.offer_len = info.offer_len;
active.depth = info.depth;
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
{ {
match (moved, has_region_node) { match (moved, has_region_node) {
@@ -667,41 +734,117 @@ impl UiRenderState {
Some(answer) Some(answer)
} }
/// Re-expresses an ordinary retained subtree in a new parent region. fn reposition(
/// An independently movable descendant needs only its own region changed;
/// its contents stay in that region's coordinate space.
fn remap_subtree(
&mut self, &mut self,
id: WidgetId, id: WidgetId,
remap: &RegionRemap, region: UiRegion,
placement: UiRegion,
info: DrawInfo,
rsc: &mut dyn UiRsc,
) {
let active = self.active.get_mut(&id).unwrap();
active.region = region;
active.placement = placement;
let local = if info.region_node {
UiRegion::FULL
} else {
region
};
for primitive in &active.primitives {
let handle = &primitive.handle;
*self.layers[handle.layer].region_mut(handle) =
primitive.region.resolve(local, placement);
}
if let Some(mask_region) = active.mask_region {
rsc.ui_mut().masks.get_mut(active.mask).region = mask_region.resolve(local, placement);
}
let parent_move = active.move_idx;
let mask = active.mask;
let children = active.inherited_children.len();
for index in 0..children {
let child = self.active[&id].inherited_children[index];
let active = &self.active[&child];
let (child_local, chosen) = ask_box(
UiRegion::FULL,
active.declared,
active.own_align,
[Some(placement.x), Some(placement.y)],
);
let child_placement = UiRegion {
x: chosen[0].unwrap_or(UiSpan::FULL),
y: chosen[1].unwrap_or(UiSpan::FULL),
};
let child_info = DrawInfo {
layer: active.layer,
parent: Some(id),
depth: info.depth + 1,
parent_move,
region_node: active.move_idx != active.parent_move,
mask,
given_region: child_local,
offer_len: active.offer_len,
offer_placement: active.offer_placement,
px: child_local.size().to_px(info.px),
offered_px: active.offer_len.to_px(info.offered_px),
placement: chosen,
};
self.place(
child,
child_local.within(&local),
child_placement,
child_info,
rsc,
);
}
}
/// A reused subtree keeps its shape, so every widget in it moves by the
/// same amount -- and where the top of it did not move, none of it did,
/// which is what makes this free in the ordinary case.
fn redepth(&mut self, id: WidgetId, depth: usize) {
let Some(active) = self.active.get_mut(&id) else {
return;
};
if active.depth == depth {
return;
}
active.depth = depth;
let children = active.children.len();
for index in 0..children {
let child = self.active[&id].children[index];
self.redepth(child, depth + 1);
}
}
/// Replays the original local compositions, including their rounding order.
/// A region node terminates the walk because its contents name its slot.
fn recompose_subtree(
&mut self,
id: WidgetId,
region: UiRegion,
parent_move: MoveIdx, parent_move: MoveIdx,
rsc: &mut dyn UiRsc, rsc: &mut dyn UiRsc,
) { ) {
let active = self.active.get_mut(&id).unwrap(); let active = self.active.get_mut(&id).unwrap();
active.given = remap.apply(active.given); active.region = region;
if active.move_idx != parent_move { if active.move_idx != parent_move {
let region = remap.apply(active.region);
active.region = region;
self.moves.set(active.move_idx, region); self.moves.set(active.move_idx, region);
return; return;
} }
for handle in &active.primitives { for primitive in &active.primitives {
let region = self.layers[handle.layer].region_mut(handle); let handle = &primitive.handle;
*region = remap.apply(*region); *self.layers[handle.layer].region_mut(handle) =
primitive.region.resolve(region, active.placement);
}
if let Some(local) = active.mask_region {
rsc.ui_mut().masks.get_mut(active.mask).region =
local.resolve(region, active.placement);
} }
active.region = remap.apply(active.region);
let own_mask = (active.mask != active.parent_mask).then_some(active.mask);
let children = active.children.len(); let children = active.children.len();
// A mask the widget set itself moves with it; one it inherited
// belongs to the widget that set it, and moves there or not at all.
if let Some(idx) = own_mask {
let mask = rsc.ui_mut().masks.get_mut(idx);
debug_assert_eq!(mask.move_idx, parent_move);
mask.region = remap.apply(mask.region);
}
for index in 0..children { for index in 0..children {
let child = self.active[&id].children[index]; let child = self.active[&id].children[index];
self.remap_subtree(child, remap, parent_move, rsc); let local = self.active[&child].given_region;
self.recompose_subtree(child, local.within(&region), parent_move, rsc);
} }
} }
@@ -720,8 +863,8 @@ impl UiRenderState {
fn remove(&mut self, id: WidgetId, undraw: bool, rsc: &mut dyn UiRsc) -> Option<ActiveData> { fn remove(&mut self, id: WidgetId, undraw: bool, rsc: &mut dyn UiRsc) -> Option<ActiveData> {
let mut active = self.active.remove(&id); let mut active = self.active.remove(&id);
if let Some(active) = &mut active { if let Some(active) = &mut active {
for h in &active.primitives { for primitive in &active.primitives {
let mask = self.layers.free(h); let mask = self.layers.free(&primitive.handle);
if mask != MaskIdx::NONE { if mask != MaskIdx::NONE {
rsc.ui_mut().masks.remove(mask); rsc.ui_mut().masks.remove(mask);
} }
@@ -776,17 +919,20 @@ impl UiRenderState {
ActiveData { ActiveData {
id, id,
region: UiRegion::FULL, region: UiRegion::FULL,
given: UiRegion::FULL, placement: UiRegion::FULL,
given_len: UiVec2::FULL_SIZE, given_region: UiRegion::FULL,
offer_len: UiVec2::FULL_SIZE, offer_len: UiVec2::FULL_SIZE,
answer: (size, [Holds::ANY; 2]), offer_placement: [None; 2],
answer: None,
size, size,
holds: [Holds::ANY; 2], holds: LayoutHolds::ANY,
drawn: false, drawn: false,
parent: info.parent, parent: info.parent,
depth: info.depth, depth: info.depth,
textures: Vec::new(), textures: Vec::new(),
primitives: Vec::new(), primitives: Vec::new(),
mask_region: None,
inherited_children: Vec::new(),
children: Vec::new(), children: Vec::new(),
size_deps: Vec::new(), size_deps: Vec::new(),
move_idx: info.parent_move, move_idx: info.parent_move,
@@ -935,8 +1081,9 @@ impl UiRenderState {
pub fn window_region(&self, id: &impl IdLike) -> Option<PixelRegion> { pub fn window_region(&self, id: &impl IdLike) -> Option<PixelRegion> {
let active = self.active.get(&id.id())?; let active = self.active.get(&id.id())?;
active.drawn.then(|| { active.drawn.then(|| {
let placed = active.placement.within(&active.region);
self.moves self.moves
.resolve(active.parent_move, active.region) .resolve(active.parent_move, placed)
.to_px(self.output_size) .to_px(self.output_size)
}) })
} }
@@ -957,7 +1104,7 @@ impl UiRenderState {
let declared_changed = declared_lens(rsc.widgets(), id) != active.declared; let declared_changed = declared_lens(rsc.widgets(), id) != active.declared;
let alignment_changed = rsc.widgets().alignment(id) != active.own_align; let alignment_changed = rsc.widgets().alignment(id) != active.own_align;
if let Some(parent) = active.parent if let Some(parent) = active.parent
&& (declared_changed || alignment_changed || !active.drawn) && (declared_changed || alignment_changed || !active.drawn || active.answer.is_none())
{ {
if declared_changed { if declared_changed {
self.replace_answers = true; self.replace_answers = true;
@@ -1011,21 +1158,32 @@ impl UiRenderState {
parent_move: active.parent_move, parent_move: active.parent_move,
region_node: rsc.widgets().is_region_node(id), region_node: rsc.widgets().is_region_node(id),
mask: active.parent_mask, mask: active.parent_mask,
given_len: active.given_len, given_region: active.given_region,
offer_len: active.offer_len, offer_len: active.offer_len,
offer_placement: active.offer_placement,
px: given_px, px: given_px,
offered_px, offered_px,
decided: active.decided, // The same question its parent asked: the axes its parent chose
// the placement on, put back where they were.
placement: AXES
.map(|axis| active.decided[axis as usize].then(|| *active.placement.axis(axis))),
}; };
let (given, was_answer) = (active.given, active.answer); let (given, was_answer) = (active.region, active.answer);
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::LocalRedraws); diag::bump(Counter::LocalRedraws);
let old = self.remove(id, false, rsc); let old = self.remove(id, false, rsc);
// `draw_inner` places the answer inside that box itself, which is the // Refresh the original measurement before restoring the assigned slot.
// ask that leaves the widget where its parent put it. // Its lengths may differ even though the fraction reference is unchanged.
let answer = self.draw_inner(id, given, info, old, rsc); let offered = DrawInfo {
if answer != was_answer { placement: info.offer_placement,
..info
};
let answer = self.draw_inner(id, given, offered, old, rsc);
if info.placement != offered.placement {
self.draw_inner(id, given, info, None, rsc);
}
if Some(answer) != was_answer {
// Its parent chose its box knowing the old answer, so it lays out // Its parent chose its box knowing the old answer, so it lays out
// again and chooses the box the new one asks for. // again and chooses the box the new one asks for.
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
@@ -1048,118 +1206,6 @@ fn within_box(size: Size, px: PxVec2, axis: Axis) -> bool {
len.leftover != Weight::ZERO || box_len.mul(len.rel) + len.px <= box_len len.leftover != Weight::ZERO || box_len.mul(len.rel) + len.px <= box_len
} }
/// 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.mul(fraction);
let to_rel = scale.to_rel + scale.to_rel_span.mul(fraction);
let to_px = scale.to_px + scale.to_px_span.mul(fraction);
Len::from_parts(to_rel, scalar.px - from_px + to_px)
}
}
impl Default for UiRenderState { impl Default for UiRenderState {
fn default() -> Self { fn default() -> Self {
Self::new() Self::new()
-1
View File
@@ -26,7 +26,6 @@ impl DefaultAppState for State {
.wrap(true) .wrap(true)
.text_align(Align::LEFT) .text_align(Align::LEFT)
.pad(16) .pad(16)
.width(rel(1.0))
.background(panel()); .background(panel());
// Each one takes the whole width, because `text_align` puts the // Each one takes the whole width, because `text_align` puts the
+1 -1
View File
@@ -251,7 +251,7 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
ui_state.renderer.draw(); ui_state.renderer.draw();
} }
WindowEvent::Resized(size) => { WindowEvent::Resized(size) => {
render.resize((size.width, size.height)); render.resize((size.width, size.height), rsc.widgets_mut());
ui_state.renderer.resize(size) ui_state.renderer.resize(size)
} }
WindowEvent::KeyboardInput { event, .. } => { WindowEvent::KeyboardInput { event, .. } => {
+3 -3
View File
@@ -144,9 +144,9 @@ impl Harness {
// bound that comes with `SyncSender` is far past anything a test // bound that comes with `SyncSender` is far past anything a test
// leaves unread. // leaves unread.
let (send, updates) = sync_channel(1024); let (send, updates) = sync_channel(1024);
let rsc = DefaultRsc::init(Arc::new(Queue(send))); let mut rsc = DefaultRsc::init(Arc::new(Queue(send)));
let mut render = UiRenderState::new(); let mut render = UiRenderState::new();
render.resize(size); render.resize(size, rsc.widgets_mut());
Self { Self {
rsc, rsc,
render, render,
@@ -161,7 +161,7 @@ impl Harness {
} }
pub fn resize(&mut self, size: impl Into<Vec2>) { pub fn resize(&mut self, size: impl Into<Vec2>) {
self.render.resize(size); self.render.resize(size, self.rsc.widgets_mut());
} }
/// Changes a length rule after the fact, the way `.width()` sets one. /// Changes a length rule after the fact, the way `.width()` sets one.
+1 -1
View File
@@ -6,7 +6,7 @@ pub struct Masked {
impl Widget for Masked { impl Widget for Masked {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
painter.set_mask(painter.region()); painter.set_mask(DrawRegion::Extent(UiRegion::FULL));
painter.widget(&self.inner); painter.widget(&self.inner);
// What it occupies is its box, on both axes, for the reason `Scroll` // What it occupies is its box, on both axes, for the reason `Scroll`
// reports the same: it clips what is inside to that box, so it can // reports the same: it clips what is inside to that box, so it can
+8 -5
View File
@@ -13,9 +13,8 @@ impl Widget for Pad {
// it; where the box is bigger -- a share of a row, a rule over this // it; where the box is bigger -- a share of a row, a rule over this
// widget -- the slack is the inner's to sit in, and forcing the near // widget -- the slack is the inner's to sit in, and forcing the near
// edge pinned it to a corner it had not asked for. // edge pinned it to a corner it had not asked for.
let inner = painter let inside = self.padding.region_of(painter.placement());
.widget_within(&self.inner, self.padding.region()) let inner = painter.widget_within(&self.inner, inside).size();
.size();
Size { Size {
x: LayoutLen { x: LayoutLen {
px: inner.x.px + self.padding.left + self.padding.right, px: inner.x.px + self.padding.left + self.padding.right,
@@ -53,14 +52,18 @@ impl Padding {
bottom: amt, bottom: amt,
} }
} }
pub fn region(&self) -> UiRegion { /// `region` less this padding on each side.
let mut region = UiRegion::FULL; pub fn region_of(&self, mut region: UiRegion) -> UiRegion {
region.x.start.px += self.left; region.x.start.px += self.left;
region.y.start.px += self.top; region.y.start.px += self.top;
region.x.end.px -= self.right; region.x.end.px -= self.right;
region.y.end.px -= self.bottom; region.y.end.px -= self.bottom;
region region
} }
pub fn region(&self) -> UiRegion {
self.region_of(UiRegion::FULL)
}
pub fn x(amt: impl UiNum) -> Self { pub fn x(amt: impl UiNum) -> Self {
let amt = Px::from_num(amt); let amt = Px::from_num(amt);
Self { Self {
+11 -5
View File
@@ -15,10 +15,12 @@ impl Widget for Scroll {
// Draw in the whole container only when its scrolling-axis length is // Draw in the whole container only when its scrolling-axis length is
// not already known, then draw it at the scrolled offset. // not already known, then draw it at the scrolled offset.
let whole = UiRegion::FULL; let whole = UiRegion::FULL;
let answer_len = match painter.known_len(&self.inner, self.axis, whole, whole.size()) { let own = painter.placement();
Some(len) => len, let answer_len =
None => painter.widget(&self.inner).size().axis(self.axis), match painter.known_len(&self.inner, self.axis, whole, [Some(own.x), Some(own.y)]) {
}; Some(len) => len,
None => painter.widget(&self.inner).size().axis(self.axis),
};
let content = answer_len.apply_leftover(); let content = answer_len.apply_leftover();
self.container_len = container_len; self.container_len = container_len;
self.content_len = content.to_px(container_len); self.content_len = content.to_px(container_len);
@@ -64,7 +66,11 @@ impl Widget for Scroll {
region = region.offset(offset); region = region.offset(offset);
region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len); region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len);
} }
painter.widget_at(&self.inner, region, region.size(), [true; 2]); // The viewport is the inner's region, so a fraction it declares or
// reports is a fraction of what is on screen rather than of the
// content box its own answer decided. Where it is put is the content
// box, scrolled.
painter.widget_at(&self.inner, whole, [Some(region.x), Some(region.y)]);
// What it occupies is its box, on both axes: it clips its content to // What it occupies is its box, on both axes: it clips its content to
// that box, so it can neither take less of one nor honestly ask for // that box, so it can neither take less of one nor honestly ask for
// more. The content's length is what it scrolls through, not what it // more. The content's length is what it scrolls through, not what it
+47 -63
View File
@@ -10,25 +10,36 @@ pub struct Span {
impl Widget for Span { impl Widget for Span {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
let axis = self.dir.axis; let axis = self.dir.axis;
// The row: this span's own box, as a span of the region it was given.
// Its children are laid out along it, and what they declare or report
// is a fraction of the region -- the area this span was told it has,
// which it passes on unchanged.
let own = painter.placement();
let row = *own.axis(axis);
// Across itself the span's own box is the child's region: a span is
// what contains its children there, and nothing divides that axis.
// Along it the whole region is, so a fraction means the same thing
// for every child however much of the row is left when it is asked.
let region = UiRegion::from_axis(axis, UiSpan::FULL, *own.axis(!axis));
let along = |from: Len, to: Len| match self.dir.sign {
Sign::Pos => UiSpan::new(row.start + from, row.start + to),
Sign::Neg => UiSpan::new(row.end - to, row.end - from),
};
let far = row.len();
// A length for every child before their final boxes are chosen: from // A length for every child before their final boxes are chosen: from
// a hint where one exists, and from drawing otherwise. // a hint where one exists, and from drawing otherwise.
let mut cursor = Len::rel_min(); let mut cursor = Len::rel_min();
let mut lens = Vec::with_capacity(self.children.len()); let mut lens = Vec::with_capacity(self.children.len());
for child in &self.children { for child in &self.children {
let mut span = UiSpan::new(cursor, Len::rel_max()); // The whole region is the child's, so `rel(0.5)` is half the area
if self.dir.sign == Sign::Neg { // this span was given whatever else is in it and wherever this
span.flip(); // child sits among them. What it is placed in is the room left
} // from the cursor, because a text has to wrap at the width
let region = UiRegion::from_axis(axis, span, UiSpan::FULL); // actually there.
// Offered the room left from the cursor, because a text has to let room = axis.pair(Some(along(cursor, far)), None);
// wrap at the width actually there, but reporting a fraction of let len = match painter.known_len(child, axis, region, room) {
// the whole row: `rel(0.5)` is half the span whatever else is in
// it and wherever this child sits among them.
let len = match painter.known_len(child, axis, region, UiVec2::FULL_SIZE) {
Some(len) => len, Some(len) => len,
None => painter None => painter.widget_at(child, region, room).len(axis),
.widget_at(child, region, UiVec2::FULL_SIZE, [false; 2])
.len(axis),
}; };
cursor.px += len.px + self.gap; cursor.px += len.px + self.gap;
cursor.rel += len.rel; cursor.rel += len.rel;
@@ -46,43 +57,26 @@ impl Widget for Span {
|sum, len| sum + *len, |sum, len| sum + *len,
); );
// What is left for the shares to divide: the row less everything
// fixed, as a length of the region rather than a number of pixels.
let room = far - Len::from_parts(total.rel, total.px);
// Whether anything is left over is a question in pixels: `rel(0.5)` // Whether anything is left over is a question in pixels: `rel(0.5)`
// beside 300 px is full at 600 and overfull at 400. The room to // beside 300 px is full at 600 and overfull at 400. Asked of `room`
// divide is `len * fixed - total.px`, and the length where it runs // itself, and answered back through the same expression, so the
// out is exactly the box a parent sizing itself from this answer // boundary is the drawing's own and not a second way of finding it:
// hands back -- which is why this used to need a margin either side // the three cases a rounded division needed -- the fixed parts
// of the boundary, and why it does not now: that box and this sum are // growing slower than the box, faster, or exactly with it -- are the
// whole counts of the same step, and both routes to it land on the // sign of `room.rel`, which `through` already reads. What the
// same count. What the generated oracle checks is the consequence, // generated oracle checks is the consequence, since which children
// since which children exist at all turns on this. // exist at all turns on this.
let fixed = Rel::ONE - total.rel;
let mut shares = false; let mut shares = false;
if total.leftover > Weight::ZERO { if total.leftover > Weight::ZERO {
let current = painter.px_len(axis); shares = room.to_px(painter.region_px_len(axis)) > Px::ZERO;
let holds = if fixed > Rel::ZERO { let holds = match shares {
// The box length the fixed parts alone fill. true => Holds::from(Px::STEP..=Px::MAX),
let full = total.px.div(fixed); false => Holds::from(Px::MIN..=Px::ZERO),
shares = current > full;
match shares {
true => Holds::from(full.next_up()..=Px::MAX),
false => Holds::from(Px::MIN..=full),
}
} else if fixed < Rel::ZERO {
// The relative parts grow faster than the box does, so here
// a shorter box is the one that leaves room.
let full = total.px.div(fixed);
shares = current < full;
match shares {
true => Holds::from(Px::MIN..=full.next_down()),
false => Holds::from(full..=Px::MAX),
}
} else {
// The relative parts take exactly the box, whatever it is, so
// the only room is what negative pixels leave.
shares = total.px < Px::ZERO;
Holds::ANY
}; };
painter.holds(axis, holds); painter.region_holds(axis, holds.through(room));
} }
// Across itself a span is as long as its longest child -- unless a // Across itself a span is as long as its longest child -- unless a
@@ -99,7 +93,6 @@ impl Widget for Span {
// row. // row.
let mut fixed = Len::rel_min(); let mut fixed = Len::rel_min();
let mut taken = Weight::ZERO; let mut taken = Weight::ZERO;
let room = Len::rel_max() - Len::from_parts(total.rel, total.px);
let mut start = Len::rel_min(); let mut start = Len::rel_min();
let mut ortho = LayoutLen::ZERO; let mut ortho = LayoutLen::ZERO;
for (child, len) in self.children.iter().zip(&lens) { for (child, len) in self.children.iter().zip(&lens) {
@@ -112,28 +105,19 @@ impl Widget for Span {
fixed.px += self.gap; fixed.px += self.gap;
continue; continue;
} }
let mut span = UiSpan::FULL; let from = start;
span.start = start;
if len.leftover > Weight::ZERO && shares { if len.leftover > Weight::ZERO && shares {
taken += len.leftover; taken += len.leftover;
} }
fixed.px += len.px; fixed.px += len.px;
fixed.rel += len.rel; fixed.rel += len.rel;
start = shared(fixed, taken, total.leftover, room); start = shared(fixed, taken, total.leftover, room);
span.end = start; // Along the row the span says where the child goes; across it the
let mut region = UiRegion::from_axis(axis, span, UiSpan::FULL); // child sits where its own alignment says. Its region is the
if self.dir.sign == Sign::Neg { // whole of what this span was given either way, which is what its
region.flip(axis); // fractions are of.
} let placed =
// Along the row this box is the child's own answer, so the answer painter.widget_at(child, region, axis.pair(Some(along(from, start)), None));
// is not placed in it again; across it the child sits where its
// alignment says.
let placed = painter.widget_at(
child,
region,
UiVec2::FULL_SIZE,
[axis == Axis::X, axis == Axis::Y],
);
if shrinks { if shrinks {
let used = placed.len(!axis); let used = placed.len(!axis);
// Choosing between a fixed and a relative length from the // Choosing between a fixed and a relative length from the
+18 -23
View File
@@ -13,30 +13,24 @@ impl Widget for Stack {
StackSize::Default => None, StackSize::Default => None,
StackSize::Child(i) => Some(i), StackSize::Child(i) => Some(i),
}; };
// Every child gets the whole of this stack's box, the sizing one // This stack's own box, which is `FULL` until its answer is known.
// included, and the stack is then handed a box of the length that let placement = painter.placement();
// child asked for. Not the part of the box that length takes: the // Whichever child sizes the stack keeps the stack's whole region as
// stack's own box becomes that length, and taking the fraction of it // its own -- the stack is the length that child asked for, so taking
// again is the fraction twice -- a child asking for half of a stack // the fraction of the stack's box again would take it twice -- and is
// that is already half a row would have a quarter of the row. // put where the stack itself is put.
//
// It cannot be told apart by asking whether this box is the answer
// yet, either. A drawing has to be a function of the box alone, since
// moving the stack into the box it asked for reuses the drawing by
// scaling it, and a drawing made a fraction of one box is right in
// any other. So: fractions of this box throughout, and the move is
// the whole of the difference.
let region = UiRegion::FULL;
// Whichever child sizes the stack is asked here and not again below,
// on the layer it ends up on: a retained drawing belongs to the layer
// it was made on, so measuring it anywhere else costs a second
// drawing of it. Its box is its own answer, so the answer is not
// placed inside it again.
let size = match sizing.and_then(|i| self.children.get(i).map(|c| (i, c))) { let size = match sizing.and_then(|i| self.children.get(i).map(|c| (i, c))) {
// On the layer that child ends up on, so the ask below is a reuse
// rather than a second drawing of it somewhere else: a retained
// drawing belongs to the layer it was made on.
Some((i, child)) => { Some((i, child)) => {
painter.child_layer_at(i); painter.child_layer_at(i);
painter painter
.widget_at(child, region, region.size(), [true; 2]) .widget_at(
child,
UiRegion::FULL,
[Some(placement.x), Some(placement.y)],
)
.size() .size()
} }
None => Size::LEFTOVER, None => Size::LEFTOVER,
@@ -46,9 +40,10 @@ impl Widget for Stack {
continue; continue;
} }
painter.child_layer_at(i); painter.child_layer_at(i);
// A box that owes nothing to this child's own answer: where it // Every other child has the stack's own box for its region, since
// sits in one bigger than itself is its own business. // the stack is what contains it, and where it sits in one bigger
painter.widget_within(child, region); // than itself is its own business.
painter.widget_within(child, placement);
} }
size size
} }
+5 -15
View File
@@ -50,20 +50,11 @@ impl TextView {
let width = self.attrs.wrap.then(|| painter.px_len(Axis::X)); let width = self.attrs.wrap.then(|| painter.px_len(Axis::X));
// The shaper measures in floats, which is where a glyph advance comes // The shaper measures in floats, which is where a glyph advance comes
// from; what it answers goes back on the grid. // from; what it answers goes back on the grid.
let text = painter.render_text(&mut self.buf, &self.attrs, width.map(Px::to_f32)); painter.render_text(&mut self.buf, &self.attrs, width.map(Px::to_f32));
// A greedy break is the same break at every width from its longest if width.is_some() {
// line up to the one it was made at: each line still fits, and none painter.holds(Axis::X, self.buf.width_holds());
// could take a word that did not fit in the wider box. A line too
// long to fit at all says nothing about narrower boxes.
//
// The step at or above that longest line rather than the nearest
// one, since the shaper measures in floats: the nearest step is
// under the line half the time, and a range starting there admits a
// box the line does not fit in, where the break is not this one.
if let Some(width) = width {
painter.holds(Axis::X, Px::ceil_from_f32(text.size.x).min(width)..=width);
} }
text self.buf.rendered().expect("render_text placed the glyphs")
} }
pub fn tex(&self) -> Option<&RenderedText> { pub fn tex(&self) -> Option<&RenderedText> {
@@ -89,8 +80,7 @@ impl TextView {
// hair under that line, and the break made in it is not the break a // hair under that line, and the break made in it is not the break a
// cold layout makes there. // cold layout makes there.
let size = Size::from_px(PxVec2::ceil_from_f32(tex.size)); let size = Size::from_px(PxVec2::ceil_from_f32(tex.size));
let within = region.within(&painter.region()); painter.glyphs(tex, DrawRegion::Extent(region));
painter.glyphs(tex, within);
(region, size) (region, size)
} }
+26 -33
View File
@@ -20,11 +20,12 @@ fn a_span_gives_each_child_the_width_it_asked_for() {
assert_corners!(h, right, (100, 0), (400, 200)); assert_corners!(h, right, (100, 0), (400, 200));
} }
/// A span offers each child the room left after the one before, because a /// A span places each child in the room left after the one before, because a
/// text has to wrap at the width actually there, but reads what the child /// text has to wrap at the width actually there, but the child's region is
/// reports as a fraction of the whole row. So two children asking for half /// the whole row. So two children asking for half each take the whole row
/// each take the whole row between them, however much of it was left when /// between them, however much of it was left when each was asked, and a third
/// each was asked, and a third overflows. /// overflows -- and a span passes its own region on unchanged, so a child of
/// a nested span asking for half asks for half of the same row.
#[test] #[test]
fn a_span_reads_a_child_report_as_a_fraction_of_the_row() { fn a_span_reads_a_child_report_as_a_fraction_of_the_row() {
let mut h = Harness::new((400, 100)); let mut h = Harness::new((400, 100));
@@ -34,10 +35,10 @@ fn a_span_reads_a_child_report_as_a_fraction_of_the_row() {
let tail = rect(Color::BLUE).width(100).add(&mut h.rsc); let tail = rect(Color::BLUE).width(100).add(&mut h.rsc);
h.set_root((half, nested, tail).span(Dir::RIGHT).width(rel(1.0))); h.set_root((half, nested, tail).span(Dir::RIGHT).width(rel(1.0)));
// The nested span is placed at the length it reported and drawn there // The nested span is placed at the length it reported, and its own child
// once more; half of that final box is what its own child takes. // asks for half of the row rather than half of that placement.
assert_corners!(h, nested, (200, 0), (400, 100)); assert_corners!(h, nested, (200, 0), (400, 100));
assert_corners!(h, inner, (200, 0), (300, 100)); assert_corners!(h, inner, (200, 0), (400, 100));
assert_corners!(h, tail, (400, 0), (500, 100)); assert_corners!(h, tail, (400, 0), (500, 100));
} }
@@ -82,31 +83,6 @@ fn a_text_in_a_span_wraps_at_the_room_left_rather_than_the_whole_row() {
assert!(crowded > whole_row, "{crowded} against {whole_row}"); assert!(crowded > whole_row, "{crowded} against {whole_row}");
} }
/// A stack takes its size from one child and gives every child that size, so
/// a child asking for half of it is asking for half of what it is itself the
/// size of. Once the stack has been placed at the length it reported that
/// length is the box, and taking the fraction of it again takes it twice:
/// half a row became a quarter, and a further stack around it a further half.
/// Nothing pinned it because a pixel is the same length wherever it is taken
/// from, so only a share ever shrank -- and warm and cold shrink alike, so no
/// oracle saw it either.
#[test]
fn a_stack_sized_by_a_child_does_not_take_that_childs_fraction_twice() {
let mut h = Harness::new((400, 200));
let half = rect(Color::RED).width(rel(0.5)).add(&mut h.rsc);
let behind = rect(Color::BLUE).add(&mut h.rsc);
let stack = Stack {
children: vec![behind.add_strong(&mut h.rsc), half.add_strong(&mut h.rsc)],
size: StackSize::Child(1),
}
.add(&mut h.rsc);
h.set_root((stack,).span(Dir::RIGHT).width(rel(1.0)));
assert_corners!(h, stack, (0, 0), (200, 200));
assert_corners!(h, half, (0, 0), (200, 200));
assert_corners!(h, behind, (0, 0), (200, 200));
}
/// The same reading through a pad: its inset is the whole box less the /// The same reading through a pad: its inset is the whole box less the
/// padding, so half of the inset plus the padding is half the box plus one /// padding, so half of the inset plus the padding is half the box plus one
/// padding, not two. /// padding, not two.
@@ -723,3 +699,20 @@ fn equal_shares_differ_by_at_most_two_steps_and_fill_the_row() {
} }
} }
} }
#[test]
fn a_stack_sized_by_a_child_does_not_take_that_childs_fraction_twice() {
let mut h = Harness::new((400, 200));
let half = rect(Color::RED).width(rel(0.5)).add(&mut h.rsc);
let behind = rect(Color::BLUE).add(&mut h.rsc);
let stack = Stack {
children: vec![behind.add_strong(&mut h.rsc), half.add_strong(&mut h.rsc)],
size: StackSize::Child(1),
}
.add(&mut h.rsc);
h.set_root((stack,).span(Dir::RIGHT).width(rel(1.0)));
assert_corners!(h, stack, (0, 0), (200, 200));
assert_corners!(h, half, (0, 0), (200, 200));
assert_corners!(h, behind, (0, 0), (200, 200));
}
+296 -10
View File
@@ -156,16 +156,9 @@ fn a_span_child_that_declares_its_length_is_drawn_once() {
h.set_root((hinted, asked).span(Dir::RIGHT)); h.set_root((hinted, asked).span(Dir::RIGHT));
assert_eq!(told_draws.get(), 1); assert_eq!(told_draws.get(), 1);
// Reading its box makes its drawing hold for the measuring box alone, // Only the available length changes: positioning the final slot does
// and it reports less than that box: so it is drawn again in the box its // not invalidate a numeric size read.
// answer places it in, and once more in the final box the span chooses. assert_eq!(asked_draws.get(), 2);
// A widget that says what it holds for, as text does, skips the middle
// one.
assert_eq!(
asked_draws.get(),
3,
"drawn to be measured, in its placed box, then in its final box"
);
} }
#[test] #[test]
@@ -628,3 +621,296 @@ fn a_masked_widget_redrawn_on_its_own_sets_its_mask_again() {
h.frame(); h.frame();
assert_corners!(h, inner, (100, 0), (400, 200)); assert_corners!(h, inner, (100, 0), (400, 200));
} }
/// The two spans a subtree changes hands between, and the branch that is not
/// in the tree yet -- kept alive by the test until it is.
struct Handover {
leaf: WidgetId,
first: WeakWidget<Span>,
second: WeakWidget<Span>,
root: WeakWidget<Span>,
spare: StrongWidget,
}
/// A subtree that changes hands while its box does not move, so nothing about
/// reusing its drawing says it changed parents. `deeper` puts a span between
/// the root and `second`, so it changes depth by changing hands as well.
fn plant_handover(h: &mut Harness, moved: bool, deeper: bool, width: f32) -> Handover {
let leaf = rect(Color::RED).add(&mut h.rsc);
let sized = leaf.width(width).add(&mut h.rsc);
let holder = (sized,).span(Dir::RIGHT).add(&mut h.rsc);
let first = Span {
children: match moved {
true => Vec::new(),
false => vec![holder.add_strong(&mut h.rsc)],
},
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.add(&mut h.rsc);
let second = Span {
children: match moved {
true => vec![holder.add_strong(&mut h.rsc)],
false => Vec::new(),
},
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.add(&mut h.rsc);
let branch = match deeper {
true => (second,).span(Dir::RIGHT).add_strong(&mut h.rsc),
false => second.add_strong(&mut h.rsc),
};
let (in_tree, spare) = match moved {
true => (branch, first.add_strong(&mut h.rsc)),
false => (first.add_strong(&mut h.rsc), branch),
};
let root = Span {
children: vec![in_tree],
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.add(&mut h.rsc);
h.state.root = Some(root.add_strong(&mut h.rsc));
Handover {
leaf: sized.id(),
first,
second,
root,
spare,
}
}
/// Moves the subtree and swaps the branch it sits in for the one it left.
fn hand_over(h: &mut Harness, tree: Handover) -> WidgetId {
let holder = h.rsc[tree.first].children.remove(0);
h.rsc[tree.second].children.push(holder);
h.rsc[tree.root].children.clear();
h.rsc[tree.root].children.push(tree.spare);
h.frame();
tree.leaf
}
#[test]
fn a_subtree_that_changed_parents_is_not_undrawn_by_the_one_it_left() {
let mut warm = Harness::new((400, 200));
let tree = plant_handover(&mut warm, false, false, 40.0);
warm.frame();
let leaf = hand_over(&mut warm, tree);
let mut cold = Harness::new((400, 200));
let grown = plant_handover(&mut cold, true, false, 40.0);
cold.frame();
assert_eq!(
warm.region(&leaf),
cold.region(&grown.leaf),
"the span it left still listed it and undrew it"
);
}
#[test]
fn a_subtree_that_changed_parents_settles_at_the_depth_it_moved_to() {
let mut warm = Harness::new((400, 200));
let tree = plant_handover(&mut warm, false, true, 40.0);
warm.frame();
let leaf = hand_over(&mut warm, tree);
// After it has changed hands, so what has to reach the new parent is a
// change made under the subtree it now holds.
warm.set_len(leaf, Axis::X, LayoutLen::px(90.0));
warm.frame();
let mut cold = Harness::new((400, 200));
let grown = plant_handover(&mut cold, true, true, 90.0);
cold.frame();
assert_eq!(
warm.region(&leaf),
cold.region(&grown.leaf),
"the span it moved to is the one the change has to reach"
);
}
fn primitive_bounds(h: &Harness, id: WidgetId) -> Vec<PixelRegion> {
h.render.active[&id]
.primitives
.iter()
.map(|primitive| {
let handle = &primitive.handle;
let instance = &h.render.layers[handle.layer].primitives()[handle.kind as usize]
.as_ref()
.unwrap()
.instances()[handle.inst_idx];
h.render
.moves
.resolve(instance.move_idx, instance.region)
.to_px(h.render.output_size())
})
.collect()
}
#[test]
fn frame_geometry_and_extent_geometry_keep_their_references() {
struct Both(Rc<Cell<usize>>);
impl Widget for Both {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.0.set(self.0.get() + 1);
painter.primitive_within(RectPrimitive::color(Color::RED), UiRegion::FULL);
painter.primitive(RectPrimitive::color(Color::BLUE));
Size::LEFTOVER
}
}
for node in [false, true] {
let mut h = Harness::new((400, 200));
let first = rect(Color::GREEN).width(100).add(&mut h.rsc);
let draws = Rc::new(Cell::new(0));
let both = Both(draws.clone()).add(&mut h.rsc);
h.rsc.widgets_mut().set_region_node(both, node);
h.set_root((first, both).span(Dir::RIGHT));
let count = draws.get();
h.set_len(first, Axis::X, 200);
h.frame();
assert_eq!(draws.get(), count);
let bounds = primitive_bounds(&h, both.id());
assert_eq!(bounds[0].top_left.x, Px::ZERO);
assert_eq!(bounds[0].bot_right.x, Px::from_int(400));
assert_eq!(bounds[1].top_left.x, Px::from_int(200));
assert_eq!(bounds[1].bot_right.x, Px::from_int(400));
}
}
#[test]
fn changing_an_inherited_extent_keeps_the_original_measurement_offer() {
fn build(h: &mut Harness, width: i32, text: &str) -> (WeakWidget<Text>, WeakWidget<Rect>) {
let first = rect(Color::RED).width(width).add(&mut h.rsc);
let words = wtext(text).size(20).wrap(true).add(&mut h.rsc);
let through = Stretchy {
inner: words.add_strong(&mut h.rsc),
draws: Rc::new(Cell::new(0)),
}
.add(&mut h.rsc);
h.set_root((first, through).span(Dir::RIGHT));
(words, first)
}
let short = "one two";
let long = "one two three four five six seven eight nine ten eleven twelve";
let mut warm = Harness::new((400, 200));
let (words, first) = build(&mut warm, 50, short);
warm.set_len(first, Axis::X, 200);
warm.frame();
*warm.rsc[words].content = long.to_string();
warm.frame();
let mut cold = Harness::new((400, 200));
let (other, _) = build(&mut cold, 200, long);
assert_eq!(warm.region(&words), cold.region(&other));
assert_eq!(
primitive_bounds(&warm, words.id()),
primitive_bounds(&cold, other.id())
);
}
#[test]
fn widening_text_without_soft_breaks_reuses_its_drawing() {
struct CountedText {
text: Text,
draws: Rc<Cell<usize>>,
}
impl Widget for CountedText {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
self.text.draw(painter)
}
}
for content in ["Short text", "Two hard\nline breaks\nhere", ""] {
let plant = |h: &mut Harness| {
let mut text = Text::new(content);
text.wrap = true;
let draws = Rc::new(Cell::new(0));
let root = CountedText {
text,
draws: draws.clone(),
}
.add(&mut h.rsc);
h.set_root(root);
(root, draws)
};
let mut warm = Harness::new((300, 200));
let (root, draws) = plant(&mut warm);
let before = draws.get();
warm.resize((500, 200));
warm.frame();
assert_eq!(draws.get(), before, "{content:?}");
let mut cold = Harness::new((500, 200));
let (other, _) = plant(&mut cold);
assert_eq!(warm.region(&root), cold.region(&other));
assert_eq!(
primitive_bounds(&warm, root.id()),
primitive_bounds(&cold, other.id())
);
}
}
#[test]
fn resizing_a_fixed_frame_recomposes_its_contents_without_drawing_them() {
struct Frame {
child: StrongWidget,
region: UiRegion,
}
impl Widget for Frame {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget_within(&self.child, self.region);
Size::LEFTOVER
}
}
struct Painted(Rc<Cell<usize>>);
impl Widget for Painted {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.0.set(self.0.get() + 1);
painter.set_mask(DrawRegion::Extent(UiRegion::FULL));
painter.primitive(RectPrimitive::color(Color::BLUE));
Size::LEFTOVER
}
}
let fixed = |start, end| UiRegion::new(UiSpan::new(Len::px(start), Len::px(end)), UiSpan::FULL);
for node in [false, true] {
let plant = |h: &mut Harness, region| {
let draws = Rc::new(Cell::new(0));
let leaf = Painted(draws.clone()).add(&mut h.rsc);
h.rsc.widgets_mut().set_region_node(leaf, node);
let inner = Frame {
child: leaf.add_strong(&mut h.rsc),
region: UiRegion::new(UiSpan::new(Len::rel(0.23), Len::rel(0.83)), UiSpan::FULL),
}
.add_strong(&mut h.rsc);
let root = Frame {
child: inner,
region,
}
.add(&mut h.rsc);
h.set_root(root);
(root, leaf, draws)
};
let mut warm = Harness::new((400, 200));
let (root, leaf, draws) = plant(&mut warm, fixed(7.0, 104.0));
let before = draws.get();
warm.rsc[root].region = fixed(19.0, 180.0);
warm.frame();
assert_eq!(draws.get(), before);
let mut cold = Harness::new((400, 200));
let (_, other, _) = plant(&mut cold, fixed(19.0, 180.0));
assert_eq!(warm.region(&leaf), cold.region(&other));
assert_eq!(
primitive_bounds(&warm, leaf.id()),
primitive_bounds(&cold, other.id())
);
let mask = |h: &Harness, id: WidgetId| {
let active = &h.render.active[&id];
let mask = &h.rsc.ui().masks[active.mask.idx()];
h.render
.moves
.resolve(mask.move_idx, mask.region)
.to_px(h.render.output_size())
};
assert_eq!(mask(&warm, leaf.id()), mask(&cold, other.id()));
}
}
+58
View File
@@ -614,3 +614,61 @@ fn a_text_is_given_back_a_box_the_line_it_measured_fits_in() {
assert_eq!(warm.region(&text), cold.region(&cold_text)); assert_eq!(warm.region(&text), cold.region(&cold_text));
} }
#[test]
fn adding_text_to_a_reverse_row_keeps_its_shared_height() {
fn build(
h: &mut Harness,
changed: bool,
) -> (WeakWidget<Span>, WeakWidget<Text>, Vec<StrongWidget>) {
let wrap = wtext("Wrapping shapes one source into as many lines as the box leaves room for, so a paragraph's height is an answer and not a setting.").size(16).wrap(true).add_strong(&mut h.rsc);
let one = || {
wtext("one line, overflowing whatever it is given")
.size(16)
.wrap(false)
};
let plain = one().add_strong(&mut h.rsc);
let shared = one()
.width(LayoutLen::LEFTOVER)
.height(LayoutLen::LEFTOVER)
.add(&mut h.rsc);
let mut extra: Vec<StrongWidget> = vec![
rect(Color::RED).add_strong(&mut h.rsc),
one().add_strong(&mut h.rsc),
one().add_strong(&mut h.rsc),
];
let children: Vec<StrongWidget> = if changed {
let mut children: Vec<StrongWidget> = vec![plain, shared.add_strong(&mut h.rsc)];
children.append(&mut extra);
children
} else {
vec![wrap, plain, shared.add_strong(&mut h.rsc)]
};
let row = Span {
children,
dir: Dir::LEFT,
gap: Px::ZERO,
}
.height(LayoutLen::rel(1.0))
.add(&mut h.rsc);
let fill: StrongWidget = rect(Color::BLUE).add_strong(&mut h.rsc);
let children: Vec<StrongWidget> = vec![fill, row.add_strong(&mut h.rsc)];
let root = Span {
children,
dir: Dir::RIGHT,
gap: Px::from_int(4),
}
.height(LayoutLen::rel(1.0))
.add(&mut h.rsc);
h.set_root(root);
(row, shared, extra)
}
let mut warm = Harness::new((900, 1200));
let (row, shared, extra) = build(&mut warm, false);
warm.rsc[row].children.remove(0);
warm.rsc[row].children.extend(extra);
warm.frame();
let mut cold = Harness::new((900, 1200));
let (_, other, _) = build(&mut cold, true);
assert_eq!(warm.region(&shared), cold.region(&other));
}
+1 -30
View File
@@ -42,21 +42,6 @@ const OUTER: (f32, f32) = (1920.0, 1200.0);
const INNER: (f32, f32) = (640.0, 900.0); const INNER: (f32, f32) = (640.0, 900.0);
const STILL: (f32, f32) = (900.0, 1200.0); const STILL: (f32, f32) = (900.0, 1200.0);
/// The same box, to two steps of the grid between the two ways of reaching
/// it. A move, a repaint, a row of shares and every length in pixels land on
/// the same number. What needs the slack is a position: a box centred in a
/// fraction of its parent against the same box centred in its own pixels,
/// and a box re-expressed as a fraction of a parent that changed length.
/// A step is a thousandth of a pixel, where this was a twentieth of one
/// before any of it was on a grid.
///
/// **One step is not enough**, tried 2026-09-17 once a length in pixels
/// stopped being composed: it passes the 100-seed oracle and fails the
/// 400-seed shrinker on `resize-size`, seeds 384 and 162, by 0.002 px. So
/// what is left here is the resize path's own rounding rather than a length
/// reached two ways.
const AGREE_STEPS: i32 = 2;
/// A way of changing what a span holds. Each is a shape worth its own case: /// A way of changing what a span holds. Each is a shape worth its own case:
/// taking a child out of the middle is not the same as emptying a span, and /// taking a child out of the middle is not the same as emptying a span, and
/// adding one is not the same as adding three. /// adding one is not the same as adding three.
@@ -399,20 +384,6 @@ fn describe_widget(id: WidgetId, h: &Harness) -> String {
label label
} }
fn same_region(got: Option<PixelRegion>, want: Option<PixelRegion>) -> bool {
match (got, want) {
(Some(got), Some(want)) => {
let same = |a: Px, b: Px| (a - b).abs() <= Px::STEP.mul_int(AGREE_STEPS);
same(got.top_left.x, want.top_left.x)
&& same(got.top_left.y, want.top_left.y)
&& same(got.bot_right.x, want.bot_right.x)
&& same(got.bot_right.y, want.bot_right.y)
}
(None, None) => true,
_ => false,
}
}
/// Runs `case` on the tree `plan` describes, warm and cold, and says where /// Runs `case` on the tree `plan` describes, warm and cold, and says where
/// the two disagree. `seed` chooses only the values a case picks at random, /// the two disagree. `seed` chooses only the values a case picks at random,
/// so one plan under one case is one comparison however it was reached. /// so one plan under one case is one comparison however it was reached.
@@ -439,7 +410,7 @@ pub fn diverges(plan: &Plan, case: Case, seed: u64) -> Option<String> {
for (i, (&w, &c)) in tree.ids.iter().zip(&cold_tree.ids).enumerate() { for (i, (&w, &c)) in tree.ids.iter().zip(&cold_tree.ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c)); let (got, want) = (warm.region(&w), cold.region(&c));
drawn += got.is_some() as usize; drawn += got.is_some() as usize;
if same_region(got, want) { if got == want {
continue; continue;
} }
// Where two trees disagree is rarely where the cause is, so the // Where two trees disagree is rarely where the cause is, so the