A local redraw keeps the narrower guarantee its parent holds when the new drawing covers it, so widening and narrowing back do not churn the parent. It checked that guarantee against `placement`, where the answer put the drawing, rather than `region`, the box the drawing was made in and the box both contracts are about. The two differ on every axis a widget reported less than it was offered, so any such widget escalated to its parent every time its contract widened -- which is the churn the retention exists to avoid. `resize` and `try_reuse` both already ask about `region`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1536 lines
49 KiB
Rust
1536 lines
49 KiB
Rust
//! What a second frame draws again, and what it keeps.
|
|
|
|
use std::{cell::Cell, rc::Rc};
|
|
|
|
use iris::harness::{Harness, assert_corners};
|
|
use iris::prelude::*;
|
|
|
|
/// A leaf that counts its draws and reports whatever size it is given, so a
|
|
/// test can see what the retained path skipped. One that reads its box in
|
|
/// pixels has a drawing that holds for that box alone.
|
|
struct Counted {
|
|
draws: Rc<Cell<usize>>,
|
|
size: Size,
|
|
reads_box: bool,
|
|
}
|
|
|
|
impl Widget for Counted {
|
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
|
self.draws.set(self.draws.get() + 1);
|
|
if self.reads_box {
|
|
painter.px_size();
|
|
}
|
|
self.size
|
|
}
|
|
}
|
|
|
|
struct Counts(Rc<Cell<usize>>);
|
|
|
|
impl Counts {
|
|
fn get(&self) -> usize {
|
|
self.0.get()
|
|
}
|
|
}
|
|
|
|
fn counted(h: &mut Harness, size: Size, reads_box: bool) -> (WeakWidget<Counted>, Counts) {
|
|
let draws = Rc::new(Cell::new(0));
|
|
let id = Counted {
|
|
draws: draws.clone(),
|
|
size,
|
|
reads_box,
|
|
}
|
|
.add(&mut h.rsc);
|
|
(id, Counts(draws))
|
|
}
|
|
|
|
struct Layered {
|
|
children: [StrongWidget<Rect>; 2],
|
|
_revision: usize,
|
|
}
|
|
|
|
impl Widget for Layered {
|
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
|
painter.child_layer();
|
|
painter.widget(&self.children[0]);
|
|
painter.next_layer();
|
|
painter.widget(&self.children[1]);
|
|
Size::default()
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_redrawn_layered_widget_keeps_the_layer_it_was_entered_on() {
|
|
let mut h = Harness::new((400, 200));
|
|
let children = [
|
|
rect(Color::RED).add_strong(&mut h.rsc),
|
|
rect(Color::BLUE).add_strong(&mut h.rsc),
|
|
];
|
|
let root = Layered {
|
|
children,
|
|
_revision: 0,
|
|
}
|
|
.add(&mut h.rsc);
|
|
h.set_root(root);
|
|
|
|
h.rsc[root]._revision += 1;
|
|
h.frame();
|
|
|
|
let label = h.rsc.widgets().label(root.id());
|
|
let active = h
|
|
.render
|
|
.debug(h.rsc.widgets(), label)
|
|
.find(|active| active.id == root.id())
|
|
.unwrap();
|
|
assert_eq!(active.layer, 0);
|
|
}
|
|
|
|
/// A fixed-width leaf beside one that takes what is left over, so changing
|
|
/// the first hands the second a different box without the output changing.
|
|
fn pair(h: &mut Harness, reads_box: bool) -> (WeakWidget<Counted>, Counts, WidgetId) {
|
|
let (first, _) = counted(h, Size::from((100, 200)), false);
|
|
let (second, draws) = counted(h, Size::LEFTOVER, reads_box);
|
|
h.set_root((first, second).span(Dir::RIGHT));
|
|
(first, draws, second.id())
|
|
}
|
|
|
|
#[test]
|
|
fn a_leaf_that_ignores_its_box_is_not_drawn_again_when_the_box_changes() {
|
|
let mut h = Harness::new((400, 200));
|
|
let (first, draws, second) = pair(&mut h, false);
|
|
let settled = draws.get();
|
|
assert_corners!(h, second, (100, 0), (400, 200));
|
|
|
|
h.rsc[first].size = Size::from((150, 200));
|
|
h.frame();
|
|
|
|
assert_eq!(
|
|
draws.get(),
|
|
settled,
|
|
"its box is a field to write, not a reason to draw"
|
|
);
|
|
assert_corners!(h, second, (150, 0), (400, 200));
|
|
}
|
|
|
|
#[test]
|
|
fn moving_an_ordinary_subtree_remaps_its_mask() {
|
|
let mut h = Harness::new((400, 200));
|
|
let (first, _) = counted(&mut h, Size::from((100, 200)), false);
|
|
let inner = rect(Color::BLUE).add(&mut h.rsc);
|
|
let masked = inner.masked().add(&mut h.rsc);
|
|
h.set_root((first, masked).span(Dir::RIGHT));
|
|
|
|
h.rsc[first].size = Size::from((150, 200));
|
|
h.frame();
|
|
|
|
let active = &h.render.active[&masked.id()];
|
|
assert_eq!(
|
|
h.rsc.ui().masks[active.mask.idx()].region,
|
|
UiRegion::new(UiSpan::new(Len::px(150.0), Len::FULL), UiSpan::FULL,)
|
|
);
|
|
assert_corners!(h, inner, (150, 0), (400, 200));
|
|
}
|
|
|
|
#[test]
|
|
fn a_leaf_that_depends_on_its_box_is_drawn_again_when_the_box_changes() {
|
|
let mut h = Harness::new((400, 200));
|
|
let (first, draws, second) = pair(&mut h, true);
|
|
let settled = draws.get();
|
|
|
|
h.rsc[first].size = Size::from((150, 200));
|
|
h.frame();
|
|
|
|
// The preceding fixed child makes the remaining box this child's real
|
|
// box, so measuring it also draws it in its final box.
|
|
assert_eq!(draws.get(), settled + 1);
|
|
assert_corners!(h, second, (150, 0), (400, 200));
|
|
}
|
|
|
|
#[test]
|
|
fn a_span_child_that_declares_its_length_is_drawn_once() {
|
|
let mut h = Harness::new((400, 200));
|
|
let (told, told_draws) = counted(&mut h, Size::from((100, 200)), false);
|
|
let (asked, asked_draws) = counted(&mut h, Size::from((100, 200)), true);
|
|
// The span takes one child's length from its hint and has to draw the
|
|
// other to find out, so only the second is drawn before its final box.
|
|
let hinted = told.width(100).add(&mut h.rsc);
|
|
h.set_root((hinted, asked).span(Dir::RIGHT));
|
|
|
|
assert_eq!(told_draws.get(), 1);
|
|
// Asked once, from the cursor; its slot is its answer and the drawing is
|
|
// moved there.
|
|
assert_eq!(asked_draws.get(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn a_span_relays_out_when_a_child_it_measured_changes() {
|
|
let mut h = Harness::new((400, 200));
|
|
let (first, _, second) = pair(&mut h, false);
|
|
|
|
h.rsc[first].size = Size::from((250, 200));
|
|
h.frame();
|
|
|
|
assert_corners!(h, first, (0, 0), (250, 200));
|
|
assert_corners!(h, second, (250, 0), (400, 200));
|
|
}
|
|
|
|
#[test]
|
|
fn a_repaint_that_keeps_its_size_does_not_relay_out() {
|
|
let mut h = Harness::new((400, 200));
|
|
let (first, draws) = counted(&mut h, Size::from((100, 200)), false);
|
|
let (second, _) = counted(&mut h, Size::LEFTOVER, false);
|
|
h.set_root((first, second).span(Dir::RIGHT));
|
|
let settled = draws.get();
|
|
|
|
// Taking mutable access is the ordinary content-change signal. This
|
|
// widget returns the same size, so the parent has nothing to lay out.
|
|
let _ = h.rsc.widgets_mut().get_dyn_mut(first.id());
|
|
h.frame();
|
|
|
|
assert_eq!(draws.get(), settled + 1);
|
|
}
|
|
|
|
#[test]
|
|
fn a_span_child_survives_the_next_frame() {
|
|
let mut h = Harness::new((400, 200));
|
|
// Both children declare a length, so the span chooses their boxes from
|
|
// hints rather than drawing them to find out.
|
|
let top = rect(Color::RED).height(80).add(&mut h.rsc);
|
|
let bottom = rect(Color::BLUE).height(120).add(&mut h.rsc);
|
|
h.set_root((top, bottom).span(Dir::DOWN));
|
|
|
|
h.rsc.widgets_mut().get_dyn_mut(top.id());
|
|
h.frame();
|
|
|
|
assert_corners!(h, top, (0, 0), (400, 80));
|
|
assert_corners!(h, bottom, (0, 80), (400, 200));
|
|
}
|
|
|
|
/// Lays its child out from the hint alone, never reading what it drew.
|
|
struct FromHint {
|
|
inner: StrongWidget,
|
|
}
|
|
|
|
impl Widget for FromHint {
|
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
|
let len = painter.size_hint(&self.inner, Axis::Y).unwrap();
|
|
let top = UiSpan::new(Len::ZERO, Len::from_parts(Rel::ZERO, len.px));
|
|
painter.widget_at(&self.inner, top.shifted_desc().on_axis(Axis::Y));
|
|
Size::LEFTOVER
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_parent_that_only_read_a_hint_relays_out_when_the_hint_changes() {
|
|
let mut h = Harness::new((400, 200));
|
|
let inner = rect(Color::RED).height(80).add(&mut h.rsc);
|
|
let parent = FromHint {
|
|
inner: inner.add_strong(&mut h.rsc),
|
|
}
|
|
.add(&mut h.rsc);
|
|
h.set_root(parent);
|
|
assert_corners!(h, inner, (0, 0), (400, 80));
|
|
|
|
h.set_len(inner, Axis::Y, 120);
|
|
h.frame();
|
|
|
|
assert_corners!(h, inner, (0, 0), (400, 120));
|
|
}
|
|
|
|
/// Reads its box's size, which nothing but its own draw can put right.
|
|
struct ReadsBox {
|
|
draws: Rc<Cell<usize>>,
|
|
}
|
|
|
|
impl Widget for ReadsBox {
|
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
|
self.draws.set(self.draws.get() + 1);
|
|
Size::from_px(painter.px_size().div_int(4))
|
|
}
|
|
}
|
|
|
|
/// Reads its box across one axis only, so its drawing holds for a taller
|
|
/// box on its own and only a wider one is worth a draw.
|
|
///
|
|
/// Both of these report a quarter of what they read. The quarter-sized box
|
|
/// the answer places them in is not a question: the drawing is moved there,
|
|
/// so each length they are asked at costs one draw.
|
|
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::from_px(PxVec2::new(
|
|
painter.px_len(Axis::X).div_int(4),
|
|
Px::from_int(20),
|
|
))
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_resize_does_not_redraw_what_the_shader_can_move() {
|
|
let mut h = Harness::new((400, 200));
|
|
let (leaf, draws) = counted(&mut h, Size::LEFTOVER, false);
|
|
h.set_root(leaf);
|
|
let settled = draws.get();
|
|
|
|
h.resize((800, 100));
|
|
assert!(h.needs_redraw());
|
|
h.frame();
|
|
|
|
assert_eq!(
|
|
draws.get(),
|
|
settled,
|
|
"a scaling drawing follows its box, and the output is one"
|
|
);
|
|
assert_corners!(h, leaf, (0, 0), (800, 100));
|
|
}
|
|
|
|
#[test]
|
|
fn a_span_ruled_across_itself_moves_its_child_without_redrawing_it() {
|
|
let mut h = Harness::new((400, 200));
|
|
let (leaf, draws) = counted(&mut h, Size::LEFTOVER, false);
|
|
let span = (leaf,).span(Dir::RIGHT).height(rel(1.0)).add(&mut h.rsc);
|
|
h.set_root(span);
|
|
let settled = draws.get();
|
|
|
|
h.resize((400, 100));
|
|
h.frame();
|
|
|
|
assert_eq!(draws.get(), settled);
|
|
assert_corners!(h, leaf, (0, 0), (400, 100));
|
|
assert_eq!(h.render.active[&span.id()].size.y, LayoutLen::rel(1.0));
|
|
}
|
|
|
|
/// A row places its children as lengths from where its own box starts, so a
|
|
/// child that grew moves the ones after it and nothing else: each of them is
|
|
/// the same box in a new place, which the retained drawing follows without
|
|
/// being made again. Both kinds of length: one the row resolves from a rule,
|
|
/// and one it takes from what the child reported.
|
|
#[test]
|
|
fn a_row_moves_what_follows_a_child_that_grew_rather_than_drawing_it() {
|
|
for declared in [false, true] {
|
|
let mut h = Harness::new((400, 200));
|
|
let first = rect(Color::RED).width(50).add(&mut h.rsc);
|
|
let ruled = Rc::new(Cell::new(0));
|
|
let second = Counted {
|
|
draws: ruled.clone(),
|
|
size: Size::LEFTOVER,
|
|
reads_box: false,
|
|
};
|
|
let second = match declared {
|
|
true => second.width(rel(0.25)).add(&mut h.rsc),
|
|
false => second.width(60).add(&mut h.rsc),
|
|
};
|
|
let (third, reported) = counted(&mut h, Size::from((70, 20)), false);
|
|
h.set_root((first, second, third).span(Dir::RIGHT).width(rel(1.0)));
|
|
let (was_ruled, was_reported) = (ruled.get(), reported.get());
|
|
// A quarter of the row is a quarter of the row, wherever it sits in
|
|
// it and whatever the first child takes.
|
|
let width = match declared {
|
|
true => 100,
|
|
false => 60,
|
|
};
|
|
assert_corners!(h, second, (50, 0), (50 + width, 200));
|
|
|
|
h.set_len(first, Axis::X, 80);
|
|
h.frame();
|
|
|
|
assert_eq!(ruled.get(), was_ruled, "the ruled child was drawn again");
|
|
assert_eq!(
|
|
reported.get(),
|
|
was_reported,
|
|
"the reported child was drawn again"
|
|
);
|
|
assert_corners!(h, second, (80, 0), (80 + width, 200));
|
|
assert_corners!(h, third, (80 + width, 90), (150 + width, 110));
|
|
}
|
|
}
|
|
|
|
/// The output is the root of the box chain, so a resize is a box that changed
|
|
/// length like any other -- there is not a second rule for the window. A
|
|
/// drawing that holds for one length is drawn again whichever box moved.
|
|
#[test]
|
|
fn a_resize_redraws_what_does_not_scale() {
|
|
let mut h = Harness::new((400, 200));
|
|
let (leaf, draws) = counted(&mut h, Size::LEFTOVER, true);
|
|
h.set_root(leaf);
|
|
let settled = draws.get();
|
|
|
|
h.resize((800, 100));
|
|
h.frame();
|
|
|
|
assert_eq!(draws.get(), settled + 1, "its box is a different length");
|
|
assert_corners!(h, leaf, (0, 0), (800, 100));
|
|
}
|
|
|
|
#[test]
|
|
fn a_resize_redraws_what_read_its_box() {
|
|
let mut h = Harness::new((400, 200));
|
|
let draws = Rc::new(Cell::new(0));
|
|
let leaf = ReadsBox {
|
|
draws: draws.clone(),
|
|
}
|
|
.add(&mut h.rsc);
|
|
h.set_root(leaf);
|
|
let settled = draws.get();
|
|
|
|
h.resize((800, 100));
|
|
h.frame();
|
|
|
|
assert_eq!(draws.get(), settled + 1);
|
|
}
|
|
|
|
#[test]
|
|
fn a_resize_only_redraws_read_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");
|
|
}
|
|
|
|
/// A window is measured onto the grid like everything else, so a resize too
|
|
/// small to reach the next step is not a resize at all -- and one that does
|
|
/// reach it is, however little of a pixel it is worth.
|
|
#[test]
|
|
fn a_resize_within_one_step_is_not_a_resize() {
|
|
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();
|
|
|
|
// All of these are 400 px to the nearest step.
|
|
let step = Px::STEP.to_f32();
|
|
for part in [0.1, 0.2, 0.3] {
|
|
h.resize((400.0 + step * part, 200.0));
|
|
h.frame();
|
|
assert_eq!(draws.get(), settled);
|
|
}
|
|
|
|
h.resize((400.0 + step, 200.0));
|
|
h.frame();
|
|
assert_eq!(draws.get(), settled + 1);
|
|
}
|
|
|
|
/// The same for a box that changes because a sibling did: what is compared
|
|
/// is the length on the grid, and three lengths that land on one step are
|
|
/// one length.
|
|
#[test]
|
|
fn a_box_change_within_one_step_is_not_a_change() {
|
|
let mut h = Harness::new((400, 200));
|
|
let (first, draws, _) = pair(&mut h, true);
|
|
let settled = draws.get();
|
|
|
|
let step = Px::STEP.to_f32();
|
|
for part in [0.1, 0.2, 0.3] {
|
|
h.rsc[first].size.x = LayoutLen::px(100.0 + step * part);
|
|
h.frame();
|
|
assert_eq!(draws.get(), settled);
|
|
}
|
|
|
|
h.rsc[first].size.x = LayoutLen::px(100.0 + step);
|
|
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));
|
|
let draws = Rc::new(Cell::new(0));
|
|
let leaf = ReadsBox {
|
|
draws: draws.clone(),
|
|
}
|
|
.add(&mut h.rsc);
|
|
h.set_root(leaf);
|
|
let settled = draws.get();
|
|
|
|
h.resize((400, 200));
|
|
|
|
assert!(!h.needs_redraw());
|
|
h.frame();
|
|
assert_eq!(draws.get(), settled);
|
|
}
|
|
|
|
#[test]
|
|
fn narrowing_the_output_reflows_text_and_relays_out_around_it() {
|
|
let mut h = Harness::new((600, 400));
|
|
let para = wtext(
|
|
"Wrapping shapes one source into as many lines as its container leaves \
|
|
room for, so the height of a paragraph is an answer rather than a setting.",
|
|
)
|
|
.size(20)
|
|
.wrap(true)
|
|
.add(&mut h.rsc);
|
|
let below = rect(Color::RED).add(&mut h.rsc);
|
|
h.set_root((para, below).span(Dir::DOWN));
|
|
let top = h.region(&below).expect("drew nothing").top_left.y;
|
|
|
|
h.resize((300, 400));
|
|
h.frame();
|
|
|
|
let lower = h.region(&below).expect("drew nothing").top_left.y;
|
|
assert!(lower > top, "same words, half the width: {top} -> {lower}");
|
|
}
|
|
|
|
#[test]
|
|
fn a_change_two_levels_under_its_reader_still_reaches_it() {
|
|
let mut h = Harness::new((400, 400));
|
|
// Every wrapper up to the outer pad read the size below it, so the outer
|
|
// pad is what draws again -- and the span it hands the box to is the same
|
|
// size as before, which is what lets a draw reuse its way past the leaf.
|
|
let (leaf, _) = counted(&mut h, Size::px((100, 100).into()), true);
|
|
let padded = leaf.pad(10).add(&mut h.rsc);
|
|
let below = rect(Color::RED).add(&mut h.rsc);
|
|
h.set_root((padded, below).span(Dir::DOWN).pad(12));
|
|
assert_corners!(h, below, (12, 132), (388, 388));
|
|
|
|
h.rsc[leaf].size = Size::px((100, 200).into());
|
|
h.frame();
|
|
|
|
assert_corners!(h, below, (12, 232), (388, 388));
|
|
}
|
|
|
|
/// Reads nothing of its box, so its drawing holds for any length, and has a
|
|
/// child so that whatever asks about the subtree has one to reach.
|
|
struct Stretchy {
|
|
inner: StrongWidget,
|
|
draws: Rc<Cell<usize>>,
|
|
}
|
|
|
|
impl Widget for Stretchy {
|
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
|
self.draws.set(self.draws.get() + 1);
|
|
painter.widget(&self.inner).size()
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn stretching_a_subtree_carries_the_children_in_it() {
|
|
let mut h = Harness::new((400, 400));
|
|
let first = rect(Color::RED).height(40).add(&mut h.rsc);
|
|
let inner = rect(Color::BLUE).add(&mut h.rsc);
|
|
let draws = Rc::new(Cell::new(0));
|
|
let outer = Stretchy {
|
|
inner: inner.add_strong(&mut h.rsc),
|
|
draws: draws.clone(),
|
|
}
|
|
.add(&mut h.rsc);
|
|
h.set_root((first, outer).span(Dir::DOWN));
|
|
let settled = draws.get();
|
|
assert_corners!(h, inner, (0, 40), (400, 400));
|
|
|
|
h.set_len(first, Axis::Y, 80);
|
|
h.frame();
|
|
|
|
assert_eq!(
|
|
draws.get(),
|
|
settled,
|
|
"its drawing follows its box, rather than being made again"
|
|
);
|
|
assert_corners!(h, outer, (0, 80), (400, 400));
|
|
assert_corners!(h, inner, (0, 80), (400, 400));
|
|
}
|
|
|
|
#[test]
|
|
fn a_widened_row_redraws_what_reads_its_length_and_nothing_else() {
|
|
let mut h = Harness::new((400, 200));
|
|
// What a transcript row is: something whose shaping depends on the width
|
|
// it is given, beside something that only has to be the right shape.
|
|
let (wraps, wrap_draws) = counted(&mut h, Size::LEFTOVER, true);
|
|
let (backing, back_draws) = counted(&mut h, Size::LEFTOVER, false);
|
|
let row = (backing, wraps).span(Dir::RIGHT).add(&mut h.rsc);
|
|
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
|
|
h.set_root((bar, row).span(Dir::RIGHT));
|
|
let (settled_wrap, settled_back) = (wrap_draws.get(), back_draws.get());
|
|
|
|
h.set_len(bar, Axis::X, 200);
|
|
h.frame();
|
|
|
|
// The span reads every child's size, so redrawing one takes the span
|
|
// with it -- and the span then measures and places the redrawn child.
|
|
assert!(wrap_draws.get() > settled_wrap, "reads the width it got");
|
|
assert_eq!(back_draws.get(), settled_back, "only has to be the shape");
|
|
assert_corners!(h, backing, (200, 0), (300, 200));
|
|
assert_corners!(h, wraps, (300, 0), (400, 200));
|
|
}
|
|
|
|
#[test]
|
|
fn a_declared_length_child_is_not_redrawn_when_the_box_around_it_grows() {
|
|
let mut h = Harness::new((400, 200));
|
|
// Its box is a fixed 80 wherever the row's edges end up, so drawing it
|
|
// again would be for a width it does not have. The declared width is what
|
|
// lets the span say that without drawing it: a width the span learnt by
|
|
// drawing the child in its own box is only an answer for that box.
|
|
let (counter, draws) = counted(&mut h, Size::from((80, 200)), true);
|
|
let fixed = counter.width(80).add(&mut h.rsc);
|
|
let (leftover, _) = counted(&mut h, Size::LEFTOVER, false);
|
|
let row = (fixed, leftover).span(Dir::RIGHT).add(&mut h.rsc);
|
|
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
|
|
h.set_root((bar, row).span(Dir::RIGHT));
|
|
let settled = draws.get();
|
|
|
|
h.set_len(bar, Axis::X, 200);
|
|
h.frame();
|
|
|
|
assert_eq!(draws.get(), settled, "its own length did not change");
|
|
assert_corners!(h, fixed, (200, 0), (280, 200));
|
|
}
|
|
|
|
/// A retained drawing belongs to the layer it was made on: asked for again
|
|
/// on another one it has to be drawn there, since nothing about its geometry
|
|
/// says it is in a list that paints at a different moment.
|
|
#[test]
|
|
fn a_widget_asked_again_on_another_layer_is_drawn_there() {
|
|
/// Draws its child on its own layer, then again one layer in -- which is
|
|
/// what a container measuring a child by drawing it used to do.
|
|
struct Twice(StrongWidget);
|
|
|
|
impl Widget for Twice {
|
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
|
let size = painter.widget(&self.0).size();
|
|
painter.child_layer();
|
|
painter.widget(&self.0);
|
|
size
|
|
}
|
|
}
|
|
|
|
let mut h = Harness::new((400, 200));
|
|
let (front, draws) = counted(&mut h, Size::from((100, 50)), false);
|
|
let outer = Twice(front.add_strong(&mut h.rsc)).add(&mut h.rsc);
|
|
h.set_root(outer);
|
|
h.frame();
|
|
|
|
assert_ne!(
|
|
h.render.active[&front.id()].layer,
|
|
h.render.active[&outer.id()].layer,
|
|
"the first drawing was kept, on the layer it was measured on"
|
|
);
|
|
assert_eq!(draws.get(), 2, "the second ask could not reuse the first");
|
|
}
|
|
|
|
/// Which is why `Stack` measures the child that sizes it on the layer that
|
|
/// child draws on: one drawing, above the background it stacks over, rather
|
|
/// than one on each layer and the wrong one kept.
|
|
#[test]
|
|
fn a_stacks_sizing_child_is_drawn_once_where_it_belongs() {
|
|
let mut h = Harness::new((400, 200));
|
|
let background = rect(Color::RED).add(&mut h.rsc);
|
|
let (front, draws) = counted(&mut h, Size::from((100, 50)), false);
|
|
let stack = Stack {
|
|
children: vec![
|
|
background.add_strong(&mut h.rsc),
|
|
front.add_strong(&mut h.rsc),
|
|
],
|
|
size: StackSize::Child(1),
|
|
}
|
|
.add(&mut h.rsc);
|
|
h.set_root(stack);
|
|
h.frame();
|
|
|
|
let layer = |id| h.render.active[&id].layer;
|
|
assert_ne!(layer(front.id()), layer(stack.id()));
|
|
assert_ne!(layer(front.id()), layer(background.id()));
|
|
assert_eq!(draws.get(), 1);
|
|
}
|
|
|
|
/// A widget's own mask is not the one it inherited, and a redraw of it
|
|
/// inherits the second: handing back the first is handing it its own mask to
|
|
/// set a second time, which `set_mask` asserts against.
|
|
#[test]
|
|
fn a_masked_widget_redrawn_on_its_own_sets_its_mask_again() {
|
|
let mut h = Harness::new((400, 200));
|
|
let inner = rect(Color::BLUE).add(&mut h.rsc);
|
|
let masked = inner.masked().add(&mut h.rsc);
|
|
let other = rect(Color::RED).width(100).add(&mut h.rsc);
|
|
h.set_root((other, masked).span(Dir::RIGHT));
|
|
h.rsc.widgets_mut().get_dyn_mut(masked.id());
|
|
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 changing_an_inherited_region_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_at(
|
|
&self.child,
|
|
PlaceDesc::new(self.region.x.shifted_desc(), self.region.y.shifted_desc()),
|
|
);
|
|
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(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 glyph_origins_compose_identically_when_drawn_and_when_retained() {
|
|
struct Glyphs {
|
|
buffer: TextBuffer,
|
|
draws: Rc<Cell<usize>>,
|
|
}
|
|
impl Widget for Glyphs {
|
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
|
self.draws.set(self.draws.get() + 1);
|
|
let text = painter.render_text(&mut self.buffer, &TextAttrs::default(), None);
|
|
let origin = UiRegion::new(
|
|
UiSpan::new(Len::rel(0.23) + Len::px(-7.125), Len::FULL),
|
|
UiSpan::new(Len::rel(0.37) + Len::px(3.25), Len::FULL),
|
|
);
|
|
painter.glyphs(text, origin);
|
|
Size::LEFTOVER
|
|
}
|
|
}
|
|
struct Frame {
|
|
child: StrongWidget,
|
|
frame: UiRegion,
|
|
region: UiRegion,
|
|
}
|
|
impl Widget for Frame {
|
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
|
painter.widget_at(
|
|
&self.child,
|
|
PlaceDesc::new(
|
|
self.region.x.shifted_desc().fills(),
|
|
self.region.y.shifted_desc().fills(),
|
|
)
|
|
.rel_base(Axis::X, self.frame.x.len()),
|
|
);
|
|
Size::LEFTOVER
|
|
}
|
|
}
|
|
for node in [false, true] {
|
|
let mut h = Harness::new((403, 211));
|
|
let draws = Rc::new(Cell::new(0));
|
|
let text = Glyphs {
|
|
buffer: TextBuffer::new("Glyphs: gj AV\nsecond line"),
|
|
draws: draws.clone(),
|
|
}
|
|
.add(&mut h.rsc);
|
|
h.rsc.widgets_mut().set_region_node(text, node);
|
|
let root = Frame {
|
|
child: text.add_strong(&mut h.rsc),
|
|
frame: UiRegion::FULL,
|
|
region: UiRegion::FULL,
|
|
}
|
|
.add(&mut h.rsc);
|
|
h.set_root(root);
|
|
for (start, end) in [(0.13, 0.83), (-0.17, 1.23), (0.31, 0.67)] {
|
|
let before = draws.get();
|
|
h.rsc[root].frame.x = UiSpan::new(Len::px(13.125), Len::px(287.375));
|
|
h.rsc[root].region = UiRegion::new(
|
|
UiSpan::new(Len::rel(start), Len::rel(end)),
|
|
UiSpan::new(Len::px(7.25), Len::rel(end)),
|
|
);
|
|
h.frame();
|
|
assert_eq!(draws.get(), before);
|
|
let retained = primitive_bounds(&h, text.id());
|
|
assert!(!retained.is_empty());
|
|
let _ = h.rsc.widgets_mut().get_dyn_mut(text.id());
|
|
h.frame();
|
|
assert!(draws.get() > before);
|
|
assert_eq!(retained, primitive_bounds(&h, text.id()));
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn resizing_does_not_remeasure_a_fixed_stack_for_its_unmeasured_overlay() {
|
|
let mut h = Harness::new((400, 200));
|
|
let (sizing, _) = counted(&mut h, Size::from((100, 80)), false);
|
|
let (overlay, draws) = counted(&mut h, Size::LEFTOVER, true);
|
|
h.set_root((sizing, overlay).stack().size(StackSize::Child(0)));
|
|
let settled = draws.get();
|
|
|
|
h.resize((800, 300));
|
|
h.frame();
|
|
|
|
assert_eq!(draws.get(), settled);
|
|
assert_corners!(h, overlay, (350, 110), (450, 190));
|
|
}
|
|
|
|
struct Unmeasured {
|
|
child: StrongWidget,
|
|
draws: Rc<Cell<usize>>,
|
|
}
|
|
|
|
impl Widget for Unmeasured {
|
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
|
self.draws.set(self.draws.get() + 1);
|
|
painter.widget(&self.child);
|
|
Size::LEFTOVER
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_declared_size_change_stops_at_an_independent_parent() {
|
|
let mut h = Harness::new((400, 200));
|
|
let leaf = rect(Color::RED).width(100).add(&mut h.rsc);
|
|
let parent = Unmeasured {
|
|
child: leaf.add_strong(&mut h.rsc),
|
|
draws: Rc::new(Cell::new(0)),
|
|
}
|
|
.add_strong(&mut h.rsc);
|
|
let draws = Rc::new(Cell::new(0));
|
|
h.set_root(Unmeasured {
|
|
child: parent,
|
|
draws: draws.clone(),
|
|
});
|
|
let settled = draws.get();
|
|
|
|
h.set_len(leaf, Axis::X, 150);
|
|
h.frame();
|
|
|
|
assert_corners!(h, leaf, (125, 0), (275, 200));
|
|
assert_eq!(draws.get(), settled);
|
|
}
|
|
|
|
#[test]
|
|
fn an_unmeasured_child_still_invalidates_its_parents_drawing_on_resize() {
|
|
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,).stack());
|
|
let settled = draws.get();
|
|
|
|
h.resize((800, 200));
|
|
h.frame();
|
|
|
|
assert!(draws.get() > settled);
|
|
assert_corners!(h, leaf, (300, 90), (500, 110));
|
|
}
|
|
|
|
#[test]
|
|
fn changed_drawing_dependencies_reach_ancestors_without_a_size_change() {
|
|
let mut h = Harness::new((400, 200));
|
|
let (leaf, draws) = counted(&mut h, Size::LEFTOVER, false);
|
|
h.set_root(((leaf,).stack(),).stack());
|
|
|
|
h.rsc[leaf].reads_box = true;
|
|
h.frame();
|
|
let settled = draws.get();
|
|
h.resize((800, 200));
|
|
h.frame();
|
|
|
|
assert_eq!(draws.get(), settled + 1);
|
|
assert_corners!(h, leaf, (0, 0), (800, 200));
|
|
}
|
|
|
|
#[test]
|
|
fn widening_and_restoring_a_contract_does_not_invalidate_its_reader() {
|
|
let mut h = Harness::new((400, 200));
|
|
let (leaf, leaf_draws) = counted(&mut h, Size::LEFTOVER, true);
|
|
let draws = Rc::new(Cell::new(0));
|
|
let child = leaf.add_strong(&mut h.rsc);
|
|
h.set_root(Unmeasured {
|
|
child,
|
|
draws: draws.clone(),
|
|
});
|
|
let settled = draws.get();
|
|
for reads_box in [false, true, false, true] {
|
|
h.rsc[leaf].reads_box = reads_box;
|
|
h.frame();
|
|
assert_eq!(draws.get(), settled);
|
|
}
|
|
let settled = leaf_draws.get();
|
|
h.resize((800, 200));
|
|
h.frame();
|
|
assert_eq!(leaf_draws.get(), settled + 1);
|
|
}
|
|
#[test]
|
|
fn padding_and_stack_boxes_follow_the_region_without_drawing_again() {
|
|
struct Observed<W> {
|
|
widget: W,
|
|
draws: Rc<Cell<usize>>,
|
|
}
|
|
impl<W: Widget> Widget for Observed<W> {
|
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
|
self.draws.set(self.draws.get() + 1);
|
|
self.widget.draw(painter)
|
|
}
|
|
}
|
|
struct Frame {
|
|
child: StrongWidget,
|
|
region: UiRegion,
|
|
}
|
|
impl Widget for Frame {
|
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
|
painter.widget_at(
|
|
&self.child,
|
|
PlaceDesc::new(
|
|
self.region.x.shifted_desc().fills(),
|
|
self.region.y.shifted_desc().fills(),
|
|
),
|
|
);
|
|
Size::LEFTOVER
|
|
}
|
|
}
|
|
for node in [false, true] {
|
|
let plant = |h: &mut Harness, region| {
|
|
let draws = Rc::new(Cell::new(0));
|
|
let leaf = rect(Color::BLUE).masked().add(&mut h.rsc);
|
|
h.rsc.widgets_mut().set_region_node(leaf, node);
|
|
let fixed = rect(Color::RED).width(31).height(19).add(&mut h.rsc);
|
|
let stack = Observed {
|
|
widget: Stack {
|
|
children: vec![leaf.add_strong(&mut h.rsc), fixed.add_strong(&mut h.rsc)],
|
|
size: StackSize::Default,
|
|
},
|
|
draws: draws.clone(),
|
|
}
|
|
.add_strong(&mut h.rsc);
|
|
let pad = Observed {
|
|
widget: Pad {
|
|
inner: stack,
|
|
padding: Padding::uniform(7).with_left(13),
|
|
},
|
|
draws: draws.clone(),
|
|
}
|
|
.add_strong(&mut h.rsc);
|
|
let root = Frame { child: pad, region }.add(&mut h.rsc);
|
|
h.set_root(root);
|
|
(root, leaf, fixed, draws)
|
|
};
|
|
// The same box in three places. A pad places its child as lengths of
|
|
// its own box measured from where that box starts, so moving it is
|
|
// nothing to the pad -- where changing its length is a different
|
|
// question, and does draw it again.
|
|
let at = |start: f32| {
|
|
let span = |start: Len| UiSpan::new(start, start + Len::rel(0.4));
|
|
UiRegion::new(span(Len::rel(start) + Len::px(3.125)), span(Len::px(11.25)))
|
|
};
|
|
let mut warm = Harness::new((403, 211));
|
|
let (root, leaf, fixed, draws) = plant(&mut warm, at(0.13));
|
|
for start in [0.13, -0.17, 0.31] {
|
|
let region = at(start);
|
|
let before = draws.get();
|
|
warm.rsc[root].region = region;
|
|
warm.frame();
|
|
assert_eq!(draws.get(), before);
|
|
let mut cold = Harness::new((403, 211));
|
|
let (_, other, other_fixed, _) = plant(&mut cold, region);
|
|
for (a, b) in [(leaf.id(), other.id()), (fixed.id(), other_fixed.id())] {
|
|
assert_eq!(warm.region(&a), cold.region(&b));
|
|
assert_eq!(primitive_bounds(&warm, a), primitive_bounds(&cold, b));
|
|
}
|
|
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 moving_a_childs_region_preserves_the_slot_chosen_from_its_measurement() {
|
|
struct Measured;
|
|
impl Widget for Measured {
|
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
|
let width = painter.px_len(Axis::X);
|
|
painter.primitive(RectPrimitive::color(Color::BLUE));
|
|
Size::from((80, if width > Px::from_int(100) { 40 } else { 60 }))
|
|
}
|
|
}
|
|
struct Frame {
|
|
child: StrongWidget,
|
|
start: f32,
|
|
}
|
|
impl Widget for Frame {
|
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
|
painter.widget_at(
|
|
&self.child,
|
|
PlaceDesc::new(
|
|
UiSpan::new(Len::px(self.start), Len::px(self.start + 200.0))
|
|
.shifted_desc()
|
|
.fills(),
|
|
UiSpan::FULL.shifted_desc().fills(),
|
|
),
|
|
);
|
|
Size::LEFTOVER
|
|
}
|
|
}
|
|
let mut h = Harness::new((400, 200));
|
|
let leaf = Measured.add(&mut h.rsc);
|
|
let stack = (leaf,).stack().add_strong(&mut h.rsc);
|
|
let root = Frame {
|
|
child: stack,
|
|
start: 0.0,
|
|
}
|
|
.add(&mut h.rsc);
|
|
h.set_root(root);
|
|
assert_corners!(h, leaf, (60, 80), (140, 120));
|
|
h.rsc[root].start = 30.0;
|
|
h.frame();
|
|
assert_corners!(h, leaf, (90, 80), (170, 120));
|
|
assert_eq!(
|
|
primitive_bounds(&h, leaf.id()),
|
|
vec![h.region(&leaf).unwrap()]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn changing_regions_keep_fractional_reports_and_numeric_dependencies_valid() {
|
|
struct Container {
|
|
child: StrongWidget,
|
|
region: UiRegion,
|
|
}
|
|
impl Widget for Container {
|
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
|
painter
|
|
.widget_at(
|
|
&self.child,
|
|
PlaceDesc::new(self.region.x.shifted_desc(), self.region.y.shifted_desc()),
|
|
)
|
|
.size()
|
|
}
|
|
}
|
|
struct Frame {
|
|
child: StrongWidget,
|
|
region: UiRegion,
|
|
answer: Rc<Cell<Size>>,
|
|
}
|
|
impl Widget for Frame {
|
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
|
self.answer.set(
|
|
painter
|
|
.widget_at(
|
|
&self.child,
|
|
PlaceDesc::new(
|
|
self.region.x.shifted_desc().fills(),
|
|
self.region.y.shifted_desc().fills(),
|
|
),
|
|
)
|
|
.size(),
|
|
);
|
|
Size::LEFTOVER
|
|
}
|
|
}
|
|
for fractional in [false, true] {
|
|
for region in [
|
|
UiRegion::FULL,
|
|
UiRegion::new(UiSpan::new(Len::rel(0.13), Len::rel(0.79)), UiSpan::FULL),
|
|
] {
|
|
let plant = |h: &mut Harness, outer| {
|
|
let size = if fractional {
|
|
Size {
|
|
x: rel(0.5),
|
|
y: LayoutLen::px(27),
|
|
}
|
|
} else {
|
|
Size::from((80, 27))
|
|
};
|
|
let (leaf, _) = counted(h, size, !fractional);
|
|
let child = Container {
|
|
child: leaf.add_strong(&mut h.rsc),
|
|
region,
|
|
}
|
|
.add_strong(&mut h.rsc);
|
|
let answer = Rc::new(Cell::new(Size::ZERO));
|
|
let root = Frame {
|
|
child,
|
|
region: outer,
|
|
answer: answer.clone(),
|
|
}
|
|
.add(&mut h.rsc);
|
|
h.set_root(root);
|
|
(root, leaf, answer)
|
|
};
|
|
let mut warm = Harness::new((403, 211));
|
|
let (root, leaf, answer) = plant(&mut warm, UiRegion::FULL);
|
|
for width in [191.125, 297.25, 83.75] {
|
|
let region =
|
|
UiRegion::new(UiSpan::new(Len::px(13.125), Len::px(width)), UiSpan::FULL);
|
|
warm.rsc[root].region = region;
|
|
warm.frame();
|
|
let mut cold = Harness::new((403, 211));
|
|
let (_, other, other_answer) = plant(&mut cold, region);
|
|
assert_eq!(answer.get(), other_answer.get());
|
|
assert_eq!(warm.region(&leaf), cold.region(&other));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct OptionalMask {
|
|
inner: StrongWidget,
|
|
enabled: bool,
|
|
}
|
|
|
|
impl Widget for OptionalMask {
|
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
|
if self.enabled {
|
|
painter.set_mask(UiRegion::FULL);
|
|
}
|
|
painter.widget(&self.inner);
|
|
Size::LEFTOVER
|
|
}
|
|
}
|
|
|
|
fn primitive_masks(h: &Harness, id: WidgetId) -> Vec<MaskIdx> {
|
|
h.render.active[&id]
|
|
.primitives
|
|
.iter()
|
|
.map(|primitive| {
|
|
let handle = &primitive.handle;
|
|
h.render.layers[handle.layer].primitives()[handle.kind as usize]
|
|
.as_ref()
|
|
.unwrap()
|
|
.instances()[handle.inst_idx]
|
|
.mask_idx
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
#[test]
|
|
fn a_redrawn_mask_keeps_reused_primitives_clipped_when_it_moves() {
|
|
for node in [false, true] {
|
|
let mut h = Harness::new((400, 200));
|
|
let first = rect(Color::RED).height(50).add(&mut h.rsc);
|
|
let inner = rect(Color::BLUE).add(&mut h.rsc);
|
|
let draws = Rc::new(Cell::new(0));
|
|
let child = Stretchy {
|
|
inner: inner.add_strong(&mut h.rsc),
|
|
draws: draws.clone(),
|
|
}
|
|
.add(&mut h.rsc);
|
|
let masked = child.masked().add(&mut h.rsc);
|
|
h.rsc.widgets_mut().set_region_node(masked, node);
|
|
h.set_root((first, masked).span(Dir::DOWN));
|
|
let mask = h.render.active[&masked.id()].mask;
|
|
let settled = draws.get();
|
|
h.rsc.widgets_mut().get_dyn_mut(masked.id());
|
|
h.frame();
|
|
assert_eq!(primitive_masks(&h, inner.id()), vec![mask]);
|
|
assert_eq!(draws.get(), settled, "a mask repaint must reuse its child");
|
|
assert_eq!(h.render.active[&masked.id()].mask, mask);
|
|
h.set_len(first, Axis::Y, 10);
|
|
h.frame();
|
|
let clip = h.rsc.ui().masks[mask.idx()];
|
|
let clip = h
|
|
.render
|
|
.moves
|
|
.resolve(clip.move_idx, clip.region)
|
|
.to_px(h.render.output_size());
|
|
assert_eq!(clip, h.region(&masked).unwrap());
|
|
assert_corners!(h, inner, (0, 10), (400, 200));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn adding_and_removing_a_mask_updates_existing_primitives() {
|
|
let mut h = Harness::new((400, 200));
|
|
let inner = rect(Color::BLUE).add(&mut h.rsc);
|
|
let masked = OptionalMask {
|
|
inner: inner.add_strong(&mut h.rsc),
|
|
enabled: false,
|
|
}
|
|
.add(&mut h.rsc);
|
|
h.set_root(masked);
|
|
for enabled in [true, false, true, false] {
|
|
h.rsc[masked].enabled = enabled;
|
|
h.frame();
|
|
let mask = h.render.active[&masked.id()].mask;
|
|
assert_eq!(mask == MaskIdx::NONE, !enabled);
|
|
assert_eq!(primitive_masks(&h, inner.id()), vec![mask]);
|
|
}
|
|
assert_eq!(h.rsc.ui().masks.len(), 1, "retired slots must be reusable");
|
|
}
|
|
|
|
#[test]
|
|
fn an_empty_masks_slot_is_released_when_the_mask_is_removed_or_undrawn() {
|
|
let mut h = Harness::new((400, 200));
|
|
let (inner, _) = counted(&mut h, Size::LEFTOVER, false);
|
|
let masked = OptionalMask {
|
|
inner: inner.add_strong(&mut h.rsc),
|
|
enabled: true,
|
|
}
|
|
.add(&mut h.rsc);
|
|
let row = (masked,).span(Dir::DOWN).add(&mut h.rsc);
|
|
h.set_root(row);
|
|
for _ in 0..3 {
|
|
h.rsc[masked].enabled = false;
|
|
h.frame();
|
|
h.rsc[masked].enabled = true;
|
|
h.frame();
|
|
let child = h.rsc[row].pop().unwrap();
|
|
h.frame();
|
|
h.rsc[row].push(child);
|
|
h.frame();
|
|
}
|
|
assert_eq!(h.rsc.ui().masks.len(), 1);
|
|
}
|
|
|
|
struct SharedChild(Rc<StrongWidget>);
|
|
|
|
impl Widget for SharedChild {
|
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
|
painter.widget(self.0.as_ref()).size()
|
|
}
|
|
}
|
|
|
|
struct SwitchParent {
|
|
choices: [StrongWidget; 2],
|
|
choice: usize,
|
|
}
|
|
|
|
impl Widget for SwitchParent {
|
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
|
painter.widget(&self.choices[self.choice]).size()
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_redrawn_subtree_is_not_undrawn_by_the_parent_it_left() {
|
|
for node in [false, true] {
|
|
let mut h = Harness::new((400, 200));
|
|
let leaf = rect(Color::RED).width(40).add(&mut h.rsc);
|
|
let held: StrongWidget = leaf.add_strong(&mut h.rsc);
|
|
let shared = Rc::new(held);
|
|
let first = SharedChild(shared.clone()).add_strong(&mut h.rsc);
|
|
let second = SharedChild(shared).add_strong(&mut h.rsc);
|
|
h.rsc.widgets_mut().set_region_node(&second, node);
|
|
let root = SwitchParent {
|
|
choices: [first, second],
|
|
choice: 0,
|
|
}
|
|
.add(&mut h.rsc);
|
|
h.set_root(root);
|
|
let before = h.region(&leaf);
|
|
h.rsc[root].choice = 1;
|
|
h.frame();
|
|
assert_eq!(h.region(&leaf), before);
|
|
}
|
|
}
|
|
|
|
/// A leaf that reports less than the box it is given and states which lengths
|
|
/// of that box its drawing holds for, so a test can widen the contract
|
|
/// without changing the answer.
|
|
struct Contracted {
|
|
holds: std::ops::RangeInclusive<Px>,
|
|
size: Size,
|
|
}
|
|
|
|
impl Widget for Contracted {
|
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
|
painter.holds(Axis::X, self.holds.clone());
|
|
self.size
|
|
}
|
|
}
|
|
|
|
struct CountedParent {
|
|
inner: StrongWidget,
|
|
draws: Rc<Cell<usize>>,
|
|
}
|
|
|
|
impl Widget for CountedParent {
|
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
|
self.draws.set(self.draws.get() + 1);
|
|
painter.widget(&self.inner).size()
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn widening_what_a_drawing_holds_for_does_not_relay_out_the_parent() {
|
|
let mut h = Harness::new((400, 200));
|
|
let child = Contracted {
|
|
holds: Px::from_int(300)..=Px::from_int(500),
|
|
size: Size::from((100, 200)),
|
|
}
|
|
.add(&mut h.rsc);
|
|
let draws = Rc::new(Cell::new(0));
|
|
let root = CountedParent {
|
|
inner: child.upgrade(&mut h.rsc),
|
|
draws: draws.clone(),
|
|
}
|
|
.add(&mut h.rsc);
|
|
h.set_root(root);
|
|
let settled = draws.get();
|
|
|
|
// The same answer, good for more boxes than before, so the guarantee the
|
|
// parent kept still holds.
|
|
h.rsc[child].holds = Px::from_int(200)..=Px::from_int(600);
|
|
h.frame();
|
|
|
|
assert_eq!(
|
|
draws.get(),
|
|
settled,
|
|
"a wider contract for the same answer is not a change to lay out"
|
|
);
|
|
}
|