Read a child's answer in the asker's frame, and drop the root move entry

A widget reports a fraction of the box it was given. Span added that
fraction straight into a cursor that counts fractions of the row, and Pad
summed its padding onto it, both right only while the offer had the
parent's whole extent -- which a span's does not after a relative child.
DrawResult::size and known_len now compose the answer through the offer's
length, so a container reads lengths of its own box.

That exposed placed_box scaling a fractional answer against a box the
parent had already chosen from it, halving a nested span twice. The
near-edge alignment override becomes per-axis `decided` flags: a box the
parent chose from the answer is the answer, and is not placed again.
Span decides the row axis; Scroll and Stack's sizing child decide both.
Alignment is always the widget's own property now.

The window is no longer a move entry. Chains bottom out in MoveIdx::NONE
and the window is applied where a fraction becomes pixels, in to_px on the
CPU and by the uniform in the shader, which now snaps the summed coordinate
since a floor does not distribute over a sum. A resize rewrites no entry.

Verified: view, minimal, random, tabs and text render byte-identical at
1920x1200 against 5f16617, a live resize to 1280x800 is identical to a
cold render, and the 100-seed oracle, all fifteen shrinker cases at 400
seeds of depth 5, and 1000 seeds of depth 6 pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
iris-aiandClaude Fable 5.1 committed 2026-09-16 23:00:15 -04:00
1 parent 5f16617511
commit 5b7800264d
11 files changed
+179 -106

No files matched your search

