Compare commits

...
Author SHA1 Message Date
iris-ai 7601aa2a5d Measure container children without intermediate placement 2026-09-17 15:53:24 -04:00
iris-ai c330ecec2b Skip inverse arithmetic for unrestricted layout validity 2026-09-17 15:53:24 -04:00
iris-ai 39f7b08c6c Honor fixed child alignment inside parent-selected slots 2026-09-17 15:53:24 -04:00
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
23 changed files with 1248 additions and 584 deletions

No files matched your search

+11
View File
@@ -7,6 +7,17 @@ pub enum Axis {
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 {
type Output = Self;
+17
View File
@@ -176,6 +176,23 @@ impl TextBuffer {
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 {
Vec2::new(self.layout.width(), self.layout.height())
}
+2 -1
View File
@@ -106,7 +106,8 @@ impl UiRenderNode {
self.active.push(i);
for change in draws.apply_free() {
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 {
h.inst_idx = change.new;
break;
+32 -20
View File
@@ -1,6 +1,6 @@
use crate::{
Holds, LayerId, LayoutLen, MaskIdx, MoveIdx, PrimitiveHandle, RegionAlign, Size, TextureHandle,
UiRegion, UiVec2, WidgetId,
DrawRegion, LayerId, LayoutHolds, LayoutLen, MaskIdx, MoveIdx, RegionAlign, RetainedPrimitive,
Size, TextureHandle, UiRegion, UiVec2, WidgetId,
};
/// What is kept of a widget its parent has asked about. `drawn` says whether
@@ -9,29 +9,29 @@ use crate::{
#[derive(Debug)]
pub struct ActiveData {
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,
/// The box its parent gave it, in the same coordinates: what it was
/// asked about, before its own answer placed its drawing inside it.
/// `region` is that placement, and a local redraw asks here.
pub given: UiRegion,
/// The same box as lengths of its parent's box, which is the one route
/// 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,
/// Where its drawing sits inside that box, in the box's own coordinates.
pub placement: UiRegion,
/// The original frame in its parent widget's coordinates. Recomposition
/// and pixel-length evaluation both follow this chain.
pub given_region: UiRegion,
/// 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
/// 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
/// why a region node between two widgets cannot break it.
pub offer_len: UiVec2,
/// What it answered there: the size and what that held for.
pub answer: (Size, [Holds; 2]),
pub offer_placement: [Option<crate::UiSpan>; 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.
pub size: Size,
/// The pixel lengths of `region`, per axis, that its drawing and `size`
/// hold for.
pub holds: [Holds; 2],
/// The frame, extent and explicit placement reads that this drawing holds for.
pub holds: LayoutHolds,
pub drawn: bool,
pub parent: Option<WidgetId>,
/// 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.
pub depth: usize,
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>,
/// The children whose size this widget read while drawing.
pub size_deps: Vec<WidgetId>,
@@ -70,8 +72,18 @@ pub struct ActiveData {
}
impl ActiveData {
/// Whether its drawing and size hold for a box of these pixel lengths.
pub fn holds_at(&self, px: crate::PxVec2) -> bool {
self.holds[0].contains(px.x) && self.holds[1].contains(px.y)
/// Whether what it answered still stands for a box of these pixel
/// lengths -- the box it was asked in, where `holds` is about the box its
/// 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,
}
+13
View File
@@ -52,6 +52,9 @@ impl Holds {
/// allowance: inverting it is two divisions and nothing else, and the
/// whole of a box maps back to itself.
pub const fn through(self, len: Len) -> Self {
if self.lo.raw() == Px::MIN.raw() && self.hi.raw() == Px::MAX.raw() {
return Self::ANY;
}
let rel = len.rel.raw() as i64;
if rel == 0 {
return Self::ANY;
@@ -92,6 +95,16 @@ mod tests {
use super::*;
use crate::Rel;
#[test]
fn an_unrestricted_range_stays_unrestricted_through_any_length() {
for rel in [-2.0, -0.5, 0.0, 0.5, 1.0, 2.0] {
for px in [-8, 0, 8] {
let len = Len::from_parts(Rel::from_f32(rel), Px::from_int(px));
assert_eq!(Holds::ANY.through(len), Holds::ANY);
}
}
}
#[test]
fn through_reverses_a_range_for_a_negative_fraction() {
// `10 - box / 2` is between 20 and 40 for boxes from -60 to -20.
+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;
mod active;
mod draw_region;
mod holds;
mod layout_holds;
mod painter;
mod render_state;
pub use active::*;
pub use draw_region::*;
pub use holds::*;
pub use layout_holds::*;
pub use painter::{Painter, PrimitiveLike};
pub use render_state::*;
+236 -127
View File
@@ -1,12 +1,12 @@
#[cfg(feature = "layout-diagnostics")]
use crate::layout_diagnostics::{self as diag, Counter};
use crate::{
Axis, Holds, LayoutLen, Len, Px, PxVec2, RegionAlign, RenderedText, Size, StrongWidget,
TextAttrs, TextBuffer, TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiVec2, Weight,
WidgetId, Widgets,
Axis, DrawRegion, Holds, LayoutLen, Len, Px, PxVec2, RegionAlign, RenderedText,
RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle,
UiRegion, UiRenderState, UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets,
render::{
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveHandle, PrimitiveInst,
PrimitiveKind, TexturePrimitive,
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveInst, PrimitiveKind,
TexturePrimitive,
},
ui::render_state::DrawInfo,
};
@@ -17,8 +17,20 @@ pub struct Painter<'a> {
pub(super) state: &'a mut UiRenderState,
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,
/// 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
/// 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
@@ -26,7 +38,11 @@ pub struct Painter<'a> {
pub(super) px: PxVec2,
pub(super) mask: MaskIdx,
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>,
/// The children asked about so far, so the first box each was asked in
/// is the one recorded as its offer.
@@ -57,13 +73,13 @@ pub struct 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>();
self.write(kind, primitive, region);
}
/// 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")]
diag::bump(Counter::PrimitiveWrites);
let h = self.state.layers.write(
@@ -72,15 +88,15 @@ impl<'a> Painter<'a> {
kind,
id: self.id,
primitive,
region,
region: region.resolve(self.region, self.placement),
mask_idx: self.mask,
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 {
// TODO: I have no clue if this works at all :joy:
self.rsc.ui_mut().masks.push_ref(self.mask);
@@ -88,28 +104,42 @@ impl<'a> Painter<'a> {
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) {
let at = DrawRegion::Extent(UiRegion::FULL);
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);
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);
self.mask = self.rsc.ui_mut().masks.push(Mask {
region,
region: region.resolve(self.region, self.placement),
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> {
self.widget_within(id, UiRegion::FULL)
let own = self.placement;
self.widget_at_inner(id, UiRegion::FULL, [Some(own.x), Some(own.y)], true, false)
}
/// What a widget's rules declare its lengths to be, which whoever draws
@@ -125,55 +155,64 @@ impl<'a> Painter<'a> {
/// this frame; what it answered is still something this widget asked.
pub fn undraw<W: ?Sized>(&mut self, id: &StrongWidget<W>) {
self.children.retain(|child| *child != id.id());
self.inherited_children.retain(|child| *child != id.id());
self.state.undraw_rec(id.id(), self.rsc);
}
/// 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
/// taken from it. Where the child's drawing sits inside what it is given
/// is the child's alignment, applied where the child is drawn, so a
/// container positions a child either by handing it a box of exactly its
/// length or by leaving it room and letting its alignment decide.
/// own region, and is the child's own region: what its declared lengths
/// and its report are fractions of. Where its drawing sits inside that is
/// its own answer placed by its alignment.
pub fn widget_within<'s, W: ?Sized>(
&'s mut self,
id: &'s StrongWidget<W>,
region: UiRegion,
) -> 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
/// lengths of this widget's own box. It is the box the child was given
/// wherever that box is the child's whole area -- a pad's inset, a stack
/// child, a scroll's content -- and a span passes its own extent along
/// the row instead: it offers each child the room left from its cursor,
/// 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.
/// `region` is the child's own area: what a fraction it declares or
/// reports is a fraction of, and the coordinates the regions it writes
/// compose within. It is the same box on the ask that measures and the
/// ask that places, which is what stops a fraction under it being
/// resolved twice.
///
/// A `decided` axis is one where this box was chosen from the widget's
/// own answer. On those the answer is not placed inside the box again: it
/// already is the box, and a fraction taken of it a second time would
/// shrink it twice. A container uses that where it hands back exactly
/// what a child asked for -- a span placing a child at the length it
/// reported, a scroll giving its content the content's own length.
/// `placement` is what of that region the child's drawing takes, per
/// axis, wherever this widget is choosing. `None` leaves the axis to the
/// child's own answer and alignment, which is what
/// [`Self::widget_within`] passes. A span passes the whole row as the
/// region, so `rel(0.5)` is half the row wherever the child sits in it,
/// and places the child by passing the slot along its axis.
pub fn widget_at<'s, W: ?Sized>(
&'s mut self,
id: &'s StrongWidget<W>,
region: UiRegion,
reports_of: UiVec2,
decided: [bool; 2],
placement: [Option<UiSpan>; 2],
) -> DrawResult<'s, 'a, W> {
self.widget_at_inner(id, region, placement, false, false)
}
fn widget_at_inner<'s, W: ?Sized>(
&'s mut self,
id: &'s StrongWidget<W>,
region: UiRegion,
placement: [Option<UiSpan>; 2],
inherited: bool,
measuring: 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 declared = self.declared_lens(id);
let align = self.rsc.widgets().alignment(id.id());
// Composing `FULL` through a box is not quite the identity in f32,
// so a child with nothing declared keeps the box it would have had.
let local = match declared.iter().any(Option::is_some) {
true => declared_box(region, declared, align),
false => region,
};
let (local, placement) = ask_box(region, declared, align, placement);
let within = match local == UiRegion::FULL {
true => self.region,
false => local.within(&self.region),
@@ -197,11 +236,16 @@ impl<'a> Painter<'a> {
.get(&id.id())
.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 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
// 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.
@@ -215,27 +259,39 @@ impl<'a> Painter<'a> {
parent_move: self.move_idx,
region_node,
mask: self.mask,
given_len,
given_region: local,
offer_len,
offer_placement,
px,
offered_px,
decided,
placement,
},
None,
measuring,
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
// that give the child a length inside it.
for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) {
*under = under.and(holds[axis as usize].through(local.axis(axis).len()));
for axis in AXES {
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 {
child: id,
painter: self,
size: in_parent_frame(size, reports_of, declared),
size: in_parent_frame(size, local.size(), declared),
}
}
@@ -267,42 +323,52 @@ impl<'a> Painter<'a> {
}
}
/// A child's length in the box it is about to be offered, if it can be
/// had without drawing it: from its hint, or from a drawing it already
/// has that holds for that box. `reports_of` is what a fraction in the
/// answer is a fraction of, as it is for [`Self::widget_at`].
pub fn known_len<W: ?Sized>(
/// Measures a child's length from its hint, a retained answer, or `draw`.
/// A fresh draw evaluates the offer without placing its answer. The caller
/// must later place or undraw the child.
pub fn measure_len<W: ?Sized>(
&mut self,
child: &StrongWidget<W>,
axis: Axis,
region: UiRegion,
reports_of: UiVec2,
) -> Option<LayoutLen> {
placement: [Option<UiSpan>; 2],
) -> LayoutLen {
let offered = placement;
let declared = self.declared_lens(child);
let align = self.rsc.widgets().alignment(child.id());
let local = declared_box(region, declared, align);
let first_ask = self.offer(child.id());
if first_ask && let Some(active) = self.state.active.get_mut(&child.id()) {
active.offer_len = local.size();
}
let (local, placement) = ask_box(region, declared, align, placement);
let first_ask = self.at_offer && !self.offered.contains(&child.id());
if let Some(hint) = self.size_hint(child, axis) {
return Some(hint);
return hint;
}
let px = local.size().to_px(self.px);
let (size, holds) =
let retained =
self.state
.retained_size(child.id(), px, self.move_idx, self.rsc.widgets())?;
.retained_size(child.id(), px, placement, self.move_idx, self.rsc.widgets());
let Some((size, holds)) = retained else {
return self
.widget_at_inner(child, region, offered, false, true)
.len(axis);
};
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::RetainedSizeHits);
self.depend_on(child);
if first_ask {
self.offered.push(child.id());
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()) {
*under = under.and(holds[axis as usize].through(local.axis(axis).len()));
}
Some(in_parent_frame(size, reports_of, declared).axis(axis))
in_parent_frame(size, local.size(), declared).axis(axis)
}
/// Whether this is the first box a child is asked about in during a draw
@@ -334,20 +400,24 @@ impl<'a> Painter<'a> {
ui.text.render(buffer, attrs, width)
}
/// Writes glyphs in the selected frame or extent coordinates.
// 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>();
for glyph in text.glyphs.iter() {
let mut region = origin;
region.x.end = region.x.start;
region.y.end = region.y.start;
let mut region = region.offset(UiVec2::from_px(glyph.offset));
let size = PxVec2::new(
Px::from_int(glyph.entry.width as i32),
Px::from_int(glyph.entry.height as i32),
);
region.x.end = region.x.start.offset(size.x);
region.y.end = region.y.start.offset(size.y);
let region = origin.map(|mut region| {
region.x.end = region.x.start;
region.y.end = region.y.start;
let mut region = region.offset(UiVec2::from_px(glyph.offset));
let size = PxVec2::new(
Px::from_int(glyph.entry.width as i32),
Px::from_int(glyph.entry.height as i32),
);
region.x.end = region.x.start.offset(size.x);
region.y.end = region.y.start.offset(size.y);
region
});
self.write(
kind,
GlyphPrimitive {
@@ -362,12 +432,26 @@ impl<'a> Painter<'a> {
}
}
/// This widget's box, in the coordinates its own primitives are written
/// in -- so a region composed `within` it may be drawn directly.
/// The box this widget's parent gave it, in the coordinates its own
/// 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 {
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
/// widget that positions its own content reads it to place that content
/// the way the box around it would have placed the widget.
@@ -401,20 +485,44 @@ impl<'a> Painter<'a> {
placed_box(UiRegion::FULL, lens, RegionAlign::NEAR)
}
/// This widget's box in pixels. Reading it makes the drawing one that
/// holds for this box only, until `holds` says how far it goes.
/// 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 {
for (own, len) in self.own.iter_mut().zip([self.px.x, self.px.y]) {
if *own == Holds::ANY {
*own = Holds::at(len);
}
}
self.px
PxVec2::new(self.px_len(Axis::X), self.px_len(Axis::Y))
}
/// One axis of this widget's box in pixels. Prefer this to
/// One axis of this widget's own box in pixels. Prefer this to
/// [`Self::px_size`] when the other axis cannot affect the drawing.
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 own = &mut self.own[axis as usize];
if *own == Holds::ANY {
@@ -423,15 +531,14 @@ impl<'a> Painter<'a> {
len
}
/// The lengths of this widget's box on `axis` that what it is drawing
/// holds for -- the same primitives, in the same fractions and offsets
/// of the box, and the same reported size. A widget that read its
/// length in pixels holds for that one alone until it says otherwise.
pub fn holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
/// [`Self::holds`] stated about the region rather than about this
/// widget's own box, for a container whose drawing turns on the box it
/// was given rather than on the part of it it took.
pub fn region_holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
let holds = holds.into();
debug_assert!(
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.id
);
@@ -521,19 +628,16 @@ impl PrimitiveLike for &TextureHandle {
}
}
/// A child's answer as lengths of the parent's own box. A widget reports a
/// fraction, and `reports_of` is the length that fraction is of: the box the
/// child was given wherever that is the child's whole area, and the parent's
/// own extent wherever the box is a positional remainder, as a span's is
/// after an earlier child. Pixels come through untouched either way, being
/// that many pixels wherever they end up. A declared axis is already the
/// 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 {
/// A child's answer as lengths of the parent's own region. A widget reports
/// a fraction of its own region, and `of` is that region as a length of this
/// one. Pixels come through untouched, being that many pixels wherever they
/// end up. A declared axis is already the parent's: it resolved the rule in
/// its own region, and the rule is what the report says.
fn in_parent_frame(size: Size, of: UiVec2, declared: [Option<LayoutLen>; 2]) -> Size {
let mut size = size;
for (axis, declared) in AXES.into_iter().zip(declared) {
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
@@ -562,9 +666,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
/// 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
/// that handed down this box. A declared axis does too: `declared_box`
/// already placed it, in the parent's box, and the rule's length is what the
/// widget reports there. And an axis the parent decided from the answer is
/// that handed down this box. A declared axis does too: the rule already gave
/// the region its length, and the rule's length is what the widget reports
/// there. And an axis the parent decided from the answer is
/// the answer already.
pub(crate) fn fills(reported: LayoutLen, declared: Option<LayoutLen>, decided: bool) -> bool {
reported.leftover != Weight::ZERO || declared.is_some() || decided
@@ -612,21 +716,26 @@ pub(crate) fn placed_box(region: UiRegion, lens: UiVec2, align: RegionAlign) ->
placed
}
/// Takes a widget's declared lengths in the box `region` is given in, since a
/// fraction of a length means a fraction of that one, and puts what is left
/// 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.
pub(crate) fn declared_box(
/// A declared axis gets a frame of that length, aligned within the parent's
/// slot (or the offer). Undeclared axes keep the offered frame and chosen
/// placement, so their reported fractions retain that reference.
pub(crate) fn ask_box(
mut region: UiRegion,
declared: [Option<LayoutLen>; 2],
align: RegionAlign,
) -> UiRegion {
for (axis, len) in AXES.into_iter().zip(declared) {
let Some(len) = len else { continue };
placement: [Option<UiSpan>; 2],
) -> (UiRegion, [Option<UiSpan>; 2]) {
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 len = Len::from_parts(len.rel, len.px);
span.start += (span.len() - len).scale(align.axis(axis).rel());
let slot = chosen.unwrap_or(*span);
span.start = slot.start + (slot.len() - len).scale(align.axis(axis).rel());
span.end = span.start + len;
}
region
(region, placed)
}
+326 -275
View File
@@ -1,9 +1,9 @@
#[cfg(feature = "layout-diagnostics")]
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::{
ActiveData, Axis, DrawLayers, Holds, IdLike, LayoutLen, Len, MaskIdx, MoveIdx, Moves, Painter,
PixelRegion, Px, PxVec2, Rel, Size, StrongWidget, UiRegion, UiRsc, UiSpan, UiVec2, Weight,
ActiveData, Axis, DrawLayers, Holds, IdLike, LayoutHolds, LayoutLen, MaskIdx, MoveIdx, Moves,
Painter, PixelRegion, PxVec2, Size, StrongWidget, UiRegion, UiRsc, UiSpan, UiVec2, Weight,
WidgetId, Widgets,
util::{HashMap, Vec2},
};
@@ -20,20 +20,36 @@ pub(super) struct DrawInfo {
pub parent_move: MoveIdx,
pub region_node: bool,
pub mask: MaskIdx,
/// The box its parent gave it, as lengths of the parent's own box, and
/// the lengths of the box it was first asked about in the same form.
/// Both describe the box the *parent* stated, so the second, placing ask
/// carries them unchanged while its own region is the placement inside.
pub given_len: UiVec2,
/// The frame in the parent widget's coordinates, before composition.
pub given_region: UiRegion,
/// The original offer's lengths relative to the parent's own offer.
pub offer_len: UiVec2,
pub offer_placement: [Option<UiSpan>; 2],
/// 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.
pub px: PxVec2,
pub offered_px: PxVec2,
/// The axes along which the parent chose this box from the widget's own
/// answer, so the answer is not placed inside it again. See
/// [`Painter::widget_at`].
pub decided: [bool; 2],
/// What of that region the parent chose to put the drawing in, per axis.
/// `None` leaves the axis to the widget's own answer and its alignment.
/// See [`Painter::widget_at`].
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 {
@@ -43,7 +59,9 @@ pub struct UiRenderState {
old_root: Option<WidgetId>,
/// 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,
/// A widget's move slot, which outlives any one `ActiveData`: a redraw
/// 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`,
/// and in the shader by its uniform. A resize therefore rewrites no
/// 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());
if size == self.output_size {
return;
}
self.output_size = size;
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
@@ -106,11 +139,12 @@ impl UiRenderState {
parent_move: MoveIdx::NONE,
region_node: false,
mask: MaskIdx::NONE,
given_len: region.size(),
given_region: region,
offer_len: UiVec2::FULL_SIZE,
offer_placement: [None; 2],
px,
offered_px: px,
decided: [false; 2],
placement: [None; 2],
}
}
@@ -143,17 +177,6 @@ impl UiRenderState {
if self.root_changed(root) {
self.redraw_all(root, rsc);
self.old_root = root.map(|r| r.id());
} else if let Some(root) = root
&& self.resized
{
// The output is the root's box, so a resize is that box changing
// length, found the way every other box change is found. Before
// anything dirty settles, so that whatever a new output draws
// again is drawn once, in the box it will have.
let 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;
if rsc.widgets().has_updates() {
@@ -170,16 +193,18 @@ impl UiRenderState {
if let Some(id) = root {
let region = Self::root_region(id.id(), rsc.widgets());
let info = self.root_info(region);
self.draw_inner(id.id(), region, info, None, rsc);
self.draw_inner(id.id(), region, info, None, false, rsc);
}
}
fn root_region(id: WidgetId, widgets: &Widgets) -> UiRegion {
declared_box(
ask_box(
UiRegion::FULL,
declared_lens(widgets, id),
widgets.alignment(id),
[None; 2],
)
.0
}
pub(super) fn draw_inner(
@@ -188,84 +213,114 @@ impl UiRenderState {
region: UiRegion,
info: DrawInfo,
mut old: Option<ActiveData>,
measuring: bool,
rsc: &mut dyn UiRsc,
) -> (Size, [Holds; 2]) {
) -> (Size, LayoutHolds) {
#[cfg(feature = "layout-diagnostics")]
{
diag::bump(Counter::DrawRequests);
diag::draw_request(id, info.parent, region, info.px, info.region_node);
}
let align = rsc.widgets().alignment(id);
// Nothing this widget has is an answer while something it measured
// is dirty: settling that changes what it would report, and a widget
// settled inside its parent's draw tells nobody -- the comparison
// that marks a reader is in `redraw`, which is not what asked here.
// 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());
// Nothing this widget measured can be dirty while it draws: layout is
// one bottom-up walk, so anything deeper has settled or deferred to
// its own parent, and a deferred one leaves that parent marked.
let stale = rsc.widgets().needs_redraw.contains(&id);
let replace_answer = self.answer_invalid.remove(&id) || (self.replace_answers && stale);
let retained = match replace_answer || stale {
true => None,
false => self
.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(|| {
if old.is_none() {
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);
// The second, final ask is in a box chosen from the answer on both
// axes, which is also what makes it terminate.
let lens = placed_lens(answer.0, declared, info.decided);
let placed = placed_box(region, lens, align);
let placed_info = DrawInfo {
px: lens.to_px(info.px),
decided: [true; 2],
..info
// Where the drawing goes, in the region's own coordinates: what the
// parent chose, and on any axis it left open, what the answer took of
// the region placed by the widget's alignment. The region itself does
// not change, so nothing under it resolves a fraction a second time.
let placement = if measuring {
info.offered_placement()
} else {
let declared = declared_lens(rsc.widgets(), id);
let lens = placed_lens(answer.0, declared, info.decided());
let own = placed_box(UiRegion::FULL, lens, align);
UiRegion {
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:
// what the widget reported in the box it was asked in, and what it
// drew in the box its report selected. Express the latter's contract
// back in terms of the box asked in before handing it to the parent.
// On axes chosen by the parent, measurement and drawing share an
// extent. Otherwise the answer fixes the final extent as a function
// of the frame, so pull that drawing's validity back through it.
let drawing_holds = self.active[&id].holds;
let mut settled = answer;
for axis in AXES {
settled.1[axis as usize] =
settled.1[axis as usize].and(drawing_holds[axis as usize].through(lens.axis(axis)));
let n = axis as usize;
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();
// 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
// same question again from these.
active.given = region;
active.given_len = info.given_len;
active.region = region;
active.given_region = info.given_region;
active.offer_len = info.offer_len;
active.answer = settled;
active.decided = info.decided;
if info.placement == info.offer_placement && info.px == info.offered_px {
active.answer = Some(settled);
active.offer_placement = info.offer_placement;
}
active.decided = info.decided();
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
}
/// Draws a widget in the final box its answer chose, reusing the drawing
/// already there where its retained contract holds for that box. The
/// symbolic box can be unchanged while the box it sits in changed pixel
/// length, so what reuse checks is the box in pixels.
fn place(&mut self, id: WidgetId, placed: UiRegion, info: DrawInfo, rsc: &mut dyn UiRsc) {
if self.try_reuse(id, placed, info, rsc).is_none() {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::PlaceRedraws);
let old = self.remove(id, false, rsc);
self.draw_at(id, placed, info, old, rsc);
/// Recompose retained geometry when the evaluation still holds at this extent.
fn place(
&mut self,
id: WidgetId,
region: UiRegion,
placement: UiRegion,
info: DrawInfo,
rsc: &mut dyn UiRsc,
) {
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`.
@@ -273,10 +328,11 @@ impl UiRenderState {
&mut self,
id: WidgetId,
region: UiRegion,
placement: UiRegion,
info: DrawInfo,
old: Option<ActiveData>,
rsc: &mut dyn UiRsc,
) -> (Size, [Holds; 2]) {
) -> (Size, LayoutHolds) {
let (move_idx, local, retired_move) = match info.region_node {
// Its box becomes its movable region, so it draws in that
// region's coordinates and its box is one entry to rewrite.
@@ -290,20 +346,25 @@ impl UiRenderState {
false => (info.parent_move, region, self.slots.remove(&id)),
};
let (old_children, old_answer) = match old {
Some(old) => (old.children, Some(old.answer)),
Some(old) => (old.children, old.answer),
None => (Vec::new(), None),
};
rsc.widgets_mut().needs_redraw.remove(&id);
// A box of the offered lengths asks the offer's question wherever it
// sits, since what a drawing depends on is its lengths -- and
// equality is the comparison, these being counts of a step rather
// than floats to be compared for nearness.
// Only evaluation at the original offer establishes the children's
// offers. A placing evaluation must not overwrite that question.
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 {
state: self,
region: local,
placement,
reads_placement: false,
px,
mask: info.mask,
layer: info.layer,
@@ -311,6 +372,8 @@ impl UiRenderState {
id,
textures: Vec::new(),
primitives: Vec::new(),
mask_region: None,
inherited_children: Vec::new(),
children: Vec::new(),
offered: Vec::new(),
offered_px: info.offered_px,
@@ -318,6 +381,8 @@ impl UiRenderState {
size_deps: Vec::new(),
own: [Holds::ANY; 2],
under: [Holds::ANY; 2],
extent_own: [Holds::ANY; 2],
extent_under: [Holds::ANY; 2],
depth: info.depth,
move_idx,
rsc,
@@ -338,10 +403,16 @@ impl UiRenderState {
state: _,
rsc: _,
region: _,
placement: _,
reads_placement,
px: _,
mask,
textures,
primitives,
mask_region,
inherited_children,
extent_own,
extent_under,
children,
offered: _,
offered_px: _,
@@ -379,9 +450,16 @@ impl UiRenderState {
"'{}' ({id:?}) clips to {px:?} and reports {size}",
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!(
holds[0].contains(px.x) && holds[1].contains(px.y),
holds.contains(px, placement),
"'{}' ({id:?}) drew in {px:?}, outside the ranges it reported: {holds:?}",
rsc.widgets().label(id),
);
@@ -408,11 +486,12 @@ impl UiRenderState {
parent_move: move_idx,
region_node: false,
mask,
given_len: UiVec2::FULL_SIZE,
given_region: UiRegion::FULL,
offer_len: UiVec2::FULL_SIZE,
offer_placement: [None; 2],
px,
offered_px: px,
decided: [false; 2],
placement: [None; 2],
},
rsc,
);
@@ -423,14 +502,12 @@ impl UiRenderState {
let active = ActiveData {
id,
region,
// The box a placing ask draws in is a part of the one its parent
// gave, which `draw_inner` writes back over these once the
// placement is done.
given: region,
given_len: info.given_len,
placement,
given_region: info.given_region,
offer_len: info.offer_len,
offer_placement: info.offer_placement,
// Whoever asked writes the answer, if this was the asking.
answer: old_answer.unwrap_or((size, holds)),
answer: old_answer,
size,
holds,
drawn: true,
@@ -438,10 +515,12 @@ impl UiRenderState {
depth: info.depth,
textures,
primitives,
mask_region,
inherited_children,
children,
size_deps,
declared: declared_lens(rsc.widgets(), id),
decided: info.decided,
decided: info.decided(),
own_align: rsc.widgets().alignment(id),
move_idx,
parent_move: info.parent_move,
@@ -481,18 +560,24 @@ impl UiRenderState {
&self,
id: WidgetId,
px: PxVec2,
placement: [Option<UiSpan>; 2],
parent_move: MoveIdx,
widgets: &Widgets,
) -> Option<(Size, [Holds; 2])> {
if widgets.needs_redraw.contains(&id) || self.dirty_size_under(id, widgets) {
) -> Option<(Size, LayoutHolds)> {
if widgets.needs_redraw.contains(&id) {
return None;
}
let active = self.active.get(&id)?;
let (size, holds) = active.answer;
let (size, holds) = active.answer?;
let valid = active.drawn
&& active.parent_move == parent_move
&& holds[0].contains(px.x)
&& holds[1].contains(px.y);
&& holds.contains(
px,
UiRegion {
x: placement[0].unwrap_or(UiSpan::FULL),
y: placement[1].unwrap_or(UiSpan::FULL),
},
);
valid.then_some((size, holds))
}
@@ -500,7 +585,7 @@ impl UiRenderState {
/// drawing ended up. Alignment is exactly that case: the first box is 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.
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 has_region_node = active.move_idx != active.parent_move;
if !active.drawn
@@ -509,22 +594,11 @@ impl UiRenderState {
{
return None;
}
let (size, holds) = active.answer;
(holds[0].contains(info.px.x) && holds[1].contains(info.px.y)).then_some((size, holds))
}
/// Whether anything whose size this widget's own size was read from is
/// 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)
})
})
let answer = active.answer?;
answer
.1
.contains(info.px, info.offered_placement())
.then_some(answer)
}
/// The pixel lengths of the box a widget was given and of the box it was
@@ -542,14 +616,10 @@ impl UiRenderState {
// Nothing above the root: the window is where a fraction becomes
// 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)) {
Some(parent) => {
let (given, offer) = self.asked_px(parent.id);
let lens = placed_lens(parent.answer.0, parent.declared, parent.decided);
(lens.to_px(given), offer)
}
Some(parent) => self.asked_px(parent.id),
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);
for axis in AXES {
// A declared length is resolved by whoever drew the widget, in
@@ -568,9 +638,10 @@ impl UiRenderState {
&mut self,
id: WidgetId,
region: UiRegion,
placement: UiRegion,
info: DrawInfo,
rsc: &mut dyn UiRsc,
) -> Option<(Size, [Holds; 2])> {
) -> Option<(Size, LayoutHolds)> {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::ReuseAttempts);
if rsc.widgets().needs_redraw.contains(&id) {
@@ -619,7 +690,7 @@ impl UiRenderState {
// In pixels, because `region` is a fraction of the box its parent
// drew in and that box may be what changed -- an unchanged fraction
// 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")]
{
diag::bump(Counter::ReuseOutside);
@@ -627,23 +698,24 @@ impl UiRenderState {
}
return None;
}
let extent_moved = active.placement != placement;
let moved = active.region != region;
let (answer, old_region, slot) =
((active.size, active.holds), active.region, active.move_idx);
let (answer, slot) = ((active.size, active.holds), active.move_idx);
if moved {
if has_region_node {
self.moves.set(slot, region);
} else {
let remap = RegionRemap::new(old_region, region)?;
self.remap_subtree(id, &remap, info.parent_move, rsc);
self.recompose_subtree(id, region, 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();
active.region = region;
active.given = region;
active.given_len = info.given_len;
active.given_region = info.given_region;
active.offer_len = info.offer_len;
active.depth = info.depth;
#[cfg(feature = "layout-diagnostics")]
{
match (moved, has_region_node) {
@@ -667,41 +739,117 @@ impl UiRenderState {
Some(answer)
}
/// Re-expresses an ordinary retained subtree in a new parent region.
/// An independently movable descendant needs only its own region changed;
/// its contents stay in that region's coordinate space.
fn remap_subtree(
fn reposition(
&mut self,
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,
rsc: &mut dyn UiRsc,
) {
let active = self.active.get_mut(&id).unwrap();
active.given = remap.apply(active.given);
active.region = region;
if active.move_idx != parent_move {
let region = remap.apply(active.region);
active.region = region;
self.moves.set(active.move_idx, region);
return;
}
for handle in &active.primitives {
let region = self.layers[handle.layer].region_mut(handle);
*region = remap.apply(*region);
for primitive in &active.primitives {
let handle = &primitive.handle;
*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();
// 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 {
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 +868,8 @@ impl UiRenderState {
fn remove(&mut self, id: WidgetId, undraw: bool, rsc: &mut dyn UiRsc) -> Option<ActiveData> {
let mut active = self.active.remove(&id);
if let Some(active) = &mut active {
for h in &active.primitives {
let mask = self.layers.free(h);
for primitive in &active.primitives {
let mask = self.layers.free(&primitive.handle);
if mask != MaskIdx::NONE {
rsc.ui_mut().masks.remove(mask);
}
@@ -776,17 +924,20 @@ impl UiRenderState {
ActiveData {
id,
region: UiRegion::FULL,
given: UiRegion::FULL,
given_len: UiVec2::FULL_SIZE,
placement: UiRegion::FULL,
given_region: UiRegion::FULL,
offer_len: UiVec2::FULL_SIZE,
answer: (size, [Holds::ANY; 2]),
offer_placement: [None; 2],
answer: None,
size,
holds: [Holds::ANY; 2],
holds: LayoutHolds::ANY,
drawn: false,
parent: info.parent,
depth: info.depth,
textures: Vec::new(),
primitives: Vec::new(),
mask_region: None,
inherited_children: Vec::new(),
children: Vec::new(),
size_deps: Vec::new(),
move_idx: info.parent_move,
@@ -935,8 +1086,9 @@ impl UiRenderState {
pub fn window_region(&self, id: &impl IdLike) -> Option<PixelRegion> {
let active = self.active.get(&id.id())?;
active.drawn.then(|| {
let placed = active.placement.within(&active.region);
self.moves
.resolve(active.parent_move, active.region)
.resolve(active.parent_move, placed)
.to_px(self.output_size)
})
}
@@ -957,7 +1109,7 @@ impl UiRenderState {
let declared_changed = declared_lens(rsc.widgets(), id) != active.declared;
let alignment_changed = rsc.widgets().alignment(id) != active.own_align;
if let Some(parent) = active.parent
&& (declared_changed || alignment_changed || !active.drawn)
&& (declared_changed || alignment_changed || !active.drawn || active.answer.is_none())
{
if declared_changed {
self.replace_answers = true;
@@ -990,7 +1142,7 @@ impl UiRenderState {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::LocalRedraws);
let old = self.remove(id, false, rsc);
self.draw_inner(id, region, info, old, rsc);
self.draw_inner(id, region, info, old, false, rsc);
return true;
};
let (given_px, offered_px) = self.asked_px(id);
@@ -1011,21 +1163,32 @@ impl UiRenderState {
parent_move: active.parent_move,
region_node: rsc.widgets().is_region_node(id),
mask: active.parent_mask,
given_len: active.given_len,
given_region: active.given_region,
offer_len: active.offer_len,
offer_placement: active.offer_placement,
px: given_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")]
diag::bump(Counter::LocalRedraws);
let old = self.remove(id, false, rsc);
// `draw_inner` places the answer inside that box itself, which is the
// ask that leaves the widget where its parent put it.
let answer = self.draw_inner(id, given, info, old, rsc);
if answer != was_answer {
// Refresh the original measurement before restoring the assigned slot.
// Its lengths may differ even though the fraction reference is unchanged.
let offered = DrawInfo {
placement: info.offer_placement,
..info
};
let answer = self.draw_inner(id, given, offered, old, false, rsc);
if info.placement != offered.placement {
self.draw_inner(id, given, info, None, false, rsc);
}
if Some(answer) != was_answer {
// Its parent chose its box knowing the old answer, so it lays out
// again and chooses the box the new one asks for.
#[cfg(feature = "layout-diagnostics")]
@@ -1048,118 +1211,6 @@ fn within_box(size: Size, px: PxVec2, axis: Axis) -> bool {
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 {
fn default() -> Self {
Self::new()
+1 -1
View File
@@ -251,7 +251,7 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
ui_state.renderer.draw();
}
WindowEvent::Resized(size) => {
render.resize((size.width, size.height));
render.resize((size.width, size.height), rsc.widgets_mut());
ui_state.renderer.resize(size)
}
WindowEvent::KeyboardInput { event, .. } => {
+3 -3
View File
@@ -144,9 +144,9 @@ impl Harness {
// bound that comes with `SyncSender` is far past anything a test
// leaves unread.
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();
render.resize(size);
render.resize(size, rsc.widgets_mut());
Self {
rsc,
render,
@@ -161,7 +161,7 @@ impl Harness {
}
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.
+1 -1
View File
@@ -6,7 +6,7 @@ pub struct Masked {
impl Widget for Masked {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.set_mask(painter.region());
painter.set_mask(DrawRegion::Extent(UiRegion::FULL));
painter.widget(&self.inner);
// 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
+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
// 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.
let inner = painter
.widget_within(&self.inner, self.padding.region())
.size();
let inside = self.padding.region_of(painter.placement());
let inner = painter.widget_within(&self.inner, inside).size();
Size {
x: LayoutLen {
px: inner.x.px + self.padding.left + self.padding.right,
@@ -53,14 +52,18 @@ impl Padding {
bottom: amt,
}
}
pub fn region(&self) -> UiRegion {
let mut region = UiRegion::FULL;
/// `region` less this padding on each side.
pub fn region_of(&self, mut region: UiRegion) -> UiRegion {
region.x.start.px += self.left;
region.y.start.px += self.top;
region.x.end.px -= self.right;
region.y.end.px -= self.bottom;
region
}
pub fn region(&self) -> UiRegion {
self.region_of(UiRegion::FULL)
}
pub fn x(amt: impl UiNum) -> Self {
let amt = Px::from_num(amt);
Self {
+8 -5
View File
@@ -15,10 +15,9 @@ impl Widget for Scroll {
// Draw in the whole container only when its scrolling-axis length is
// not already known, then draw it at the scrolled offset.
let whole = UiRegion::FULL;
let answer_len = match painter.known_len(&self.inner, self.axis, whole, whole.size()) {
Some(len) => len,
None => painter.widget(&self.inner).size().axis(self.axis),
};
let own = painter.placement();
let answer_len =
painter.measure_len(&self.inner, self.axis, whole, [Some(own.x), Some(own.y)]);
let content = answer_len.apply_leftover();
self.container_len = container_len;
self.content_len = content.to_px(container_len);
@@ -64,7 +63,11 @@ impl Widget for Scroll {
region = region.offset(offset);
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
// 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
+46 -65
View File
@@ -10,26 +10,34 @@ pub struct Span {
impl Widget for Span {
fn draw(&mut self, painter: &mut Painter) -> Size {
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 hint where one exists, and from drawing otherwise.
let mut cursor = Len::rel_min();
let mut lens = Vec::with_capacity(self.children.len());
for child in &self.children {
let mut span = UiSpan::new(cursor, Len::rel_max());
if self.dir.sign == Sign::Neg {
span.flip();
}
let region = UiRegion::from_axis(axis, span, UiSpan::FULL);
// Offered the room left from the cursor, because a text has to
// wrap at the width actually there, but reporting a fraction of
// 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,
None => painter
.widget_at(child, region, UiVec2::FULL_SIZE, [false; 2])
.len(axis),
};
// The whole region is the child's, so `rel(0.5)` is half the area
// this span was given whatever else is in it and wherever this
// 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
// actually there.
let room = axis.pair(Some(along(cursor, far)), None);
let len = painter.measure_len(child, axis, region, room);
cursor.px += len.px + self.gap;
cursor.rel += len.rel;
lens.push(len);
@@ -46,43 +54,26 @@ impl Widget for Span {
|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)`
// beside 300 px is full at 600 and overfull at 400. The room to
// divide is `len * fixed - total.px`, and the length where it runs
// out is exactly the box a parent sizing itself from this answer
// hands back -- which is why this used to need a margin either side
// of the boundary, and why it does not now: that box and this sum are
// whole counts of the same step, and both routes to it land on the
// same count. What the generated oracle checks is the consequence,
// since which children exist at all turns on this.
let fixed = Rel::ONE - total.rel;
// beside 300 px is full at 600 and overfull at 400. Asked of `room`
// itself, and answered back through the same expression, so the
// boundary is the drawing's own and not a second way of finding it:
// the three cases a rounded division needed -- the fixed parts
// growing slower than the box, faster, or exactly with it -- are the
// sign of `room.rel`, which `through` already reads. What the
// generated oracle checks is the consequence, since which children
// exist at all turns on this.
let mut shares = false;
if total.leftover > Weight::ZERO {
let current = painter.px_len(axis);
let holds = if fixed > Rel::ZERO {
// The box length the fixed parts alone fill.
let full = total.px.div(fixed);
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
shares = room.to_px(painter.region_px_len(axis)) > Px::ZERO;
let holds = match shares {
true => Holds::from(Px::STEP..=Px::MAX),
false => Holds::from(Px::MIN..=Px::ZERO),
};
painter.holds(axis, holds);
painter.region_holds(axis, holds.through(room));
}
// Across itself a span is as long as its longest child -- unless a
@@ -99,7 +90,6 @@ impl Widget for Span {
// row.
let mut fixed = Len::rel_min();
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 ortho = LayoutLen::ZERO;
for (child, len) in self.children.iter().zip(&lens) {
@@ -112,28 +102,19 @@ impl Widget for Span {
fixed.px += self.gap;
continue;
}
let mut span = UiSpan::FULL;
span.start = start;
let from = start;
if len.leftover > Weight::ZERO && shares {
taken += len.leftover;
}
fixed.px += len.px;
fixed.rel += len.rel;
start = shared(fixed, taken, total.leftover, room);
span.end = start;
let mut region = UiRegion::from_axis(axis, span, UiSpan::FULL);
if self.dir.sign == Sign::Neg {
region.flip(axis);
}
// Along the row this box is the child's own answer, so the answer
// 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],
);
// Along the row the span says where the child goes; across it the
// child sits where its own alignment says. Its region is the
// whole of what this span was given either way, which is what its
// fractions are of.
let placed =
painter.widget_at(child, region, axis.pair(Some(along(from, start)), None));
if shrinks {
let used = placed.len(!axis);
// Choosing between a fixed and a relative length from the
+20 -14
View File
@@ -13,31 +13,37 @@ impl Widget for Stack {
StackSize::Default => None,
StackSize::Child(i) => Some(i),
};
// Whichever child sizes the stack decides the box every child gets.
// The stack reports that size, so a child given a longer box would
// draw outside what the stack says it occupies.
// This stack's own box, which is `FULL` until its answer is known.
let placement = painter.placement();
// Whichever child sizes the stack keeps the stack's whole region as
// its own -- the stack is the length that child asked for, so taking
// the fraction of the stack's box again would take it twice -- and is
// put where the stack itself is put.
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)) => {
painter.child_layer_at(i);
painter.widget(child).size()
painter
.widget_at(
child,
UiRegion::FULL,
[Some(placement.x), Some(placement.y)],
)
.size()
}
None => Size::LEFTOVER,
};
let region = painter.box_of(size);
for (i, child) in self.children.iter().enumerate() {
if sizing == Some(i) {
continue;
}
painter.child_layer_at(i);
// The sizing child placed its own content in the box its answer
// decided, and this box was derived from that answer, so applying
// its alignment again here would place it twice. Every other
// child is handed a box that owes nothing to its own answer, and
// where it sits in one bigger than itself is its own business.
match sizing == Some(i) {
true => painter.widget_at(child, region, region.size(), [true; 2]),
false => painter.widget_within(child, region),
};
// Every other child has the stack's own box for its region, since
// the stack is what contains it, and where it sits in one bigger
// than itself is its own business.
painter.widget_within(child, placement);
}
size
}
+5 -15
View File
@@ -50,20 +50,11 @@ impl TextView {
let width = self.attrs.wrap.then(|| painter.px_len(Axis::X));
// The shaper measures in floats, which is where a glyph advance comes
// from; what it answers goes back on the grid.
let text = 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
// line up to the one it was made at: each line still fits, and none
// 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);
painter.render_text(&mut self.buf, &self.attrs, width.map(Px::to_f32));
if width.is_some() {
painter.holds(Axis::X, self.buf.width_holds());
}
text
self.buf.rendered().expect("render_text placed the glyphs")
}
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
// cold layout makes there.
let size = Size::from_px(PxVec2::ceil_from_f32(tex.size));
let within = region.within(&painter.region());
painter.glyphs(tex, within);
painter.glyphs(tex, DrawRegion::Extent(region));
(region, size)
}
+47 -8
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));
}
/// A span offers each child the room left after the one before, because a
/// text has to wrap at the width actually there, but reads what the child
/// reports as a fraction of the whole row. So two children asking for half
/// each take the whole row between them, however much of it was left when
/// each was asked, and a third overflows.
/// 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 the child's region is
/// the whole row. So two children asking for half each take the whole row
/// between them, however much of it was left when each was asked, and a third
/// 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]
fn a_span_reads_a_child_report_as_a_fraction_of_the_row() {
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);
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
// once more; half of that final box is what its own child takes.
// The nested span is placed at the length it reported, and its own child
// asks for half of the row rather than half of that placement.
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));
}
@@ -698,3 +699,41 @@ 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));
}
#[test]
fn a_fixed_child_is_centered_in_its_wrappers_share() {
let mut h = Harness::new((600, 300));
let leaf = rect(Color::RED).sized((100, 100)).center().add(&mut h.rsc);
let wrapper = leaf
.wrapper()
.width(leftover(2))
.height(rel(1.0))
.add(&mut h.rsc);
let other = rect(Color::BLUE).width(200).add(&mut h.rsc);
h.set_root((other, wrapper).span(Dir::RIGHT));
assert_corners!(h, wrapper, (200, 0), (600, 300));
assert_corners!(h, leaf, (350, 100), (450, 200));
h.resize((900, 400));
h.frame();
assert_corners!(h, wrapper, (200, 0), (900, 400));
assert_corners!(h, leaf, (500, 150), (600, 250));
}
+333 -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));
assert_eq!(told_draws.get(), 1);
// Reading its box makes its drawing hold for the measuring box alone,
// and it reports less than that box: so it is drawn again in the box its
// answer places it in, and once more in the final box the span chooses.
// 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"
);
// Only the available length changes: positioning the final slot does
// not invalidate a numeric size read.
assert_eq!(asked_draws.get(), 2);
}
#[test]
@@ -628,3 +621,333 @@ fn a_masked_widget_redrawn_on_its_own_sets_its_mask_again() {
h.frame();
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()));
}
}
#[test]
fn a_span_does_not_place_its_measurement_before_assigning_the_childs_slot() {
struct MeasuredBox(Rc<Cell<usize>>);
impl Widget for MeasuredBox {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.0.set(self.0.get() + 1);
painter.px_size();
painter.primitive(RectPrimitive::color(Color::BLUE));
Size::from((100, 50))
}
}
let mut h = Harness::new((400, 200));
let draws = Rc::new(Cell::new(0));
let leaf = MeasuredBox(draws.clone()).add(&mut h.rsc);
h.set_root((leaf,).span(Dir::RIGHT).width(rel(1.0)).height(rel(1.0)));
assert_eq!(draws.get(), 3);
assert_corners!(h, leaf, (0, 75), (100, 125));
assert_eq!(
primitive_bounds(&h, leaf.id()),
vec![h.region(&leaf.id()).unwrap()]
);
h.frame();
assert_eq!(draws.get(), 3);
h.resize((600, 300));
h.frame();
assert_eq!(draws.get(), 6);
assert_corners!(h, leaf, (0, 125), (100, 175));
assert_eq!(
primitive_bounds(&h, leaf.id()),
vec![h.region(&leaf.id()).unwrap()]
);
}
+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));
}
#[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));
}
+7 -4
View File
@@ -5,11 +5,11 @@
//! cargo test --release --features layout-diagnostics \
//! --test layout_diagnostics -- --ignored --nocapture
//!
//! Uninstrumented hardware totals for one phase:
//! Build the uninstrumented test with `cargo test --release --test
//! layout_diagnostics --no-run`, then run the emitted executable directly:
//!
//! IRIS_PHASE=resize IRIS_FRAMES=1000 perf stat \
//! -e cycles:u,instructions:u cargo test --release \
//! --test layout_diagnostics -- --ignored --nocapture
//! IRIS_PHASE=resize IRIS_FRAMES=10000 perf stat -r 7 \
//! -e cycles:u,instructions:u /path/to/layout_diagnostics --ignored --nocapture
//!
//! `IRIS_PHASE` is `cold`, `repaint`, `many`, `size`, `scroll`, `resize`, or
//! `all`. `IRIS_SEED`, `IRIS_DEPTH`, and `IRIS_FRAMES` select the load, and
@@ -134,6 +134,9 @@ fn report(label: &str, mut elapsed: Vec<f64>, _harness: &Harness) {
{
let diagnostics = iris::core::layout_diagnostics::take();
print!("{}", diagnostics.per_frame(frames));
for event in diagnostics.traces() {
println!(" {event:?}");
}
for callsite in diagnostics.hot_text().iter().take(3) {
let mut ancestry = Vec::new();
let mut id = Some(callsite.id);
+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 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:
/// 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.
@@ -399,20 +384,6 @@ fn describe_widget(id: WidgetId, h: &Harness) -> String {
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
/// 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.
@@ -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() {
let (got, want) = (warm.region(&w), cold.region(&c));
drawn += got.is_some() as usize;
if same_region(got, want) {
if got == want {
continue;
}
// Where two trees disagree is rarely where the cause is, so the