use guido::layout::Flex;
mod common;
use common::Harness;
use guido::prelude::*;
use guido::renderer::{
CommandLayer, CornerRadii, DrawCommand, FlattenScratch, FlattenedCommand, NodeId, RenderNode,
ScratchCapacity, flatten_root_into,
};
use guido::shape::PlacedShape;
use guido::tree::WidgetId;
use guido::widgets::Corners;
use guido::widgets::Rect;
use std::rc::Rc;
const VIEWPORT: f32 = 200.0;
const PAD: f32 = 8.0;
const GAP: f32 = 16.0;
const CAPTION: f32 = 17.0;
const CAPTION_GAP: f32 = 4.0;
const ROW_HEIGHT: f32 = 24.0;
const ROW_GAP: f32 = 8.0;
const ROWS: usize = 20;
const SCROLL: f32 = 120.0;
const SCROLLER_TOP: f32 = PAD + CAPTION + CAPTION_GAP;
const COLUMN_A_X: f32 = PAD;
const COLUMN_B_X: f32 = PAD + VIEWPORT + GAP;
const BOX_HEIGHT: f32 = 30.0;
const GROWER_WIDTH: f32 = 40.0;
const GROWN_WIDTH: f32 = 90.0;
const PUSHED_WIDTH: f32 = 50.0;
const PUSHED_FILL: Color = Color::rgb(0.9, 0.4, 0.1);
const PUSHED_NUDGE: (f32, f32) = (5.0, 3.0);
const CLIPPED_FILL: Color = Color::rgb(0.2, 0.7, 0.5);
const SCROLLED_FILL: Color = Color::rgb(0.7, 0.2, 0.5);
const TALL_ROW: f32 = 80.0;
const TALL_ROWS: usize = 3;
const SHORT_SCROLL: f32 = 40.0;
const OUTER_CLIP: f32 = 100.0;
const NUDGE: f32 = 40.0;
const POKER: f32 = 50.0;
const SQUARE_CORNERS: Corners = Corners {
radii: CornerRadii {
top_left: 0.0,
top_right: 0.0,
bottom_right: 0.0,
bottom_left: 0.0,
},
curvature: 1.0,
};
const ROUNDED_CORNERS: Corners = Corners {
radii: CornerRadii {
top_left: 24.0,
top_right: 24.0,
bottom_right: 24.0,
bottom_left: 24.0,
},
curvature: 1.0,
};
fn column() -> Container {
container()
.layout(Flex::column().spacing(CAPTION_GAP))
.child(container().width(VIEWPORT).height(CAPTION))
.child(
container()
.width(VIEWPORT)
.height(VIEWPORT)
.background(Color::rgb(0.15, 0.15, 0.2))
.scroll(Scroll::vertical())
.child(
container()
.layout(Flex::column().spacing(ROW_GAP))
.padding(PAD)
.background(Color::rgb(0.1, 0.1, 0.12))
.children((0..ROWS).map(|_| {
container()
.width(120.0)
.height(ROW_HEIGHT)
.background(Color::rgb(0.25, 0.25, 0.35))
})),
),
)
}
fn two_columns() -> Container {
container()
.layout(Flex::row().spacing(GAP))
.padding(PAD)
.child(column())
.child(column())
}
fn pushed_row(grower: RwSignal<f32>) -> Container {
container()
.layout(Flex::row().spacing(GAP))
.padding(PAD)
.child(
container()
.width(grower)
.child(container().height(BOX_HEIGHT)),
)
.child(
container()
.width(PUSHED_WIDTH)
.height(BOX_HEIGHT)
.translate(PUSHED_NUDGE)
.background(PUSHED_FILL),
)
}
fn hidden_box_over_a_poker<M>(corners: impl IntoAnimated<Corners, M>) -> Container {
container()
.layout(Flex::column().spacing(GAP))
.padding(PAD)
.child(
container()
.width(VIEWPORT)
.height(VIEWPORT)
.corners(corners)
.overflow(Overflow::Hidden)
.child(
container()
.width(VIEWPORT)
.height(TALL_ROW)
.background(CLIPPED_FILL),
),
)
.child(container().width(POKER).height(POKER))
}
fn scroller_of_clipping_rows() -> Container {
container().padding(PAD).child(
container()
.width(VIEWPORT)
.height(VIEWPORT)
.scroll(Scroll::vertical())
.child(
container()
.layout(Flex::column())
.children((0..TALL_ROWS).map(|row| {
container()
.width(VIEWPORT)
.height(TALL_ROW)
.overflow(Overflow::Hidden)
.child(
container()
.width(VIEWPORT)
.height(TALL_ROW * 2.0)
.background(if row == 0 {
CLIPPED_FILL
} else {
Color::rgb(0.25, 0.25, 0.35)
}),
)
})),
),
)
}
fn a_clip_that_appears_over_one_that_does_not(outer: RwSignal<Overflow>) -> Container {
container()
.layout(Flex::column().spacing(GAP))
.child(
container()
.width(VIEWPORT)
.height(OUTER_CLIP)
.overflow(outer)
.child(
container()
.width(VIEWPORT)
.height(OUTER_CLIP)
.translate((0.0, NUDGE))
.overflow(Overflow::Hidden)
.child(
container()
.width(VIEWPORT)
.height(TALL_ROW * 2.0)
.background(CLIPPED_FILL),
),
),
)
.child(container().width(POKER).height(POKER))
}
fn scroller_of_tall_rows() -> Container {
container().padding(PAD).child(
container()
.width(VIEWPORT)
.height(VIEWPORT)
.scroll(Scroll::vertical())
.child(
container()
.layout(Flex::column())
.children((0..TALL_ROWS).map(|row| {
container()
.width(VIEWPORT)
.height(TALL_ROW)
.background(if row == 0 {
SCROLLED_FILL
} else {
Color::rgb(0.25, 0.25, 0.35)
})
})),
),
)
}
struct Surface {
surface: Harness,
root_node: RenderNode,
commands: Vec<FlattenedCommand>,
layers: Vec<CommandLayer>,
scratch: FlattenScratch,
width: f32,
height: f32,
}
impl Surface {
fn new(view: impl Widget + 'static, width: f32, height: f32) -> Self {
let harness = Harness::laid_out(view, width, height);
let root = harness.root;
Self {
surface: harness,
root_node: RenderNode::new(root.as_u64()),
commands: Vec::new(),
layers: Vec::new(),
scratch: FlattenScratch::default(),
width,
height,
}
}
fn scroller(&self, column: usize) -> WidgetId {
let col = self.surface.tree.get_children(self.surface.root)[column];
self.surface.tree.get_children(col)[1]
}
fn caption_node(&self, column: usize) -> Rc<RenderNode> {
Rc::clone(&self.root_node.children[column].children[0])
}
fn frame(&mut self) {
if !self.surface.tree.needs_paint(self.surface.root) {
return;
}
self.root_node.clear();
self.root_node.bounds = Rect::new(0.0, 0.0, self.width, self.height);
let Self {
surface: Harness { tree, root },
root_node,
commands,
layers,
scratch,
..
} = self;
tree.paint_widget(*root, root_node);
let _ = flatten_root_into(root_node, commands, layers, scratch);
for child in &root_node.children {
guido::cache_paint_results(tree, child);
}
tree.clear_needs_paint(*root);
}
fn frames(&mut self, poke: WidgetId, count: usize) {
for _ in 0..count {
self.surface.tree.mark_needs_paint(poke);
self.frame();
}
}
fn box_drawn(&self, fill: Color) -> (Rc<DrawCommand>, (f32, f32)) {
let cmd = self.command_of(fill);
let DrawCommand::RoundedRect { rect, .. } = &*cmd.command else {
unreachable!("`command_of` matched on this variant")
};
(
Rc::clone(&cmd.command),
(
cmd.world_transform.tx() + rect.x,
cmd.world_transform.ty() + rect.y,
),
)
}
fn command_of(&self, fill: Color) -> &FlattenedCommand {
let mut matches = self.commands.iter().filter(
|cmd| matches!(&*cmd.command, DrawCommand::RoundedRect { color, .. } if *color == fill),
);
let found = matches.next().expect("the box contributed a command");
assert!(
matches.next().is_none(),
"the fill names more than one box, so neither what this returns \
nor what is asserted of it is about the box the test means"
);
found
}
fn scroll(&mut self, column: usize, delta: f32) {
let x = if column == 0 {
COLUMN_A_X + VIEWPORT / 2.0
} else {
COLUMN_B_X + VIEWPORT / 2.0
};
let scroller = self.scroller(column);
self.scroll_at(x, SCROLLER_TOP + VIEWPORT / 2.0, delta, scroller);
}
fn scroll_at(&mut self, x: f32, y: f32, delta: f32, scroller: WidgetId) {
let root = self.surface.root;
self.surface.tree.with_widget_mut(root, |w, id, t| {
w.event(t, id, &Event::scroll(x, y, 0.0, delta, ScrollSource::Wheel))
});
self.surface.tree.mark_needs_paint(scroller);
}
fn node_of(&self, widget: WidgetId) -> &RenderNode {
fn find(node: &RenderNode, id: NodeId) -> Option<&RenderNode> {
if node.id == id {
return Some(node);
}
node.children.iter().find_map(|child| find(child, id))
}
find(&self.root_node, widget.as_u64()).expect("the widget contributed a node to this frame")
}
fn clip_shape(&self, fill: Color) -> PlacedShape {
self.command_of(fill)
.clip()
.expect("the box was drawn under a clip")
}
fn drawn_top(&self, column: usize) -> f32 {
let column_x = if column == 0 { COLUMN_A_X } else { COLUMN_B_X };
self.commands
.iter()
.filter(|cmd| {
cmd.clip()
.is_some_and(|clip| (clip.world_aabb().x - column_x).abs() < 0.01)
})
.map(|cmd| cmd.world_transform.ty())
.fold(f32::INFINITY, f32::min)
}
}
#[test]
fn a_scrolled_list_survives_a_frame_painted_for_its_sibling() {
let mut s = Surface::new(
two_columns(),
PAD + VIEWPORT + GAP + VIEWPORT + PAD,
PAD + CAPTION + CAPTION_GAP + VIEWPORT + PAD,
);
s.frame();
let caption = s.caption_node(0);
let at_rest = s.drawn_top(0);
assert_eq!(
at_rest, SCROLLER_TOP,
"at rest the top of what is drawn is the top of the viewport"
);
s.scroll(0, SCROLL);
s.frame();
assert_eq!(
s.drawn_top(0),
SCROLLER_TOP - SCROLL,
"the list should be drawn {SCROLL}px above the viewport"
);
let other = s.scroller(1);
s.surface.tree.mark_needs_paint(other);
s.frame();
assert_eq!(
s.drawn_top(0),
SCROLLER_TOP - SCROLL,
"the first list snapped back when the second one asked for a frame"
);
assert_eq!(
s.drawn_top(1),
SCROLLER_TOP,
"the second list was never scrolled"
);
assert!(
Rc::ptr_eq(&caption, &s.caption_node(0)),
"the caption was repainted rather than served from the paint cache — \
a harness that stops reaching `reuse_cached` goes green however \
broken the cache is"
);
}
#[test]
fn a_cached_child_is_drawn_where_it_moved_to() {
let grower = create_signal(GROWER_WIDTH);
let mut s = Surface::new(
pushed_row(grower),
PAD + GROWN_WIDTH + GAP + PUSHED_WIDTH + PAD,
PAD + BOX_HEIGHT + PAD,
);
s.frame();
let (painted, at_rest) = s.box_drawn(PUSHED_FILL);
assert_eq!(
at_rest,
(
PAD + GROWER_WIDTH + GAP + PUSHED_NUDGE.0,
PAD + PUSHED_NUDGE.1
),
"the box starts behind the grower at its declared width, nudged by its \
own transform"
);
grower.set(GROWN_WIDTH);
let grower_id = s.surface.tree.get_children(s.surface.root)[0];
let boundary = s
.surface
.tree
.mark_needs_layout(grower_id)
.expect("the grower is registered and was not already dirty");
s.surface.lay_out(s.width, s.height);
assert_eq!(
boundary, s.surface.root,
"the grower is its own relayout boundary, so nothing outside it would \
have moved in the running loop either"
);
s.frame();
let (reused, moved_to) = s.box_drawn(PUSHED_FILL);
assert_eq!(
moved_to,
(
PAD + GROWN_WIDTH + GAP + PUSHED_NUDGE.0,
PAD + PUSHED_NUDGE.1
),
"the box did not land at its new position plus its own transform — \
the cached transform either kept a position that should have been \
stripped out or lost the user part that should have been kept"
);
assert!(
Rc::ptr_eq(&painted, &reused),
"the box was repainted rather than re-parented out of the paint \
cache — the moved branch of `reuse_cached` was never reached, so \
this test would go green however broken it is"
);
}
#[test]
fn a_static_clip_and_what_it_cuts_are_both_replayed() {
let mut s = Surface::new(
hidden_box_over_a_poker(SQUARE_CORNERS),
PAD + VIEWPORT + PAD,
PAD + VIEWPORT + GAP + POKER + PAD,
);
s.frame();
let children = s.surface.tree.get_children(s.surface.root);
let (hidden, poker) = (children[0], children[1]);
let clipped = s.surface.tree.get_children(hidden)[0];
let viewport = Rect::new(PAD, PAD, VIEWPORT, VIEWPORT);
assert_eq!(
s.clip_shape(CLIPPED_FILL).world_aabb(),
viewport,
"the box is drawn under the box that hides its overflow"
);
let entries = |s: &Surface| {
(
s.node_of(hidden)
.cached_flatten
.borrow()
.clone()
.expect("a node that sets a clip is cached like any other: the clip is one of the ones it placed"),
s.node_of(clipped)
.cached_flatten
.borrow()
.clone()
.expect("and so is what it cuts"),
)
};
let first = entries(&s);
s.frames(poker, 1);
let second = entries(&s);
assert!(
Rc::ptr_eq(&first.0, &second.0) && Rc::ptr_eq(&first.1, &second.1),
"the clipped box was flattened again although neither it nor the clip \
above it had moved: replay writes nothing back, so a fresh entry here \
means the cache refused"
);
assert_eq!(
s.clip_shape(CLIPPED_FILL).world_aabb(),
viewport,
"the replayed commands lost the clip they name"
);
}
#[test]
fn a_clip_that_appears_above_a_replayed_subtree_cuts_it() {
let outer = create_signal(Overflow::Visible);
let mut s = Surface::new(
a_clip_that_appears_over_one_that_does_not(outer),
VIEWPORT,
OUTER_CLIP + GAP + POKER,
);
s.frame();
let children = s.surface.tree.get_children(s.surface.root);
let (outer_box, poker) = (children[0], children[1]);
let inner = s.surface.tree.get_children(outer_box)[0];
let (painted, _) = s.box_drawn(CLIPPED_FILL);
assert_eq!(
s.clip_shape(CLIPPED_FILL).world_aabb(),
Rect::new(0.0, NUDGE, VIEWPORT, OUTER_CLIP),
"the box starts cut by the inner clip alone, which is the only one \
switched on"
);
s.frames(poker, 1);
let first = s
.node_of(inner)
.cached_flatten
.borrow()
.clone()
.expect("a subtree that only translates is cached, clip of its own or not");
outer.set(Overflow::Hidden);
s.surface.tree.mark_needs_paint(outer_box);
s.frame();
let (reused, _) = s.box_drawn(CLIPPED_FILL);
assert!(
Rc::ptr_eq(&painted, &reused),
"the inner box repainted when the clip above it appeared, so flatten \
was never asked whether its commands could be replayed — this test is \
about the answer, not about the question never being put"
);
let second = s
.node_of(inner)
.cached_flatten
.borrow()
.clone()
.expect("cached again after the next frame");
assert!(
Rc::ptr_eq(&first, &second),
"the inner box was flattened again over a clip it does not carry"
);
assert_eq!(
s.clip_shape(CLIPPED_FILL).world_aabb(),
Rect::new(0.0, NUDGE, VIEWPORT, OUTER_CLIP - NUDGE),
"the clip that appeared above the replayed subtree cuts nothing: its \
own clip was put back answering to no ancestor, because on the frame \
it was cached there was no ancestor to answer to"
);
}
#[test]
fn a_list_that_culled_its_rows_is_not_written_down() {
let mut s = Surface::new(two_columns(), 2.0 * (PAD + VIEWPORT) + GAP, 400.0);
s.frame();
let scroller = s.scroller(0);
let list = s.surface.tree.get_children(scroller)[0];
s.scroll(0, SCROLL);
s.frame();
assert!(
s.surface.tree.get_children(list).len() > s.node_of(list).children.len(),
"nothing was culled, so this test is about a subtree that does not \
exist and would pass however the entry is decided"
);
assert!(
s.node_of(list).cached_flatten.borrow().is_none(),
"the list culled the rows that left the viewport, so its paint is \
partial, so it repaints next frame and no frame can ever replay what \
flatten copied down for it"
);
assert!(
s.caption_node(0).cached_flatten.borrow().is_some(),
"the caption had nothing culled under it and is where it was, so it is \
exactly what an entry is for — refusing it too would be refusing the \
cache itself"
);
}
#[test]
fn a_scrolled_row_is_replayed_and_its_viewport_stays_put() {
let mut s = Surface::new(
scroller_of_tall_rows(),
PAD + VIEWPORT + PAD,
PAD + VIEWPORT + PAD,
);
s.frame();
let scroller = s.surface.tree.get_children(s.surface.root)[0];
let list = s.surface.tree.get_children(scroller)[0];
let row = s.surface.tree.get_children(list)[0];
let viewport = Rect::new(PAD, PAD, VIEWPORT, VIEWPORT);
let (painted, at_rest) = s.box_drawn(SCROLLED_FILL);
assert_eq!(
at_rest,
(PAD, PAD),
"the row starts at the top of the viewport"
);
assert_eq!(
s.clip_shape(SCROLLED_FILL).world_aabb(),
viewport,
"the row is drawn under the scroller's viewport"
);
let first = s
.node_of(row)
.cached_flatten
.borrow()
.clone()
.expect("a row that only translates is worth keeping, and this test is about what happens to that entry when it moves");
s.scroll_at(
PAD + VIEWPORT / 2.0,
PAD + VIEWPORT / 2.0,
SHORT_SCROLL,
scroller,
);
s.frame();
let (reused, moved_to) = s.box_drawn(SCROLLED_FILL);
assert!(
Rc::ptr_eq(&painted, &reused),
"the row repainted instead of coming out of the paint cache, so flatten \
was never asked whether its commands could be replayed — this test is \
about the answer, not about the question never being put"
);
assert_eq!(
moved_to,
(PAD, PAD - SHORT_SCROLL),
"the row did not scroll"
);
assert_eq!(
s.clip_shape(SCROLLED_FILL).world_aabb(),
viewport,
"the clip travelled with the scrolled row instead of staying on the \
viewport, so it now cuts nothing"
);
let second = s
.node_of(row)
.cached_flatten
.borrow()
.clone()
.expect("still cached after the scrolled frame");
assert!(
Rc::ptr_eq(&first, &second),
"the row was flattened again although it only moved, and its clip is \
no longer something it carries — replay writes nothing back, so a \
fresh entry here means the cache refused"
);
}
#[test]
fn a_scrolled_row_carries_its_own_clip_and_not_its_scroller_s() {
let mut s = Surface::new(
scroller_of_clipping_rows(),
PAD + VIEWPORT + PAD,
PAD + VIEWPORT + PAD,
);
s.frame();
let scroller = s.surface.tree.get_children(s.surface.root)[0];
let list = s.surface.tree.get_children(scroller)[0];
let row = s.surface.tree.get_children(list)[0];
let at_rest = s.clip_shape(CLIPPED_FILL).world_aabb();
assert_eq!(
(at_rest.x, at_rest.y, at_rest.height),
(PAD, PAD, TALL_ROW),
"the box is cut by the row that hides its overflow, which is the \
tighter of the two clips over it"
);
let first = s
.node_of(row)
.cached_flatten
.borrow()
.clone()
.expect("a row that only translates is worth keeping, clip of its own or not");
s.scroll_at(
PAD + VIEWPORT / 2.0,
PAD + VIEWPORT / 2.0,
SHORT_SCROLL,
scroller,
);
s.frame();
let second = s
.node_of(row)
.cached_flatten
.borrow()
.clone()
.expect("still cached after the scrolled frame");
assert!(
Rc::ptr_eq(&first, &second),
"the row was flattened again although it only moved — replay writes \
nothing back, so a fresh entry means the cache refused, and this test \
is about what the replay does with two clips"
);
assert_eq!(
s.box_drawn(CLIPPED_FILL).1,
(PAD, PAD - SHORT_SCROLL),
"the row did not scroll"
);
assert_eq!(
s.clip_shape(CLIPPED_FILL).world_aabb(),
Rect::new(at_rest.x, PAD, at_rest.width, TALL_ROW - SHORT_SCROLL),
"the row's own clip and the scroller's viewport did not end up in the \
two different places they belong: the row's travels with it, the \
viewport's stays"
);
}
#[test]
fn a_subtree_whose_clip_changed_shape_is_cut_by_the_new_shape() {
let corners = create_signal(SQUARE_CORNERS);
let mut s = Surface::new(
hidden_box_over_a_poker(corners),
PAD + VIEWPORT + PAD,
PAD + VIEWPORT + GAP + POKER + PAD,
);
s.frame();
let children = s.surface.tree.get_children(s.surface.root);
let hidden = children[0];
let clipped = s.surface.tree.get_children(hidden)[0];
let (painted, _) = s.box_drawn(CLIPPED_FILL);
assert_eq!(
s.clip_shape(CLIPPED_FILL).radii,
SQUARE_CORNERS.radii,
"the box starts under a clip with square corners"
);
let first = s
.node_of(clipped)
.cached_flatten
.borrow()
.clone()
.expect("a subtree under a static clip is cached");
corners.set(ROUNDED_CORNERS);
s.surface.tree.mark_needs_paint(hidden);
s.frame();
let (reused, _) = s.box_drawn(CLIPPED_FILL);
assert!(
Rc::ptr_eq(&painted, &reused),
"the box repainted when the clip around it changed, so flatten was \
never asked whether its commands could be replayed — this test is \
about the answer, not about the question never being put"
);
let second = s
.node_of(clipped)
.cached_flatten
.borrow()
.clone()
.expect("cached again after the next frame");
assert!(
Rc::ptr_eq(&first, &second),
"the box was flattened again over a change to a shape it does not \
carry — replay writes nothing back, so a fresh entry means the cache \
refused what it no longer has any reason to refuse"
);
assert_eq!(
s.clip_shape(CLIPPED_FILL).radii,
ROUNDED_CORNERS.radii,
"the box was cut by the corners its clip used to have, so naming the \
clip bought nothing"
);
}
#[test]
fn a_subtree_whose_ancestor_turned_is_not_reused() {
let angle = create_signal(0.0f32);
let mut s = Surface::new(
container()
.layout(Flex::column().spacing(GAP))
.padding(PAD)
.child(
container().rotate(angle).child(
container()
.width(PUSHED_WIDTH)
.height(BOX_HEIGHT)
.background(CLIPPED_FILL),
),
)
.child(container().width(POKER).height(POKER)),
PAD + VIEWPORT + PAD,
PAD + VIEWPORT + PAD,
);
s.frame();
let children = s.surface.tree.get_children(s.surface.root);
let (turner, poker) = (children[0], children[1]);
let turned = s.surface.tree.get_children(turner)[0];
s.frames(poker, 1);
let first = s.node_of(turned).cached_flatten.borrow().clone();
assert!(
first.is_some(),
"nothing is clipped here, so the box is cached from its first flatten \
— without an entry there is nothing for the next frame to refuse"
);
assert!(
s.command_of(CLIPPED_FILL)
.world_transform
.is_translation_only(),
"the box starts upright, which is what makes the next frame a test of \
anything"
);
angle.set(std::f32::consts::FRAC_PI_4);
s.surface.tree.mark_needs_paint(turner);
s.frame();
assert!(
s.node_of(turned).cached_flatten.borrow().is_none(),
"the box still holds the entry it was cached with. Replay leaves it \
untouched and a full flatten under a turn clears it, because a turned \
subtree can be replayed by no offset at all — so the entry surviving \
says its old self was handed back"
);
drop(first);
assert!(
!s.command_of(CLIPPED_FILL)
.world_transform
.is_translation_only(),
"the box was drawn by a transform that only translates, so it was \
replayed as the upright thing it used to be rather than flattened \
under the turn"
);
}
#[test]
fn a_subtree_whose_ancestor_faded_is_replayed_at_the_new_opacity() {
let opacity = create_signal(1.0f32);
let mut s = Surface::new(
container()
.layout(Flex::column().spacing(GAP))
.padding(PAD)
.child(
container().opacity(opacity).child(
container()
.width(PUSHED_WIDTH)
.height(BOX_HEIGHT)
.opacity(0.5)
.background(CLIPPED_FILL),
),
)
.child(container().width(POKER).height(POKER)),
PAD + VIEWPORT + PAD,
PAD + VIEWPORT + PAD,
);
s.frame();
let children = s.surface.tree.get_children(s.surface.root);
let (fader, poker) = (children[0], children[1]);
let faded = s.surface.tree.get_children(fader)[0];
s.frames(poker, 1);
let (painted, _) = s.box_drawn(CLIPPED_FILL);
let first = s
.node_of(faded)
.cached_flatten
.borrow()
.clone()
.expect("the box is cached from its first flatten");
assert_eq!(
s.command_of(CLIPPED_FILL).opacity,
0.5,
"at its own opacity"
);
opacity.set(0.5);
s.surface.tree.mark_needs_paint(fader);
s.frame();
let (reused, _) = s.box_drawn(CLIPPED_FILL);
assert!(
Rc::ptr_eq(&painted, &reused),
"the box repainted when the container around it faded, so flatten was \
never asked to replay it under the new opacity"
);
assert!(
Rc::ptr_eq(
&first,
&s.node_of(faded).cached_flatten.borrow().clone().unwrap()
),
"the box was flattened again rather than replayed — replay writes \
nothing back, so a fresh entry means the cache refused"
);
assert_eq!(
s.command_of(CLIPPED_FILL).opacity,
0.25,
"the replayed box was drawn at the opacity it was cached under, not at \
half of the container's new one"
);
}
#[test]
fn a_frame_flattens_into_the_buffers_the_last_one_left() {
let mut s = Surface::new(
two_columns(),
PAD + VIEWPORT + GAP + VIEWPORT + PAD,
PAD + CAPTION + CAPTION_GAP + VIEWPORT + PAD,
);
s.frame();
let poke = s.scroller(1);
s.frames(poke, 1);
let wide = s.scratch.capacity();
assert!(
!s.commands.is_empty(),
"the frame drew nothing, so there is nothing for the scratch to have held"
);
let ScratchCapacity {
commands,
rects,
clips,
..
} = wide;
assert!(
commands > 0 && rects > 0 && clips > 0,
"the scratch the surface owns came out of the frame empty-handed \
({wide:?}) — flatten never touched the one it was handed"
);
let mut one_box = RenderNode::new(0);
one_box.bounds = Rect::new(0.0, 0.0, 10.0, 10.0);
one_box.commands.push(Rc::new(DrawCommand::rounded_rect(
one_box.bounds,
CLIPPED_FILL,
0.0,
)));
let _ = flatten_root_into(&one_box, &mut s.commands, &mut s.layers, &mut s.scratch);
assert_eq!(
s.commands.len(),
1,
"the narrow frame is meant to need next to nothing — a frame that \
needs as much as the last one cannot tell a kept buffer from a new one"
);
assert_eq!(
s.scratch.capacity(),
wide,
"the narrow frame left the scratch its own size, so the buffers the \
wide frames filled were given back and built again"
);
}