+1 -2
View File
@@ -84,8 +84,7 @@ pub struct RegionAlign {
}
impl RegionAlign {
/// Both axes at the near edge. What a container passes as an override for
/// a child it is going to position itself.
/// Both axes at the near edge: the start of a box in its own orientation.
pub const NEAR: Self = Self {
x: AxisAlign::NEG,
y: AxisAlign::NEG,
+19
View File
@@ -125,6 +125,13 @@ impl Size {
Axis::Y => self.y,
}
}
pub fn axis_mut(&mut self, axis: Axis) -> &mut LayoutLen {
match axis {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
}
}
}
impl LayoutLen {
@@ -151,6 +158,18 @@ impl LayoutLen {
Len::from_parts(self.rel.add(share), self.px)
}
/// This length, given as a part of a box `len` long, as a part of the
/// box `len` is itself a part of. The share is untouched: it is a claim
/// on whoever divides the room, not a fraction of anything.
pub const fn within_len(self, len: Len) -> Self {
let part = Len::from_parts(self.rel, self.px).within_len(len);
Self {
px: part.px,
rel: part.rel,
leftover: self.leftover,
}
}
pub fn px(px: impl UiNum) -> Self {
Self {
px: Px::from_num(px),
+1 -1
View File
@@ -219,7 +219,7 @@ impl Len {
}
}
pub fn within_len(&self, len: Len) -> Self {
pub const fn within_len(&self, len: Len) -> Self {
self.within(&UiSpan {
start: Len::ZERO,
end: len,
+8 -4
View File
@@ -35,6 +35,10 @@ struct MoveOffset {
// belongs to the pixel above it. Flooring the product instead drops a pixel
// wherever a fraction divides a window exactly: a fifth of 1920 comes out of
// `REL_STEP` as 383.99998, and five tabs each lose their last column.
//
// Taken over the whole coordinate, fraction and pixels summed, since a floor
// does not distribute over a sum: floored apart, a half of one and a half of
// the other lose the pixel the two together make.
fn snap_floor(v: vec2<f32>) -> vec2<f32> {
return floor(v + PX_STEP * 0.5);
}
@@ -147,8 +151,8 @@ fn vs_main(
let bot_right_rel = vec2(r.x.end.rel, r.y.end.rel);
let bot_right_px = vec2(r.x.end.px, r.y.end.px);
let top_left = snap_floor(top_left_rel * window.dim) + snap_floor(top_left_px);
let bot_right = snap_floor(bot_right_rel * window.dim) + snap_floor(bot_right_px);
let top_left = snap_floor(top_left_rel * window.dim + top_left_px);
let bot_right = snap_floor(bot_right_rel * window.dim + bot_right_px);
let size = bot_right - top_left;
let uv = vec2<f32>(
@@ -179,8 +183,8 @@ fn masked(in: VertexOutput, color: vec4<f32>) -> vec4<f32> {
let br = vec2(m.x.end.rel, m.y.end.rel);
let br_px = vec2(m.x.end.px, m.y.end.px);
let top_left = snap_floor(tl * window.dim) + snap_floor(tl_px);
let bot_right = snap_floor(br * window.dim) + snap_floor(br_px);
let top_left = snap_floor(tl * window.dim + tl_px);
let bot_right = snap_floor(br * window.dim + br_px);
let pos = in.clip_position.xy;
if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y {
return color * 0.0;
+5 -8
View File
@@ -41,14 +41,11 @@ pub struct ActiveData {
/// A change to one moves a box this widget cannot fix by drawing again,
/// and comparing them is what says so.
pub declared: [Option<LayoutLen>; 2],
/// The alignment its parent asked it with. A local redraw repeats that
/// question, including an override chosen by a container.
pub align: RegionAlign,
/// Whether that alignment was the parent's override rather than the
/// widget's own property.
pub align_override: bool,
/// Its own alignment when it was last drawn. A change to the property is
/// found against this even when its parent overrode the alignment.
/// The axes along which its parent chose its box from its own answer,
/// so a local redraw asks the question its parent asked.
pub decided: [bool; 2],
/// Its alignment when it was last drawn, which a change to the property
/// is found against.
pub own_align: RegionAlign,
/// The movable region whose coordinates `region` uses.
pub parent_move: MoveIdx,
+56 -20
View File
@@ -135,30 +135,34 @@ impl<'a> Painter<'a> {
id: &'s StrongWidget<W>,
region: UiRegion,
) -> DrawResult<'s, 'a, W> {
self.widget_at(id, region, None)
self.widget_at(id, region, [false; 2])
}
/// Draws a widget with an alignment chosen by its container rather than
/// the widget's property. Containers use this when the box they hand down
/// already expresses the size they report around the child.
pub fn widget_aligned<'s, W: ?Sized>(
/// Draws a widget in a box this widget chose from the widget's own
/// answer along the `decided` axes. On those the answer is not placed
/// inside the box again: it already is the box, and a fraction the
/// widget reported of its offer, taken of this box a second time, would
/// shrink it twice. A container uses this 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.
pub fn widget_decided<'s, W: ?Sized>(
&'s mut self,
id: &'s StrongWidget<W>,
region: UiRegion,
align: RegionAlign,
decided: [bool; 2],
) -> DrawResult<'s, 'a, W> {
self.widget_at(id, region, Some(align))
self.widget_at(id, region, decided)
}
fn widget_at<'s, W: ?Sized>(
&'s mut self,
id: &'s StrongWidget<W>,
region: UiRegion,
align_override: Option<RegionAlign>,
decided: [bool; 2],
) -> DrawResult<'s, 'a, W> {
let region_node = self.rsc.widgets().is_region_node(id.id());
let declared = self.declared_lens(id);
let align = align_override.unwrap_or_else(|| self.rsc.widgets().alignment(id.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) {
@@ -200,7 +204,7 @@ impl<'a> Painter<'a> {
offer,
offered_px: self.px_within_offer(offer),
slot_wide: self.slot_wide,
align: align_override,
decided,
},
None,
self.rsc,
@@ -216,7 +220,7 @@ impl<'a> Painter<'a> {
DrawResult {
child: id,
painter: self,
size,
size: in_parent_frame(size, local, declared),
}
}
@@ -282,7 +286,7 @@ impl<'a> Painter<'a> {
for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) {
*under = under.and(holds[axis as usize].through(local.axis(axis).len()));
}
Some(size.axis(axis))
Some(in_parent_frame(size, local, declared).axis(axis))
}
/// Whether this is the first box a child is asked about in during a draw
@@ -393,7 +397,13 @@ impl<'a> Painter<'a> {
/// near edge. A container that reports one child's size gives every child
/// this, so what it draws is inside what it says it occupies.
pub fn box_of(&self, size: Size) -> UiRegion {
placed_box(UiRegion::FULL, size, RegionAlign::NEAR, [None; 2])
placed_box(
UiRegion::FULL,
size,
RegionAlign::NEAR,
[None; 2],
[false; 2],
)
}
/// This widget's box in pixels. Reading it makes the drawing one that
@@ -517,6 +527,23 @@ impl PrimitiveLike for &TextureHandle {
}
}
/// A child's answer as lengths of the box it was asked from. A widget reports
/// a fraction of the box it was given, and the widget that gave it wants the
/// same length as a fraction of its own: one composition apart wherever the
/// offer was not the whole of the parent's extent, as a span's is after a
/// relative child. 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, local: UiRegion, declared: [Option<LayoutLen>; 2]) -> Size {
let mut size = size;
for (axis, declared) in AXES.into_iter().zip(declared) {
if declared.is_none() {
let len = local.axis(axis).len();
*size.axis_mut(axis) = size.axis(axis).within_len(len);
}
}
size
}
/// What a widget declares a length of its box to be. `leftover` is not one: a
/// share of what is left over is only a length to the widget dividing one,
/// so it passes up in the size instead.
@@ -537,12 +564,20 @@ 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
/// the answer already.
pub(crate) fn fills(reported: LayoutLen, declared: Option<LayoutLen>, decided: bool) -> bool {
reported.leftover != Weight::ZERO || declared.is_some() || decided
}
/// The box a drawing occupies: the size the widget reported, on the side of
/// the box it was asked in that its alignment says. An axis reported as 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 is
/// left alone too: `declared_box` already placed it, in the parent's box,
/// and the rule's length is what the widget reports there.
/// the box it was asked in that its alignment says, on every axis that is
/// not simply filled.
///
/// A reported fraction is a fraction of the box the widget drew in, where a
/// declared one is a fraction of the box its parent handed down -- a span
@@ -553,11 +588,12 @@ pub(crate) fn placed_box(
size: Size,
align: RegionAlign,
declared: [Option<LayoutLen>; 2],
decided: [bool; 2],
) -> UiRegion {
let mut placed = region;
for (axis, declared) in AXES.into_iter().zip(declared) {
for (axis, (declared, decided)) in AXES.into_iter().zip(declared.into_iter().zip(decided)) {
let reported = size.axis(axis);
if reported.leftover != Weight::ZERO || declared.is_some() {
if fills(reported, declared, decided) {
continue;
}
let span = placed.axis_mut(axis);
+36 -59
View File
@@ -1,10 +1,10 @@
#[cfg(feature = "layout-diagnostics")]
use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind};
use crate::ui::painter::{declared_box, declared_lens, placed_box};
use crate::ui::painter::{declared_box, declared_lens, fills, placed_box};
use crate::{
ActiveData, Axis, DrawLayers, Holds, IdLike, LayoutLen, Len, MaskIdx, MoveIdx, Moves, Painter,
PixelRegion, Px, PxVec2, RegionAlign, Rel, Size, StrongWidget, UiRegion, UiRsc, UiSpan, Weight,
WideRegion, WidgetId, Widgets,
PixelRegion, Px, PxVec2, Rel, Size, StrongWidget, UiRegion, UiRsc, UiSpan, Weight, WideRegion,
WidgetId, Widgets,
util::{HashMap, Vec2},
};
@@ -27,9 +27,10 @@ pub(super) struct DrawInfo {
/// The box `parent_move` composes to, on the fine grid, so a widget's own
/// box is one step further and not a walk back up the chain.
pub slot_wide: WideRegion,
/// A container's answer for where the widget sits. `None` uses the
/// widget's own property.
pub align: Option<RegionAlign>,
/// 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_decided`].
pub decided: [bool; 2],
}
pub struct UiRenderState {
@@ -38,8 +39,6 @@ pub struct UiRenderState {
pub(super) output_size: PxVec2,
old_root: Option<WidgetId>,
/// The slot every chain bottoms out in, holding the output as a box.
root_move: MoveIdx,
/// Whether the output has changed since the last update. A frame is
/// owed for that whether or not anything has to be drawn again.
resized: bool,
@@ -67,35 +66,22 @@ impl UiRenderState {
answer_invalid: Default::default(),
replace_answers: false,
moves: Default::default(),
root_move: MoveIdx::NONE,
resized: false,
}
}
/// The window as a box, so a chain bottoms out in one rather than in a
/// multiplication applied after it. Composing through a box held in
/// pixels leaves everything below it in pixels, which is why nothing
/// downstream has to know the output's size to resolve a position.
fn write_root(&mut self) {
let region = UiRegion::new(
UiSpan::new(Len::ZERO, Len::from_parts(Rel::ZERO, self.output_size.x)),
UiSpan::new(Len::ZERO, Len::from_parts(Rel::ZERO, self.output_size.y)),
);
match self.root_move == MoveIdx::NONE {
true => self.root_move = self.moves.push(MoveIdx::NONE, region),
false => self.moves.set(self.root_move, region),
}
}
/// The window, in whatever the platform measures it in, onto the grid
/// everything below it is decided on.
/// everything below it is decided on. No move entry holds it: a chain
/// bottoms out in `MoveIdx::NONE`, which is the window, and the window's
/// 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>) {
let size = PxVec2::from_f32(size.into());
if size == self.output_size {
return;
}
self.output_size = size;
self.write_root();
self.resized = true;
}
@@ -104,13 +90,13 @@ impl UiRenderState {
layer: 0,
parent: None,
depth: 1,
parent_move: self.root_move,
parent_move: MoveIdx::NONE,
region_node: false,
mask: MaskIdx::NONE,
offer: UiRegion::FULL,
offered_px: self.output_size,
slot_wide: self.moves.compose(self.root_move, UiRegion::FULL),
align: None,
slot_wide: WideRegion::of(UiRegion::FULL),
decided: [false; 2],
}
}
@@ -167,8 +153,6 @@ impl UiRenderState {
#[cfg(feature = "layout-diagnostics")]
let _layout = diag::timer(TimerKind::FullLayout);
self.clear(rsc);
// free all resources & cache
self.write_root();
if let Some(id) = root {
let info = self.root_info();
let region = Self::root_region(id.id(), rsc.widgets());
@@ -203,8 +187,7 @@ impl UiRenderState {
info.region_node,
);
}
let own_align = rsc.widgets().alignment(id);
let align = info.align.unwrap_or(own_align);
let align = rsc.widgets().alignment(id);
let replace_answer = self.answer_invalid.remove(&id)
|| (self.replace_answers
&& (rsc.widgets().needs_redraw.contains(&id)
@@ -219,19 +202,15 @@ impl UiRenderState {
if old.is_none() {
old = self.remove(id, false, rsc);
}
self.draw_at(id, region, info, align, old.take(), rsc)
self.draw_at(id, region, info, old.take(), rsc)
});
let declared = declared_lens(rsc.widgets(), id);
// A near-edge override means the caller already chose this box from
// the child's answer. Applying the answer again would compound the
// placement; it is also how the second, final ask terminates.
let placed = match info.align == Some(RegionAlign::NEAR) {
true => region,
false => placed_box(region, answer.0, align, declared),
};
// The second, final ask is in a box chosen from the answer on both
// axes, which is also what makes it terminate.
let placed = placed_box(region, answer.0, align, declared, info.decided);
let placed_info = DrawInfo {
align: Some(RegionAlign::NEAR),
decided: [true; 2],
..info
};
// The symbolic box can be unchanged while its parent slot changed
@@ -240,7 +219,7 @@ impl UiRenderState {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::PlaceRedraws);
let old = self.remove(id, false, rsc);
self.draw_at(id, placed, placed_info, RegionAlign::NEAR, old, rsc);
self.draw_at(id, placed, placed_info, old, rsc);
}
// The answer is only reusable while both parts of the operation are:
@@ -251,8 +230,11 @@ impl UiRenderState {
let mut settled = answer;
for axis in AXES {
let reported = answer.0.axis(axis);
let placed_len =
match reported.leftover != Weight::ZERO || declared[axis as usize].is_some() {
let placed_len = match fills(
reported,
declared[axis as usize],
info.decided[axis as usize],
) {
true => Len::FULL,
false => Len::from_parts(reported.rel, reported.px),
};
@@ -263,9 +245,8 @@ impl UiRenderState {
let active = self.active.get_mut(&id).unwrap();
active.offer = info.offer;
active.answer = settled;
active.align = align;
active.align_override = info.align.is_some();
active.own_align = own_align;
active.decided = info.decided;
active.own_align = align;
active.depth = info.depth;
settled
}
@@ -276,7 +257,6 @@ impl UiRenderState {
id: WidgetId,
region: UiRegion,
info: DrawInfo,
align: RegionAlign,
old: Option<ActiveData>,
rsc: &mut dyn UiRsc,
) -> (Size, [Holds; 2]) {
@@ -417,7 +397,7 @@ impl UiRenderState {
offer: UiRegion::FULL,
offered_px: px,
slot_wide,
align: None,
decided: [false; 2],
},
rsc,
);
@@ -441,8 +421,7 @@ impl UiRenderState {
children,
size_deps,
declared: declared_lens(rsc.widgets(), id),
align,
align_override: info.align.is_some(),
decided: info.decided,
own_align: rsc.widgets().alignment(id),
move_idx,
parent_move: info.parent_move,
@@ -800,8 +779,7 @@ impl UiRenderState {
size_deps: Vec::new(),
move_idx: info.parent_move,
declared: [None; 2],
align: RegionAlign::default(),
align_override: false,
decided: [false; 2],
own_align: rsc.widgets().alignment(id),
parent_move: info.parent_move,
mask: info.mask,
@@ -820,7 +798,6 @@ impl UiRenderState {
self.answer_invalid.clear();
self.replace_answers = false;
self.moves.clear();
self.root_move = MoveIdx::NONE;
self.layers.clear();
rsc.widgets_mut().needs_redraw.clear();
self.free(rsc);
@@ -977,8 +954,8 @@ impl UiRenderState {
self.px_region(active.parent_move, region),
self.px_region(active.parent_move, asked_in),
);
let parent_must_place =
active.parent.is_some() && (!region_node || active.align_override) && !at_offer;
let decided = active.decided.contains(&true);
let parent_must_place = active.parent.is_some() && (!region_node || decided) && !at_offer;
// An independently positioned region node can redraw at its offer
// and move its slot to its own placement. Every other widget needs
// its parent to reproduce a different final position.
@@ -1000,7 +977,7 @@ impl UiRenderState {
offer: active.offer,
offered_px,
slot_wide: self.moves.compose(active.parent_move, UiRegion::FULL),
align: active.align_override.then_some(active.align),
decided: active.decided,
};
let (was_answer, was) = (active.answer, (active.size, active.holds));
#[cfg(feature = "layout-diagnostics")]
@@ -1032,7 +1009,7 @@ impl UiRenderState {
// the way it did for a region node under a `Stack` once the stack
// stopped overriding every child's alignment.
let placed_info = DrawInfo {
align: Some(RegionAlign::NEAR),
decided: [true; 2],
..info
};
self.draw_inner(id, region, placed_info, None, rsc);
+1 -1
View File
@@ -63,7 +63,7 @@ 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_aligned(&self.inner, region, RegionAlign::NEAR);
painter.widget_decided(&self.inner, region, [true; 2]);
// 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
+4 -1
View File
@@ -119,7 +119,10 @@ impl Widget for Span {
if self.dir.sign == Sign::Neg {
region.flip(axis);
}
let placed = painter.widget_within(child, region);
// 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_decided(child, region, [axis == Axis::X, axis == Axis::Y]);
if shrinks {
let used = placed.len(!axis);
// Choosing between a fixed and a relative length from the
+1 -1
View File
@@ -35,7 +35,7 @@ impl Widget for Stack {
// 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_aligned(child, region, RegionAlign::NEAR),
true => painter.widget_decided(child, region, [true; 2]),
false => painter.widget_within(child, region),
};
}
+47 -9
View File
@@ -20,6 +20,45 @@ fn a_span_gives_each_child_the_width_it_asked_for() {
assert_corners!(h, right, (100, 0), (400, 200));
}
/// A drawn child reports a fraction of the box it was given, and a span
/// offers each child what is left after the one before. So a nested span
/// that takes half of the half it was offered has taken a quarter of the row,
/// and what follows starts three quarters along -- not at the end, which is
/// where adding its report straight into the cursor put it.
#[test]
fn a_span_reads_a_child_report_as_a_fraction_of_what_it_offered() {
let mut h = Harness::new((400, 100));
let half = rect(Color::RED).width(rel(0.5)).add(&mut h.rsc);
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut h.rsc);
let nested = (inner,).span(Dir::RIGHT).add(&mut h.rsc);
let tail = rect(Color::BLUE).width(100).add(&mut h.rsc);
h.set_root((half, nested, tail).span(Dir::RIGHT).width(rel(1.0)));
// The nested span is placed at the length it reported and drawn there
// once more; half of that final box is what its child takes, packed at
// the nested span's own start.
assert_corners!(h, nested, (200, 0), (300, 100));
assert_corners!(h, inner, (200, 0), (250, 100));
assert_corners!(h, tail, (300, 0), (400, 100));
}
/// The same reading through a pad: its inset is the whole box less the
/// padding, so half of the inset plus the padding is half the box plus one
/// padding, not two.
#[test]
fn a_pad_reports_a_fraction_of_its_inset_as_a_fraction_of_its_box() {
let mut h = Harness::new((400, 100));
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut h.rsc);
let padded = (inner,).span(Dir::RIGHT).pad(10).add(&mut h.rsc);
let tail = rect(Color::BLUE).width(100).add(&mut h.rsc);
// Ruled to the window: a root reporting a fraction of it is otherwise
// placed inside it by its own alignment, which is not what is under test.
h.set_root((padded, tail).span(Dir::RIGHT).width(rel(1.0)));
assert_corners!(h, padded, (0, 0), (210, 100));
assert_corners!(h, tail, (210, 0), (310, 100));
}
#[test]
fn a_span_ruled_across_itself_does_not_measure_its_children_there() {
let mut h = Harness::new((400, 200));
@@ -225,21 +264,21 @@ fn only_a_region_node_lengthens_the_chain_and_it_can_be_removed() {
h.set_root((bar, buried).span(Dir::RIGHT));
let move_idx = h.render.active[&leaf.id()].parent_move;
assert_eq!(h.render.moves.depth(move_idx), 1, "only the root region");
assert_eq!(h.render.moves.depth(move_idx), 0, "the window is no entry");
h.rsc.widgets_mut().set_region_node(buried, true);
h.frame();
let move_idx = h.render.active[&leaf.id()].parent_move;
assert_eq!(
h.render.moves.depth(move_idx),
2,
"the opted-in widget's region and the root region"
1,
"the opted-in widget's region alone"
);
h.rsc.widgets_mut().set_region_node(buried, false);
h.frame();
let move_idx = h.render.active[&leaf.id()].parent_move;
assert_eq!(h.render.moves.depth(move_idx), 1);
assert_eq!(h.render.moves.depth(move_idx), 0);
}
/// A span that sizes from its children passes their `leftover` weight up
@@ -341,16 +380,15 @@ fn a_row_of_equal_shares_fills_it_exactly() {
}
}
/// Where the shader puts an edge: the two parts of a scalar are floored
/// apart, so a fraction and a pixel offset snap independently, and each is
/// taken to the boundary it composes to within half a step of. Kept in step
/// with `snap_floor` in `prelude.wgsl`.
/// Where the shader puts an edge: the fraction resolved against the window
/// plus the pixel offset, taken to the boundary it composes to within half
/// a step of. Kept in step with `snap_floor` in `prelude.wgsl`.
fn drawn_edges(h: &Harness, id: WidgetId, axis: Axis) -> (f32, f32) {
let active = &h.render.active[&id];
let region = h.render.moves.resolve(active.parent_move, active.region);
let dim = h.size().axis(axis);
let snap = |v: f32| (v + Px::STEP.to_f32() * 0.5).floor();
let edge = |s: Len| snap(s.rel.to_f32() * dim) + snap(s.px.to_f32());
let edge = |s: Len| snap(s.rel.to_f32() * dim + s.px.to_f32());
let span = region.axis(axis);
(edge(span.start), edge(span.end))
}