Retain layout sizes by pixel axis

This commit is contained in:
iris-ai committed 2026-09-14 17:47:21 -04:00
1 parent 82fa6c1123
commit b1b3eca1c0
11 files changed
+371 -69

No files matched your search

+2
View File
@@ -33,6 +33,7 @@ pub(crate) enum Counter {
SizeReads,
HintHits,
HintMisses,
RetainedSizeHits,
ReuseAttempts,
ReuseExact,
ReuseMoved,
@@ -67,6 +68,7 @@ impl Counter {
"draw-result size reads",
"hint hits",
"hint misses",
"retained size hits",
"reuse attempts",
"reuse exact",
"reuse moved",
+10 -2
View File
@@ -19,8 +19,16 @@ pub struct ActiveData {
pub children: Vec<WidgetId>,
/// The children whose size this widget read while drawing.
pub size_deps: Vec<WidgetId>,
/// Whether it read the output's size, and so is wrong when that changes.
pub reads_output: bool,
/// Offered pixel axes which flowed into this widget's reported size,
/// directly or through a child size it read.
pub size_box_inputs: [bool; 2],
/// Output axes read while producing `size`, distinct from the widget's
/// own box when that box has a fixed pixel length.
pub size_output_inputs: [bool; 2],
/// The output dimensions against which those dependencies were observed.
pub output_px: Vec2,
/// Output axes it read directly or while resolving its offered box.
pub reads_output: [bool; 2],
/// The slot its primitives are positioned through: its own if its parent
/// placed it, otherwise the nearest ancestor that has one.
pub move_idx: MoveIdx,
+87 -6
View File
@@ -23,7 +23,10 @@ pub struct Painter<'a> {
pub(super) children: Vec<WidgetId>,
/// The children whose size this widget read while drawing.
pub(super) size_deps: Vec<WidgetId>,
pub(super) reads_output: bool,
/// Offered pixel axes which can affect the size this draw reports.
pub(super) size_box_inputs: [bool; 2],
pub(super) size_output_inputs: [bool; 2],
pub(super) reads_output: [bool; 2],
/// The slot this widget's primitives are positioned through: its own if
/// its parent placed it, otherwise the nearest ancestor that has one.
pub(super) move_idx: MoveIdx,
@@ -158,7 +161,7 @@ impl<'a> Painter<'a> {
Some(hint) => {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::HintHits);
self.depend_on_size(id);
self.depend_on_size(id, false);
Some(hint)
}
None => {
@@ -169,10 +172,66 @@ impl<'a> Painter<'a> {
}
}
fn depend_on_size<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
/// A retained child length valid under the region it is about to be
/// offered. Unlike a hint, this is contextual: it is kept only when none
/// of the offered pixel axes which produced it changed.
pub fn known_len<W: ?Sized>(
&mut self,
child: &StrongWidget<W>,
axis: Axis,
region: UiRegion,
) -> Option<Len> {
if let Some(hint) = self.size_hint(child, axis) {
return Some(hint);
}
self.retained_size(child, region)
.map(|size| size.axis(axis))
}
fn retained_size<W: ?Sized>(
&mut self,
child: &StrongWidget<W>,
region: UiRegion,
) -> Option<Size> {
let region = region.within(&self.region);
let (size, box_inputs, output_inputs) =
self.state
.retained_size(child.id(), region, self.move_idx, self.rsc.widgets())?;
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::RetainedSizeHits);
self.depend_on_size_inputs(child, box_inputs, output_inputs);
Some(size)
}
fn depend_on_size<W: ?Sized>(&mut self, child: &StrongWidget<W>, inherit_inputs: bool) {
let (box_inputs, output_inputs) = match inherit_inputs {
true => self
.state
.active
.get(&child.id())
.map_or(([false; 2], [false; 2]), |active| {
(active.size_box_inputs, active.size_output_inputs)
}),
false => ([false; 2], [false; 2]),
};
self.depend_on_size_inputs(child, box_inputs, output_inputs);
}
fn depend_on_size_inputs<W: ?Sized>(
&mut self,
child: &StrongWidget<W>,
box_inputs: [bool; 2],
output_inputs: [bool; 2],
) {
if !self.size_deps.contains(&child.id()) {
self.size_deps.push(child.id());
}
for (own, child) in self.size_box_inputs.iter_mut().zip(box_inputs) {
*own |= child;
}
for (own, child) in self.size_output_inputs.iter_mut().zip(output_inputs) {
*own |= child;
}
}
pub fn render_text(
@@ -220,19 +279,41 @@ impl<'a> Painter<'a> {
/// The output's size in pixels. A widget that reads it draws again when
/// the output changes, since nothing else can put that right.
pub fn output_size(&mut self) -> Vec2 {
self.reads_output = true;
self.reads_output = [true; 2];
self.size_output_inputs = [true; 2];
self.state.output_size
}
/// One axis of the output in pixels. Prefer this to [`Self::output_size`]
/// when the other axis cannot affect the size this widget reports.
pub fn output_len(&mut self, axis: Axis) -> f32 {
self.reads_output[axis as usize] = true;
self.size_output_inputs[axis as usize] = true;
self.state.output_size.axis(axis)
}
/// This widget's box in pixels. Resolved against the output's size and
/// the boxes it sits within, so a widget that reads it draws again when
/// the output changes.
pub fn px_size(&mut self) -> Vec2 {
self.reads_output = true;
self.reads_output = [true; 2];
self.size_box_inputs = [true; 2];
let region = self.state.moves.resolve(self.move_idx, self.region);
region.size().to_abs(self.state.output_size)
}
/// One axis of this widget's box in pixels. Prefer this to
/// [`Self::px_size`] when the other axis cannot affect the reported size.
pub fn px_len(&mut self, axis: Axis) -> f32 {
self.reads_output[axis as usize] = true;
self.size_box_inputs[axis as usize] = true;
let region = self.state.moves.resolve(self.move_idx, self.region);
region
.size()
.axis(axis)
.to_abs(self.state.output_size.axis(axis))
}
pub fn text_data(&mut self) -> &mut TextData {
&mut self.rsc.ui_mut().text
}
@@ -270,7 +351,7 @@ impl<W: ?Sized> DrawResult<'_, '_, W> {
diag::bump(Counter::SizeReads);
diag::size_read(self.child.id(), self.painter.id, self.size);
}
self.painter.depend_on_size(self.child);
self.painter.depend_on_size(self.child, true);
self.size
}
+128 -19
View File
@@ -7,6 +7,11 @@ use crate::{
};
const AXES: [Axis; 2] = [Axis::X, Axis::Y];
const LAYOUT_EPSILON_PX: f32 = 0.05;
fn pixel_len_changed(old: f32, new: f32) -> bool {
(old - new).abs() > LAYOUT_EPSILON_PX
}
pub struct UiRenderState {
pub active: HashMap<WidgetId, ActiveData>,
@@ -14,7 +19,14 @@ pub struct UiRenderState {
pub(super) output_size: Vec2,
old_root: Option<WidgetId>,
resized: bool,
resized: [bool; 2],
/// Content/state dirtiness whose retained size cannot answer a layout
/// question until that widget has drawn again.
invalid_sizes: HashSet<WidgetId>,
/// Marks introduced only to traverse resize dependency paths. Unlike
/// content dirtiness, these may retain an answer whose observed pixel
/// axes did not change.
resize_marks: HashSet<WidgetId>,
draw_started: HashSet<WidgetId>,
/// A widget's move slot, which outlives any one `ActiveData`: a redraw
/// replaces that while its children go on pointing at the slot.
@@ -29,7 +41,9 @@ impl UiRenderState {
layers: Default::default(),
output_size: Vec2::ZERO,
old_root: None,
resized: false,
resized: [false; 2],
invalid_sizes: Default::default(),
resize_marks: Default::default(),
draw_started: Default::default(),
slots: Default::default(),
moves: Default::default(),
@@ -38,10 +52,10 @@ impl UiRenderState {
pub fn resize(&mut self, size: impl Into<Vec2>) {
let size = size.into();
if size != self.output_size {
self.output_size = size;
self.resized = true;
for (axis, resized) in AXES.into_iter().zip(self.resized.iter_mut()) {
*resized |= size.axis(axis) != self.output_size.axis(axis);
}
self.output_size = size;
}
pub fn output_size(&self) -> Vec2 {
@@ -53,6 +67,10 @@ impl UiRenderState {
diag::bump(Counter::Updates);
#[cfg(feature = "layout-diagnostics")]
let _update = diag::timer(TimerKind::Update);
self.invalid_sizes.clear();
self.invalid_sizes
.extend(rsc.widgets().needs_redraw.iter().copied());
self.resize_marks.clear();
// safety mechanism for memory leaks; might wanna return a result instead so user can
// decide whether to panic or not
if !rsc.widgets().waiting.is_empty() {
@@ -73,7 +91,7 @@ impl UiRenderState {
if self.root_changed(root) {
self.redraw_all(root, rsc);
self.old_root = root.map(|r| r.id());
} else if self.resized {
} else if self.resized.iter().any(|&resized| resized) {
// A region is a fraction of the output plus an offset, resolved
// against the window in the shader, so a resize moves the whole
// drawing on its own. Only a widget that read pixels can be wrong.
@@ -83,7 +101,19 @@ impl UiRenderState {
let dependents: Vec<_> = self
.active
.iter()
.filter_map(|(&id, active)| active.reads_output.then_some(id))
.filter_map(|(&id, active)| {
AXES.into_iter()
.zip(self.resized)
.any(|(axis, changed)| {
changed
&& active.reads_output[axis as usize]
&& pixel_len_changed(
active.output_px.axis(axis),
self.output_size.axis(axis),
)
})
.then_some(id)
})
.collect();
for id in dependents {
#[cfg(feature = "layout-diagnostics")]
@@ -93,12 +123,21 @@ impl UiRenderState {
rsc.widgets_mut().needs_redraw.insert(top);
}
}
self.resize_marks.extend(
rsc.widgets()
.needs_redraw
.iter()
.filter(|id| !self.invalid_sizes.contains(id))
.copied(),
);
}
}
if rsc.widgets().has_updates() {
self.redraw_updates(rsc);
}
self.resized = false;
self.resized = [false; 2];
self.invalid_sizes.clear();
self.resize_marks.clear();
}
fn redraw_all(&mut self, root: Option<&StrongWidget>, rsc: &mut dyn UiRsc) {
@@ -174,7 +213,9 @@ impl UiRenderState {
primitives: Vec::new(),
children: Vec::new(),
size_deps: Vec::new(),
reads_output: false,
size_box_inputs: [false; 2],
size_output_inputs: [false; 2],
reads_output: [false; 2],
move_idx,
rsc,
};
@@ -199,6 +240,8 @@ impl UiRenderState {
primitives,
children,
size_deps,
size_box_inputs,
size_output_inputs,
reads_output,
move_idx,
layer,
@@ -222,13 +265,15 @@ impl UiRenderState {
primitives,
children,
size_deps,
size_box_inputs,
size_output_inputs,
output_px: self.output_size,
reads_output,
move_idx,
parent_move,
mask,
layer,
};
// remove old children that weren't kept
for c in &old_children {
if !active.children.contains(c) {
@@ -238,6 +283,8 @@ impl UiRenderState {
rsc.on_draw(&active);
self.active.insert(id, active);
self.invalid_sizes.remove(&id);
self.resize_marks.remove(&id);
size
}
@@ -272,6 +319,60 @@ impl UiRenderState {
.to_abs(self.output_size)
}
/// A clean widget's retained size, when the offered pixel axes which
/// produced that answer are unchanged. This observes the old answer only;
/// it does not move or otherwise reuse the widget's drawing.
pub(super) fn retained_size(
&self,
id: WidgetId,
region: UiRegion,
parent_move: MoveIdx,
widgets: &Widgets,
) -> Option<(Size, [bool; 2], [bool; 2])> {
if self.size_is_invalid(id, widgets) || self.dirty_size_under(id, widgets) {
return None;
}
let active = self.active.get(&id)?;
if active.parent_move != parent_move {
return None;
}
let px = self.px_of(parent_move, region);
let valid_box = AXES
.into_iter()
.zip(active.size_box_inputs)
.all(|(axis, depends)| {
!depends || !pixel_len_changed(active.px.axis(axis), px.axis(axis))
});
let valid_output =
AXES.into_iter()
.zip(active.size_output_inputs)
.all(|(axis, depends)| {
!depends
|| !pixel_len_changed(
active.output_px.axis(axis),
self.output_size.axis(axis),
)
});
(valid_box && valid_output).then_some((
active.size,
active.size_box_inputs,
active.size_output_inputs,
))
}
fn size_is_invalid(&self, id: WidgetId, widgets: &Widgets) -> bool {
self.invalid_sizes.contains(&id)
|| (widgets.needs_redraw.contains(&id) && !self.resize_marks.contains(&id))
}
fn dirty_size_under(&self, id: WidgetId, widgets: &Widgets) -> bool {
self.active.get(&id).is_some_and(|active| {
active.size_deps.iter().any(|child| {
self.size_is_invalid(*child, widgets) || self.dirty_size_under(*child, widgets)
})
})
}
/// The drawing a widget already has, kept for a new box if the box has not
/// changed in a way it depends on.
fn try_reuse(
@@ -310,7 +411,7 @@ impl UiRenderState {
let px = self.px_of(parent_move, region);
let mut changed = [false; 2];
for (axis, c) in AXES.into_iter().zip(changed.iter_mut()) {
*c = px.axis(axis) != old_px.axis(axis);
*c = pixel_len_changed(old_px.axis(axis), px.axis(axis));
}
if !changed.iter().any(|&c| c) && old_region == region {
#[cfg(feature = "layout-diagnostics")]
@@ -360,7 +461,6 @@ impl UiRenderState {
self.moves.set(slot, region);
let active = self.active.get_mut(&id).unwrap();
active.region = region;
active.px = px;
#[cfg(feature = "layout-diagnostics")]
{
diag::bump(Counter::ReuseMoved);
@@ -472,6 +572,8 @@ impl UiRenderState {
self.slots.clear();
self.moves.clear();
self.layers.clear();
self.invalid_sizes.clear();
self.resize_marks.clear();
rsc.widgets_mut().needs_redraw.clear();
rsc.free();
}
@@ -486,7 +588,7 @@ impl UiRenderState {
// reader and gives each changing box its final constraints first.
while let Some(id) = {
let dirty = rsc.widgets().needs_redraw.iter().copied();
match self.resized {
match self.resized.iter().any(|&resized| resized) {
true => dirty.min_by_key(|&id| self.depth(id)),
false => dirty.max_by_key(|&id| self.depth(id)),
}
@@ -519,7 +621,9 @@ impl UiRenderState {
root: impl Into<Option<&'a StrongWidget>>,
widgets: &Widgets,
) -> bool {
self.root_changed(root) || self.resized || widgets.has_updates()
self.root_changed(root)
|| self.resized.iter().any(|&resized| resized)
|| widgets.has_updates()
}
pub fn active_widgets(&self) -> usize {
@@ -556,16 +660,20 @@ impl UiRenderState {
/// redraws a widget that's currently active (drawn)
pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) {
self.draw_started.remove(&id);
if rsc.widgets().needs_redraw.contains(&id) && !self.resize_marks.contains(&id) {
self.invalid_sizes.insert(id);
}
// A widget can only answer whether its size changed by drawing in the
// box its parent chose. If that box changed in pixels, its retained
// placement is stale and the highest size reader must choose the new
// box first. Otherwise the widget can draw locally, and its readers
// only matter if the returned size actually changed.
let box_changed = self
.active
.get(&id)
.is_some_and(|active| self.px_of(active.parent_move, active.region) != active.px);
if (self.resized || box_changed)
let box_changed = self.active.get(&id).is_some_and(|active| {
let px = self.px_of(active.parent_move, active.region);
AXES.into_iter()
.any(|axis| pixel_len_changed(active.px.axis(axis), px.axis(axis)))
});
if (self.resized.iter().any(|&resized| resized) || box_changed)
&& let Some(top) = self.mark_readers(id, rsc)
{
#[cfg(feature = "layout-diagnostics")]
@@ -611,6 +719,7 @@ impl UiRenderState {
// Propagate one dependency edge at a time. If drawing the reader
// does not change its own size, nothing above it can observe this.
rsc.widgets_mut().needs_redraw.insert(parent);
self.invalid_sizes.insert(parent);
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::ReaderEdges);
}
+19 -5
View File
@@ -7,17 +7,31 @@ pub struct Aligned {
impl Widget for Aligned {
fn draw(&mut self, painter: &mut Painter) -> Size {
// Drawn where it may be too big, then given its aligned box once its
// size is known.
let size = painter.place(&self.inner, UiRegion::FULL).size();
let known = match self.align.tuple() {
(Some(_), Some(_)) => painter
.known_len(&self.inner, Axis::X, UiRegion::FULL)
.zip(painter.known_len(&self.inner, Axis::Y, UiRegion::FULL))
.map(|(x, y)| Size { x, y }),
(Some(_), None) => painter
.known_len(&self.inner, Axis::X, UiRegion::FULL)
.map(|x| Size { x, y: Len::REST }),
(None, Some(_)) => painter
.known_len(&self.inner, Axis::Y, UiRegion::FULL)
.map(|y| Size { x: Len::REST, y }),
(None, None) => Some(Size::REST),
};
// Drawn where it may be too big only when the aligned axes are not
// already known, then given its aligned box once its size is known.
let had_size = known.is_some();
let size = known.unwrap_or_else(|| painter.place(&self.inner, UiRegion::FULL).size());
let region = match self.align.tuple() {
(Some(x), Some(y)) => size.to_uivec2().align(RegionAlign { x, y }),
(Some(x), None) => UiRegion::new(size.x.apply_rest().align(x), UiSpan::FULL),
(None, Some(y)) => UiRegion::new(UiSpan::FULL, size.y.apply_rest().align(y)),
(None, None) => UiRegion::FULL,
};
painter.place(&self.inner, region);
size
let placed = painter.place(&self.inner, region).size();
if had_size { placed } else { size }
}
/// The aligned box is a fraction of its own, so the child keeps its
+11 -9
View File
@@ -11,13 +11,15 @@ pub struct Scroll {
impl Widget for Scroll {
fn draw(&mut self, painter: &mut Painter) -> Size {
let output_len = painter.output_size().axis(self.axis);
let container_len = UiScalar::abs(painter.px_size().axis(self.axis));
// Draw in the whole container to learn the content's length, then
// place it at the scrolled offset.
let child = painter.place(&self.inner, UiRegion::FULL).size();
let content_len = child
.axis(self.axis)
let output_len = painter.output_len(self.axis);
let container_len = UiScalar::abs(painter.px_len(self.axis));
// Draw in the whole container only when its scrolling-axis length is
// not already known, then place it at the scrolled offset.
let known_len = painter.known_len(&self.inner, self.axis, UiRegion::FULL);
let measured = known_len.is_none();
let child = measured.then(|| painter.place(&self.inner, UiRegion::FULL).size());
let content_len = known_len
.unwrap_or_else(|| child.unwrap().axis(self.axis))
.apply_rest()
.within_len(container_len)
.to_abs(output_len);
@@ -31,8 +33,8 @@ impl Widget for Scroll {
let mut region = UiRegion::FULL.offset(Vec2::from_axis(self.axis, -self.amt, 0.0));
region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len);
painter.place(&self.inner, region);
child
let placed = painter.place(&self.inner, region).size();
child.unwrap_or(placed)
}
}
+7 -9
View File
@@ -15,16 +15,14 @@ impl Widget for Span {
let mut cursor = UiScalar::rel_min();
let mut lens = Vec::with_capacity(self.children.len());
for child in &self.children {
let len = match painter.size_hint(child, axis) {
let mut span = UiSpan::new(cursor, UiScalar::rel_max());
if self.dir.sign == Sign::Neg {
span.flip();
}
let region = UiRegion::from_axis(axis, span, UiSpan::FULL);
let len = match painter.known_len(child, axis, region) {
Some(len) => len,
None => {
let mut span = UiSpan::new(cursor, UiScalar::rel_max());
if self.dir.sign == Sign::Neg {
span.flip();
}
let region = UiRegion::from_axis(axis, span, UiSpan::FULL);
painter.place(child, region).len(axis)
}
None => painter.place(child, region).len(axis),
};
cursor.abs += len.abs + self.gap;
cursor.rel += len.rel;
+1 -1
View File
@@ -54,7 +54,7 @@ impl TextView {
fn render(&mut self, painter: &mut Painter) -> &RenderedText {
let width = if self.attrs.wrap {
Some(painter.px_size().x)
Some(painter.px_len(Axis::X))
} else {
None
};
+14 -17
View File
@@ -17,20 +17,11 @@ use iris::prelude::*;
use iris::random::{Edits, Lens, Rng, SpanEdit, Tree, grow};
const DEPTH: usize = 4;
const SEEDS: [u64; 6] = [1, 2, 3, 5, 8, 13];
const REGION_ULPS: u32 = 4;
fn ordered_bits(value: f32) -> u32 {
const SIGN: u32 = 1 << 31;
let bits = value.to_bits();
match bits & SIGN {
0 => bits | SIGN,
_ => !bits,
}
}
const SEEDS: [u64; 7] = [1, 2, 3, 5, 8, 13, 98];
const REGION_EPSILON_PX: f32 = 0.05;
fn same_coordinate(got: f32, want: f32) -> bool {
got == want || ordered_bits(got).abs_diff(ordered_bits(want)) <= REGION_ULPS
(got - want).abs() <= REGION_EPSILON_PX
}
fn same_region(got: Option<PixelRegion>, want: Option<PixelRegion>) -> bool {
@@ -165,9 +156,10 @@ fn assert_same(seed: u64, what: &str, warm: (&Harness, &Tree), cold: (&Harness,
for (i, (&w, &c)) in wt.ids.iter().zip(&ct.ids).enumerate() {
let (got, want) = (wh.region(&w), ch.region(&c));
drawn += usize::from(got.is_some());
// Equivalent composition orders can differ by a few f32 ULPs. Bound
// that representation drift directly, while whether a widget drew
// remains exact.
// This oracle cares where rasterization lands, not whether equivalent
// arithmetic produced the same f32. Keep the tolerance to one
// twentieth of a physical pixel, while whether a widget drew remains
// exact.
if same_region(got, want) {
continue;
}
@@ -327,9 +319,14 @@ fn adding_and_removing_span_children_lands_where_growing_it_that_way_would() {
/// same defect, and it wants fixing where the two draws meet -- LAYOUT.md §4 --
/// rather than anywhere in the chain.
#[test]
#[ignore = "a hundred seeds, rather than the six the others check"]
#[ignore = "a hundred seeds, rather than the seven the others check"]
fn a_long_run_of_seeds_agrees() {
for seed in 1..=100 {
let seeds = std::env::var("IRIS_GENERATED_SEED")
.ok()
.and_then(|seed| seed.parse().ok())
.map(|seed| seed..=seed)
.unwrap_or(1..=100);
for seed in seeds {
changed_size(seed);
resized(seed);
resized_then_changed(seed);
+22 -1
View File
@@ -75,6 +75,22 @@ fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
.unwrap_or(fallback)
}
#[cfg(feature = "layout-diagnostics")]
fn trace_selected(tree: &Tree) {
let Ok(value) = std::env::var("IRIS_TRACE_INDEX") else {
return;
};
let index = value
.parse::<usize>()
.expect("IRIS_TRACE_INDEX must be a tree.ids index");
let id = tree.ids[index];
iris::core::layout_diagnostics::trace_widget(id);
println!("tracing tree.ids[{index}] = {id:?}");
}
#[cfg(not(feature = "layout-diagnostics"))]
fn trace_selected(_: &Tree) {}
fn warm(seed: u64, depth: usize) -> (Harness, Tree) {
let mut harness = Harness::new(OUTPUT);
let (root, tree) = grow(&mut harness.rsc, seed, depth, &Edits::default());
@@ -157,6 +173,7 @@ fn layout_cost() {
"fixture: seed {seed}, depth {depth}, {} widgets",
tree.ids.len()
);
trace_selected(&tree);
#[cfg(feature = "layout-diagnostics")]
let _ = iris::core::layout_diagnostics::take();
run("cold", 1, &mut harness, |_, _| {});
@@ -165,6 +182,7 @@ fn layout_cost() {
if selected("repaint") {
let (mut harness, tree) = warm(seed, depth);
trace_selected(&tree);
let leaf = tree.ids[0];
run("repaint", frames, &mut harness, move |harness, _| {
let _ = harness.rsc.widgets_mut().get_dyn_mut(leaf);
@@ -173,6 +191,7 @@ fn layout_cost() {
if selected("size") {
let (mut harness, tree) = warm(seed, depth);
trace_selected(&tree);
let sized = tree.sized[0];
run("size", frames, &mut harness, move |harness, frame| {
harness.rsc[sized].x = Some(Len::abs(100.0 + (frame % 2) as f32 * 40.0));
@@ -181,6 +200,7 @@ fn layout_cost() {
if selected("scroll") {
let (mut harness, tree) = warm(seed, depth);
trace_selected(&tree);
let scroll = tree.scrolls[0];
run("scroll", frames, &mut harness, move |harness, frame| {
harness.rsc[scroll].scroll(if frame % 2 == 0 { 12.0 } else { -12.0 });
@@ -189,8 +209,9 @@ fn layout_cost() {
if selected("resize") {
let (mut harness, tree) = warm(seed, depth);
trace_selected(&tree);
run("resize", frames, &mut harness, |harness, frame| {
harness.resize((OUTPUT.0 - (frame % 2) as f32 * 8.0, OUTPUT.1));
harness.resize((OUTPUT.0 - ((frame + 1) % 2) as f32 * 8.0, OUTPUT.1));
});
drop(tree);
}
+70
View File
@@ -191,6 +191,17 @@ impl Widget for ReadsOutput {
}
}
struct ReadsWidth {
draws: Rc<Cell<usize>>,
}
impl Widget for ReadsWidth {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
Size::abs((painter.output_len(Axis::X) / 4.0, 20.0).into())
}
}
#[test]
fn a_resize_does_not_redraw_what_the_shader_can_move() {
let mut h = Harness::new((400, 200));
@@ -227,6 +238,65 @@ fn a_resize_redraws_what_read_the_output() {
assert_eq!(draws.get(), settled + 1);
}
#[test]
fn a_resize_only_redraws_read_output_axes() {
let mut h = Harness::new((400, 200));
let draws = Rc::new(Cell::new(0));
let leaf = ReadsWidth {
draws: draws.clone(),
}
.add(&mut h.rsc);
h.set_root(leaf);
let settled = draws.get();
h.resize((400, 300));
h.frame();
assert_eq!(draws.get(), settled, "height was never read");
h.resize((800, 300));
h.frame();
assert_eq!(draws.get(), settled + 1, "width changes its answer");
}
#[test]
fn subpixel_resize_changes_accumulate_from_the_last_layout() {
let mut h = Harness::new((400, 200));
let draws = Rc::new(Cell::new(0));
let leaf = ReadsWidth {
draws: draws.clone(),
}
.add(&mut h.rsc);
h.set_root(leaf);
let settled = draws.get();
for width in [400.02, 400.04, 400.05] {
h.resize((width, 200.0));
h.frame();
assert_eq!(draws.get(), settled);
}
h.resize((400.06, 200.0));
h.frame();
assert_eq!(draws.get(), settled + 1);
}
#[test]
fn subpixel_box_changes_accumulate_from_the_last_draw() {
let mut h = Harness::new((400, 200));
let (first, draws, _) = pair(&mut h, OnResize::Redraw);
let settled = draws.get();
for width in [100.02, 100.04, 100.05] {
h.rsc[first].size.x = Len::abs(width);
h.frame();
assert_eq!(draws.get(), settled);
}
h.rsc[first].size.x = Len::abs(100.06);
h.frame();
assert_eq!(draws.get(), settled + 1);
}
#[test]
fn reporting_the_same_output_size_does_not_start_a_resize() {
let mut h = Harness::new((400, 200));