use std::ops::Range;
use std::rc::Rc;
use smallvec::SmallVec;
use crate::transform::Transform;
use crate::widgets::Rect;
use super::clip::{ClipChains, ClipIndex, ClipRef, ClipTree, ResolvedClip};
use super::commands::DrawCommand;
use super::tree::{CachedFlatten, RenderNode};
use crate::shape::PlacedShape;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum RenderLayer {
#[default]
Backdrop = 0,
Shapes = 1,
Images = 2,
Text = 3,
Overlay = 4,
}
#[derive(Debug, Clone)]
pub struct FlattenedCommand {
pub command: Rc<DrawCommand>,
pub world_transform: Transform,
pub layer: RenderLayer,
pub opacity: f32,
pub clip: Option<ClipRef>,
}
impl FlattenedCommand {
pub fn clip(&self) -> Option<ResolvedClip> {
self.clip.as_ref().map(ClipRef::resolved)
}
}
#[derive(Clone, Copy)]
pub struct FlatFrame<'a> {
pub commands: &'a [FlattenedCommand],
pub layers: &'a [CommandLayer],
pub clips: &'a ClipChains,
}
pub struct FlattenScratch {
groups: Vec<LayerBuckets>,
used: usize,
clips: ClipTree,
carried: RegionsCarried,
}
#[derive(Default)]
struct LayerBuckets {
backdrop: Vec<FlattenedCommand>,
shapes: Vec<FlattenedCommand>,
images: Vec<FlattenedCommand>,
text: Vec<FlattenedCommand>,
overlay: Vec<FlattenedCommand>,
high_water: RenderLayer,
bounds: [LayerBounds; LAYER_COUNT],
}
#[derive(Default)]
struct LayerBounds {
union: Option<Rect>,
rects: SmallVec<[Rect; 16]>,
overflowed: bool,
}
const MAX_TRACKED_RECTS: usize = 64;
impl LayerBounds {
fn intersects(&self, rect: Rect) -> bool {
let Some(union) = self.union else {
return false;
};
if !union.intersects(&rect) {
return false;
}
self.overflowed || self.rects.iter().any(|held| held.intersects(&rect))
}
fn record(&mut self, rect: Option<Rect>) {
let Some(rect) = rect else {
self.union = Some(EVERYTHING);
self.overflowed = true;
return;
};
self.union = Some(match self.union {
Some(union) => union_of(union, rect),
None => rect,
});
if self.rects.len() < MAX_TRACKED_RECTS {
self.rects.push(rect);
} else {
self.overflowed = true;
}
}
fn clear(&mut self) {
let Self {
union,
rects,
overflowed,
} = self;
*union = None;
rects.clear();
*overflowed = false;
}
fn heap_rects(&self) -> usize {
if self.rects.spilled() {
self.rects.capacity()
} else {
0
}
}
}
const LAYER_COUNT: usize = 5;
impl LayerBuckets {
fn covered_above(&self, layer: RenderLayer, rect: Option<Rect>) -> bool {
let Some(rect) = rect else {
return true;
};
(layer as usize + 1..LAYER_COUNT).any(|above| self.bounds[above].intersects(rect))
}
fn record(&mut self, layer: RenderLayer, rect: Option<Rect>) {
self.bounds[layer as usize].record(rect);
}
fn bucket_mut(&mut self, layer: RenderLayer) -> &mut Vec<FlattenedCommand> {
match layer {
RenderLayer::Backdrop => &mut self.backdrop,
RenderLayer::Shapes => &mut self.shapes,
RenderLayer::Images => &mut self.images,
RenderLayer::Text => &mut self.text,
RenderLayer::Overlay => &mut self.overlay,
}
}
fn buckets_mut(&mut self) -> [&mut Vec<FlattenedCommand>; LAYER_COUNT] {
[
&mut self.backdrop,
&mut self.shapes,
&mut self.images,
&mut self.text,
&mut self.overlay,
]
}
fn clear(&mut self) {
let Self {
backdrop,
shapes,
images,
text,
overlay,
high_water,
bounds,
} = self;
for bucket in [backdrop, shapes, images, text, overlay] {
bucket.clear();
}
*high_water = RenderLayer::default();
for bounds in bounds {
bounds.clear();
}
}
fn command_capacity(&self) -> usize {
self.backdrop.capacity()
+ self.shapes.capacity()
+ self.images.capacity()
+ self.text.capacity()
+ self.overlay.capacity()
}
}
impl Default for FlattenScratch {
fn default() -> Self {
Self {
groups: vec![LayerBuckets::default()],
used: 1,
clips: ClipTree::default(),
carried: RegionsCarried::default(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct ScratchCapacity {
pub groups: usize,
pub commands: usize,
pub rects: usize,
pub clips: usize,
pub clip_links: usize,
}
impl FlattenScratch {
pub fn capacity(&self) -> ScratchCapacity {
ScratchCapacity {
groups: self.groups.capacity(),
commands: self.groups.iter().map(LayerBuckets::command_capacity).sum(),
rects: self
.groups
.iter()
.flat_map(|group| &group.bounds)
.map(LayerBounds::heap_rects)
.sum(),
clips: self.clips.capacity(),
clip_links: self.clips.link_capacity(),
}
}
pub fn clip_chains(&self) -> &ClipChains {
self.clips.chains()
}
fn clear(&mut self) {
let Self {
groups,
used,
clips,
carried,
} = self;
for group in &mut groups[..*used] {
group.clear();
}
*used = 1;
clips.clear();
*carried = RegionsCarried::default();
}
fn live(&self) -> &[LayerBuckets] {
&self.groups[..self.used]
}
fn live_mut(&mut self) -> &mut [LayerBuckets] {
&mut self.groups[..self.used]
}
fn current(&self) -> &LayerBuckets {
&self.groups[self.used - 1]
}
fn current_mut(&mut self) -> &mut LayerBuckets {
&mut self.groups[self.used - 1]
}
fn open_group(&mut self, high_water: RenderLayer) {
if self.used == self.groups.len() {
self.groups.push(LayerBuckets::default());
}
self.groups[self.used].high_water = high_water;
self.used += 1;
}
fn push(&mut self, cmd: FlattenedCommand) {
match &*cmd.command {
DrawCommand::BackdropBlur { sources, .. }
if sources.contains(crate::backdrop::BackdropSources::COMPOSITOR) =>
{
self.carried.compositor_blur = true;
}
DrawCommand::InputRegion { .. } => {
self.carried.input_region = true;
self.current_mut().bucket_mut(cmd.layer).push(cmd);
return;
}
_ => {}
}
let layer = cmd.layer;
let rect = world_bounds(&cmd);
let current = self.current();
if layer < current.high_water && current.covered_above(layer, rect) {
self.open_group(layer);
}
let current = self.current_mut();
current.high_water = current.high_water.max(layer);
current.record(layer, rect);
current.bucket_mut(layer).push(cmd);
}
fn mark(&self) -> Mark {
let last = self.current();
Mark {
groups: self.used,
buckets: [
last.backdrop.len(),
last.shapes.len(),
last.images.len(),
last.text.len(),
last.overlay.len(),
],
}
}
fn commands_since(&self, mark: Mark, inherited: Option<&ClipRef>) -> Vec<FlattenedCommand> {
let mut result = Vec::with_capacity(self.tails_since(mark).map(<[_]>::len).sum());
for tail in self.tails_since(mark) {
result.extend(tail.iter().map(|cmd| {
let mut kept = cmd.clone();
if kept
.clip
.as_ref()
.is_some_and(|clip| inherited.is_some_and(|base| base.is(clip)))
{
kept.clip = None;
}
kept
}));
}
result
}
fn tails_since(&self, mark: Mark) -> impl Iterator<Item = &[FlattenedCommand]> {
self.live()
.iter()
.enumerate()
.skip(mark.groups - 1)
.flat_map(move |(index, group)| {
let buckets = [
&group.backdrop,
&group.shapes,
&group.images,
&group.text,
&group.overlay,
];
buckets
.into_iter()
.enumerate()
.map(move |(bucket_index, bucket)| {
let from = if index == mark.groups - 1 {
mark.buckets[bucket_index]
} else {
0
};
&bucket[from..]
})
})
}
fn drain_into(&mut self, out: &mut Vec<FlattenedCommand>, layers: &mut Vec<CommandLayer>) {
for group in self.live_mut() {
let mut layer = CommandLayer::default();
let ranges = [
&mut layer.backdrop,
&mut layer.shapes,
&mut layer.images,
&mut layer.text,
&mut layer.overlay,
];
for (bucket, range) in group.buckets_mut().into_iter().zip(ranges) {
let start = out.len();
out.append(bucket);
*range = start..out.len();
}
if !layer.is_empty() {
layers.push(layer);
}
}
}
}
#[derive(Debug, Clone, Copy)]
struct Mark {
groups: usize,
buckets: [usize; LAYER_COUNT],
}
#[derive(Debug, Clone, Default)]
pub struct CommandLayer {
pub backdrop: Range<usize>,
pub shapes: Range<usize>,
pub images: Range<usize>,
pub text: Range<usize>,
pub overlay: Range<usize>,
}
impl CommandLayer {
pub fn is_empty(&self) -> bool {
self.backdrop.is_empty()
&& self.shapes.is_empty()
&& self.images.is_empty()
&& self.text.is_empty()
&& self.overlay.is_empty()
}
}
pub fn flatten_root_into(
root: &RenderNode,
commands: &mut Vec<FlattenedCommand>,
layers: &mut Vec<CommandLayer>,
scratch: &mut FlattenScratch,
) -> RegionsCarried {
commands.clear();
layers.clear();
scratch.clear();
flatten_node(root, Transform::IDENTITY, None, 1.0, false, scratch);
let carried = scratch.carried;
scratch.drain_into(commands, layers);
carried
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct RegionsCarried {
pub compositor_blur: bool,
pub input_region: bool,
}
fn flatten_node(
node: &RenderNode,
parent_world_transform: Transform,
parent_clip: Option<ClipIndex>,
parent_opacity: f32,
replayable: bool,
out: &mut FlattenScratch,
) -> bool {
let local_centered = if node.local_transform.is_identity() {
Transform::IDENTITY
} else {
node.local_transform.about(node.pivot, node.bounds)
};
let world_transform = parent_world_transform.then(&local_centered);
let opacity = parent_opacity * node.opacity;
let inherited = parent_clip.map(|index| out.clips.reference(index));
let cached_flatten = if !node.repainted.get() {
node.cached_flatten.borrow().clone()
} else {
None
};
if let Some(cached) = cached_flatten
&& let Some((dx, dy)) = cached.replay_offset(world_transform)
&& let Some(fade) = cached.replay_fade(parent_opacity)
{
out.clips.replay(&cached.clips, dx, dy, parent_clip);
for cmd in &cached.commands {
let mut replayed = cmd.clone();
replayed.world_transform = cmd.world_transform.translated(dx, dy);
replayed.opacity = cmd.opacity * fade;
if replayed.clip.is_none() {
replayed.clip = inherited.clone();
}
out.push(replayed);
}
crate::render_stats::record_flatten_cached();
return false;
}
let marks = (replayable && world_transform.is_translation_only())
.then(|| (out.mark(), out.clips.mark()));
let (clip_index, effective_clip) = match &node.clip {
Some(clip) => {
let own = PlacedShape::from_clip(clip, world_transform);
let index = out.clips.place(own, parent_clip);
(Some(index), Some(out.clips.reference(index)))
}
None => (parent_clip, inherited.clone()),
};
for cmd in &node.commands {
let layer = match &**cmd {
DrawCommand::Text { .. } => RenderLayer::Text,
DrawCommand::Image { .. } => RenderLayer::Images,
DrawCommand::BackdropBlur { sources, .. }
if !sources.contains(crate::backdrop::BackdropSources::SURFACE) =>
{
RenderLayer::Shapes
}
DrawCommand::InputRegion { .. } => RenderLayer::Shapes,
DrawCommand::BackdropBlur { .. } | DrawCommand::TextBackdropBlur { .. } => {
RenderLayer::Backdrop
}
_ => RenderLayer::Shapes,
};
out.push(FlattenedCommand {
command: Rc::clone(cmd),
world_transform,
layer,
opacity,
clip: effective_clip.clone(),
});
}
let mut subtree_partial = node.partial;
for child in &node.children {
subtree_partial |= flatten_node(child, world_transform, clip_index, opacity, true, out);
}
if !node.overlay_commands.is_empty() {
let overlay_clip = match node.overlay_clip {
Some(ref clip) => {
let own = PlacedShape::from_clip(clip, world_transform);
let index = out.clips.place(own, clip_index);
Some(out.clips.reference(index))
}
None => effective_clip,
};
for cmd in &node.overlay_commands {
out.push(FlattenedCommand {
command: Rc::clone(cmd),
world_transform,
layer: RenderLayer::Overlay,
opacity,
clip: overlay_clip.clone(),
});
}
}
*node.cached_flatten.borrow_mut() =
marks.filter(|_| !subtree_partial).map(|(mark, clip_mark)| {
Rc::new(CachedFlatten {
commands: out.commands_since(mark, inherited.as_ref()),
clips: out.clips.since(clip_mark),
world_transform,
opacity: parent_opacity,
})
});
crate::render_stats::record_flatten_full();
subtree_partial
}
const EVERYTHING: Rect = Rect {
x: f32::MIN / 2.0,
y: f32::MIN / 2.0,
width: f32::MAX,
height: f32::MAX,
};
fn union_of(a: Rect, b: Rect) -> Rect {
let x = a.x.min(b.x);
let y = a.y.min(b.y);
let right = (a.x + a.width).max(b.x + b.width);
let bottom = (a.y + a.height).max(b.y + b.height);
Rect::new(x, y, right - x, bottom - y)
}
fn world_bounds(cmd: &FlattenedCommand) -> Option<Rect> {
let local = match &*cmd.command {
DrawCommand::RoundedRect {
rect,
border,
shadow,
..
} => {
let mut grow: f32 = border.map_or(0.0, |b| b.width);
let mut offset: (f32, f32) = (0.0, 0.0);
if let Some(shadow) = shadow {
grow = grow.max(shadow.spill());
offset = shadow.offset;
}
Rect::new(
rect.x - grow + offset.0.min(0.0),
rect.y - grow + offset.1.min(0.0),
rect.width + grow * 2.0 + offset.0.abs(),
rect.height + grow * 2.0 + offset.1.abs(),
)
}
DrawCommand::Circle { center, radius, .. } => Rect::new(
center.0 - radius,
center.1 - radius,
radius * 2.0,
radius * 2.0,
),
DrawCommand::Image { rect, .. } => *rect,
DrawCommand::BackdropBlur { rect, .. } | DrawCommand::InputRegion { rect, .. } => *rect,
DrawCommand::Text {
rect, font_size, ..
}
| DrawCommand::TextBackdropBlur {
rect, font_size, ..
} => {
let slack = font_size * 0.5;
Rect::new(
rect.x - slack,
rect.y - slack,
rect.width + slack * 2.0,
rect.height + slack * 2.0,
)
}
};
if !cmd.world_transform.is_translation_only() {
return None;
}
Some(Rect::new(
local.x + cmd.world_transform.tx(),
local.y + cmd.world_transform.ty(),
local.width,
local.height,
))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::renderer::CornerRadii;
use crate::renderer::NodeId;
use crate::renderer::tree::ClipRegion;
use crate::widgets::Color;
fn command(layer: RenderLayer) -> FlattenedCommand {
command_at(layer, Rect::new(0.0, 0.0, 100.0, 100.0))
}
fn command_at(layer: RenderLayer, rect: Rect) -> FlattenedCommand {
FlattenedCommand {
command: Rc::new(DrawCommand::rounded_rect(rect, Color::WHITE, 0.0)),
world_transform: Transform::IDENTITY,
layer,
opacity: 1.0,
clip: None,
}
}
fn groups(sequence: &[RenderLayer]) -> Vec<Vec<RenderLayer>> {
let mut layered = FlattenScratch::default();
for layer in sequence {
layered.push(command(*layer));
}
let mut commands = Vec::new();
let mut layers = Vec::new();
layered.drain_into(&mut commands, &mut layers);
layers
.iter()
.map(|layer| {
[&layer.shapes, &layer.images, &layer.text, &layer.overlay]
.iter()
.flat_map(|range| commands[(*range).clone()].iter().map(|c| c.layer))
.collect()
})
.collect()
}
use RenderLayer::{Backdrop, Images, Overlay, Shapes, Text};
#[test]
fn a_compositor_only_blur_neither_breaks_the_pass_nor_splits_the_group() {
let frame = |sources| {
let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
let mut node = RenderNode::new(1);
node.bounds = rect;
node.commands
.push(Rc::new(DrawCommand::rounded_rect(rect, Color::WHITE, 0.0)));
node.commands.push(Rc::new(DrawCommand::BackdropBlur {
rect,
sources,
radius: 24.0,
corner_radii: CornerRadii::from(0.0),
curvature: 1.0,
}));
let (mut commands, mut layers) = (Vec::new(), Vec::new());
let told = flatten_root_into(
&node,
&mut commands,
&mut layers,
&mut FlattenScratch::default(),
)
.compositor_blur;
let drawn = layers.iter().any(|l| !l.backdrop.is_empty());
(told, drawn, layers.len())
};
let (told, drawn, groups) = frame(crate::backdrop::BackdropSources::COMPOSITOR);
assert!(told, "the compositor still has to be handed a region");
assert!(!drawn, "and this renderer has nothing to do for it");
assert_eq!(groups, 1, "so the frame is not split for it either");
let (told, drawn, groups) = frame(crate::backdrop::BackdropSources::SURFACE);
assert!(!told, "nothing for the compositor");
assert!(drawn, "and a pass of our own to run");
assert_eq!(groups, 2, "which is what the split is for");
}
#[test]
fn an_input_declaration_is_carried_without_costing_a_group() {
let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
let mut node = RenderNode::new(1);
node.bounds = rect;
node.commands
.push(Rc::new(DrawCommand::rounded_rect(rect, Color::WHITE, 0.0)));
node.commands.push(Rc::new(DrawCommand::InputRegion {
rect,
corner_radii: CornerRadii::from(0.0),
curvature: 1.0,
takes: true,
}));
let (mut commands, mut layers) = (Vec::new(), Vec::new());
let carried = flatten_root_into(
&node,
&mut commands,
&mut layers,
&mut FlattenScratch::default(),
);
assert!(
carried.input_region,
"the frame carries a declaration, and the loop is told so"
);
assert_eq!(layers.len(), 1, "and it splits nothing to say it");
assert_eq!(commands.len(), 2, "while still travelling with the frame");
}
#[test]
fn ascending_layers_stay_in_one_group() {
assert_eq!(
groups(&[Shapes, Shapes, Images, Text, Overlay]),
vec![vec![Shapes, Shapes, Images, Text, Overlay]]
);
}
#[test]
fn a_shape_after_an_image_opens_a_group() {
assert_eq!(
groups(&[Shapes, Images, Shapes]),
vec![vec![Shapes, Images], vec![Shapes]]
);
}
#[test]
fn an_image_after_text_opens_a_group() {
assert_eq!(
groups(&[Images, Text, Images]),
vec![vec![Images, Text], vec![Images]]
);
}
#[test]
fn repeats_of_the_current_layer_do_not_split() {
assert_eq!(
groups(&[Images, Images, Images]),
vec![vec![Images, Images, Images]]
);
}
#[test]
fn each_regression_opens_exactly_one_group() {
assert_eq!(
groups(&[Shapes, Images, Shapes, Images, Shapes]),
vec![vec![Shapes, Images], vec![Shapes, Images], vec![Shapes]]
);
}
#[test]
fn a_group_is_reopened_only_below_its_high_water_mark() {
assert_eq!(
groups(&[Shapes, Text, Images]),
vec![vec![Shapes, Text], vec![Images]]
);
}
#[test]
fn replaying_a_cached_subtree_reproduces_its_grouping() {
let sequence = [Shapes, Images, Shapes, Text, Images];
let mut original = FlattenScratch::default();
for layer in sequence {
original.push(command(layer));
}
let cached = original.commands_since(
Mark {
groups: 1,
buckets: [0; LAYER_COUNT],
},
None,
);
let mut replayed = FlattenScratch::default();
for cmd in cached {
replayed.push(cmd);
}
let (mut a_cmds, mut a_layers) = (Vec::new(), Vec::new());
original.drain_into(&mut a_cmds, &mut a_layers);
let (mut b_cmds, mut b_layers) = (Vec::new(), Vec::new());
replayed.drain_into(&mut b_cmds, &mut b_layers);
assert_eq!(a_layers.len(), b_layers.len());
let order = |cmds: &[FlattenedCommand], layers: &[CommandLayer]| -> Vec<RenderLayer> {
layers
.iter()
.flat_map(|l| {
[&l.shapes, &l.images, &l.text, &l.overlay]
.iter()
.flat_map(|r| cmds[(*r).clone()].iter().map(|c| c.layer))
.collect::<Vec<_>>()
})
.collect()
};
assert_eq!(order(&a_cmds, &a_layers), order(&b_cmds, &b_layers));
}
#[test]
fn a_regression_that_covers_nothing_stays_in_the_group() {
let mut layered = FlattenScratch::default();
for column in 0..4 {
let x = column as f32 * 200.0;
layered.push(command_at(Shapes, Rect::new(x, 0.0, 100.0, 40.0)));
layered.push(command_at(Text, Rect::new(x, 0.0, 100.0, 40.0)));
}
let mut commands = Vec::new();
let mut layers = Vec::new();
layered.drain_into(&mut commands, &mut layers);
assert_eq!(layers.len(), 1, "non-overlapping siblings must not split");
}
#[test]
fn a_regression_that_covers_something_still_splits() {
let mut layered = FlattenScratch::default();
layered.push(command_at(Text, Rect::new(0.0, 0.0, 100.0, 40.0)));
layered.push(command_at(Shapes, Rect::new(20.0, 10.0, 40.0, 10.0)));
let mut commands = Vec::new();
let mut layers = Vec::new();
layered.drain_into(&mut commands, &mut layers);
assert_eq!(layers.len(), 2);
}
#[test]
fn a_shadow_that_reaches_a_label_only_with_its_outer_ring_still_splits() {
let mut layered = FlattenScratch::default();
layered.push(command_at(Text, Rect::new(0.0, 0.0, 100.0, 40.0)));
let shadow = crate::renderer::Shadow::new((0.0, 0.0), 8.0, 0.0, Color::BLACK);
layered.push(FlattenedCommand {
command: Rc::new(DrawCommand::RoundedRect {
rect: Rect::new(0.0, 52.0, 100.0, 40.0),
color: Color::WHITE,
radius: 0.0.into(),
curvature: 1.0,
border: None,
shadow: Some(shadow),
gradient: None,
}),
..command(Shapes)
});
let mut commands = Vec::new();
let mut layers = Vec::new();
layered.drain_into(&mut commands, &mut layers);
assert_eq!(layers.len(), 2, "the ring lands on the label");
}
#[test]
fn an_unbounded_command_forces_a_split() {
let mut layered = FlattenScratch::default();
layered.push(command_at(Text, Rect::new(0.0, 0.0, 10.0, 10.0)));
let mut rotated = command_at(Shapes, Rect::new(900.0, 900.0, 10.0, 10.0));
rotated.world_transform = Transform::rotate(45.0);
layered.push(rotated);
let mut commands = Vec::new();
let mut layers = Vec::new();
layered.drain_into(&mut commands, &mut layers);
assert_eq!(layers.len(), 2);
}
#[test]
fn an_unbounded_command_already_drawn_covers_what_comes_after() {
for bars in [vec![], vec![Rect::new(0.0, 0.0, 100.0, 100.0)]] {
let mut layered = FlattenScratch::default();
let mut turned = command_at(Shapes, Rect::new(900.0, 900.0, 10.0, 10.0));
turned.world_transform = Transform::rotate(12.0);
layered.push(turned);
for bar in bars {
layered.push(command_at(Shapes, bar));
}
layered.push(command_at(Backdrop, Rect::new(10.0, 10.0, 50.0, 20.0)));
let mut commands = Vec::new();
let mut layers = Vec::new();
layered.drain_into(&mut commands, &mut layers);
assert_eq!(layers.len(), 2);
assert!(layers[0].backdrop.is_empty());
assert_eq!(layers[1].backdrop.len(), 1);
}
}
#[test]
fn a_mark_survives_a_later_command_landing_in_an_earlier_bucket() {
let mut layered = FlattenScratch::default();
layered.push(command_at(Text, Rect::new(0.0, 0.0, 50.0, 20.0)));
let mark = layered.mark();
layered.push(command_at(Shapes, Rect::new(500.0, 0.0, 50.0, 20.0)));
layered.push(command_at(Text, Rect::new(500.0, 0.0, 50.0, 20.0)));
let captured = layered.commands_since(mark, None);
assert_eq!(
captured.len(),
2,
"the mark must not sweep up the earlier text"
);
assert_eq!(captured[0].layer, Shapes);
assert_eq!(captured[1].layer, Text);
}
#[test]
fn a_captured_subtree_is_allocated_once() {
let mut layered = FlattenScratch::default();
let mark = layered.mark();
for column in 0..8 {
let x = column as f32 * 200.0;
layered.push(command_at(Shapes, Rect::new(x, 0.0, 100.0, 40.0)));
}
layered.push(command_at(Text, Rect::new(0.0, 0.0, 100.0, 40.0)));
let captured = layered.commands_since(mark, None);
assert_eq!(captured.len(), 9);
assert_eq!(
captured.capacity(),
captured.len(),
"the capture must be allocated to its final size, not grown into it"
);
}
#[test]
fn a_gap_between_two_labels_is_not_covered() {
let mut layered = FlattenScratch::default();
layered.push(command_at(Text, Rect::new(0.0, 0.0, 40.0, 20.0)));
layered.push(command_at(Text, Rect::new(400.0, 0.0, 40.0, 20.0)));
layered.push(command_at(Shapes, Rect::new(200.0, 0.0, 40.0, 20.0)));
let mut commands = Vec::new();
let mut layers = Vec::new();
layered.drain_into(&mut commands, &mut layers);
assert_eq!(layers.len(), 1);
}
#[test]
fn past_the_rect_cap_the_union_decides() {
let mut layered = FlattenScratch::default();
for i in 0..(MAX_TRACKED_RECTS + 1) {
let x = i as f32 * 100.0;
layered.push(command_at(Text, Rect::new(x, 0.0, 10.0, 10.0)));
}
layered.push(command_at(Shapes, Rect::new(50.0, 0.0, 10.0, 10.0)));
let mut commands = Vec::new();
let mut layers = Vec::new();
layered.drain_into(&mut commands, &mut layers);
assert_eq!(layers.len(), 2, "the union must stay conservative");
}
#[test]
fn empty_groups_are_dropped() {
let mut layered = FlattenScratch::default();
layered.push(command(Images));
layered.push(command(Shapes));
let mut commands = Vec::new();
let mut layers = Vec::new();
layered.drain_into(&mut commands, &mut layers);
assert!(layers.iter().all(|layer| !layer.is_empty()));
}
const NEAR: Rect = Rect {
x: 0.0,
y: 0.0,
width: 20.0,
height: 20.0,
};
const CARD: Rect = Rect {
x: 100.0,
y: 100.0,
width: 40.0,
height: 40.0,
};
fn box_node(id: NodeId, rect: Rect) -> RenderNode {
let mut node = RenderNode::new(id);
node.bounds = rect;
node.commands
.push(Rc::new(DrawCommand::rounded_rect(rect, Color::WHITE, 0.0)));
node
}
fn text_node(id: NodeId, rect: Rect) -> RenderNode {
let mut node = RenderNode::new(id);
node.bounds = rect;
node.commands.push(Rc::new(DrawCommand::Text {
text: "hi".into(),
rect,
color: Color::WHITE,
font_size: 10.0,
font_family: crate::widgets::FontFamily::default(),
font_weight: crate::widgets::FontWeight::default(),
line_height: Default::default(),
letter_spacing: 0.0,
align: Default::default(),
fit: None,
}));
node
}
fn frame_of(children: Vec<RenderNode>) -> RenderNode {
let mut node = RenderNode::new(1);
node.bounds = Rect::new(0.0, 0.0, 200.0, 200.0);
node.children = children.into_iter().map(Rc::new).collect();
node
}
fn frame_through(node: &RenderNode, scratch: &mut FlattenScratch) -> (String, usize) {
let (mut commands, mut layers) = (Vec::new(), Vec::new());
let carried = flatten_root_into(node, &mut commands, &mut layers, scratch);
(
format!("{carried:?}\n{commands:?}\n{layers:?}"),
layers.len(),
)
}
#[test]
fn the_capacity_reported_is_the_room_the_buffers_hold() {
let mut scratch = FlattenScratch::default();
scratch.groups.push(LayerBuckets::default());
let rooms = [[2, 3, 5, 7, 11], [13, 17, 19, 23, 29]];
for (group, rooms) in scratch.groups.iter_mut().zip(rooms) {
for (bucket, room) in group.buckets_mut().into_iter().zip(rooms) {
bucket.reserve_exact(room);
assert_eq!(
bucket.capacity(),
room,
"the allocator gave a bucket more than it asked for, so \
the sum below is arithmetic about the wrong numbers"
);
}
}
let commands: usize = rooms.iter().flatten().sum();
for (group, room) in scratch.groups.iter_mut().zip([31, 37]) {
let bounds = &mut group.bounds[RenderLayer::Shapes as usize];
bounds.rects.reserve_exact(room);
assert!(
bounds.rects.spilled() && bounds.rects.capacity() == room,
"the rects were meant to spill onto the heap with exactly the \
room asked for"
);
}
let clip = PlacedShape::from_clip(
&ClipRegion {
rect: CARD,
corner_radius: CornerRadii::from(0.0),
curvature: 1.0,
},
Transform::IDENTITY,
);
let placed = 10;
for _ in 0..placed {
scratch.clips.place(clip, None);
}
assert_eq!(
commands, 129,
"the primes above add up to this and to nothing else, which is \
what the sum below is being held to"
);
let reported = scratch.capacity();
assert_eq!(
reported.commands, commands,
"the commands reported are not the five buckets of both groups \
added together"
);
assert_eq!(
reported.rects,
31 + 37,
"the rects reported are not the two spilled buffers added \
together — the eight that never left the stack are not room a \
frame won"
);
assert_eq!(
reported.groups,
scratch.groups.capacity(),
"the groups reported are not the groups the scratch can hold"
);
assert!(
reported.clips >= placed,
"the clip tree holds {placed} clips and reports room for \
{} of them",
reported.clips
);
}
#[test]
fn a_scratch_that_drew_a_frame_flattens_the_next_like_a_fresh_one() {
let first_frame = || {
let mut node = box_node(1, CARD);
node.clip = Some(ClipRegion {
rect: CARD,
corner_radius: CornerRadii::uniform(4.0),
curvature: 1.0,
});
node.commands.push(Rc::new(DrawCommand::InputRegion {
rect: CARD,
corner_radii: CornerRadii::from(0.0),
curvature: 1.0,
takes: true,
}));
node.overlay_commands
.push(Rc::new(DrawCommand::rounded_rect(CARD, Color::WHITE, 0.0)));
node
};
let second_frame = || {
let mut badge = box_node(3, NEAR);
badge.local_transform = Transform::rotate_degrees(45.0);
frame_of(vec![
box_node(6, CARD),
badge,
text_node(4, NEAR),
box_node(7, CARD),
])
};
let mut reused = FlattenScratch::default();
frame_through(&first_frame(), &mut reused);
let after_a_frame = frame_through(&second_frame(), &mut reused);
let from_scratch = frame_through(&second_frame(), &mut FlattenScratch::default());
assert_eq!(
from_scratch.1, 1,
"the second frame is meant to be one group of its own accord — \
split it here and the comparison below stops asking anything"
);
assert_eq!(
after_a_frame.0, from_scratch.0,
"the second frame came out differently because the first one had \
been through the same scratch"
);
}
#[test]
fn a_group_opened_again_is_as_empty_as_a_new_one() {
let splitting =
|last: RenderNode| frame_of(vec![text_node(2, NEAR), box_node(3, NEAR), last]);
let first_frame = || splitting(text_node(4, CARD));
let second_frame = || splitting(frame_of(vec![text_node(5, NEAR), box_node(6, CARD)]));
let mut reused = FlattenScratch::default();
let first = frame_through(&first_frame(), &mut reused);
let after_a_frame = frame_through(&second_frame(), &mut reused);
let from_scratch = frame_through(&second_frame(), &mut FlattenScratch::default());
assert_eq!(
(first.1, from_scratch.1),
(2, 2),
"both frames are meant to split once and only once — a frame that \
does not split never reaches the second group, and one that \
splits twice reached it for a reason this test cannot name"
);
assert_eq!(
after_a_frame.0, from_scratch.0,
"the second frame came out differently because it was given the \
first frame's second group back"
);
}
#[test]
fn three_frames_leave_the_scratch_the_size_of_one() {
let scene = || {
let mut node = frame_of(vec![text_node(2, CARD), box_node(3, CARD)]);
node.clip = Some(ClipRegion {
rect: CARD,
corner_radius: CornerRadii::uniform(4.0),
curvature: 1.0,
});
node
};
let mut scratch = FlattenScratch::default();
frame_through(&scene(), &mut scratch);
frame_through(&scene(), &mut scratch);
let (third, groups) = frame_through(&scene(), &mut scratch);
let from_scratch = frame_through(&scene(), &mut FlattenScratch::default());
assert_eq!(
groups, 2,
"the tint is meant to regress over the label — one group here and \
this test is about a scratch that never opens a second one"
);
assert_eq!(
(scratch.groups.len(), scratch.clips.mark()),
(groups, 1),
"three frames of the same two groups and the same one clip left \
the scratch holding more than one frame's worth"
);
assert_eq!(
third, from_scratch.0,
"the third frame's groups came back holding something a fresh \
scratch does not put in them"
);
}
}
#[cfg(test)]
mod world_geometry_tests {
use super::*;
use crate::renderer::CornerRadii;
use crate::renderer::tree::ClipRegion;
use crate::widgets::Color;
#[test]
fn a_turned_clip_keeps_its_shape_and_its_box_does_not() {
let declared = ClipRegion {
rect: Rect::new(0.0, 0.0, 80.0, 80.0),
corner_radius: CornerRadii::uniform(16.0),
curvature: 1.0,
};
let placement = Transform::rotate_degrees(45.0).center_at(40.0, 40.0);
let clip = PlacedShape::from_clip(&declared, placement);
assert_eq!(clip.rect, declared.rect, "the declaration, untouched");
assert_eq!(
clip.radii.top_left, 16.0,
"and the radii with it — in the clip's own space a turned corner is \
still a circle, so nothing has to be approximated"
);
let boxed = clip.world_aabb();
assert!(
boxed.width > 112.0 && boxed.width < 114.0,
"80·√2 ≈ 113.1, and this is what the clip used to be reduced to: \
wide enough that a 100-wide child is not touched, which is how \
`Overflow::Hidden` stopped hiding — got {}",
boxed.width
);
}
#[test]
fn a_fragment_comes_back_to_the_space_the_clip_was_written_in() {
let placement = Transform::rotate_degrees(45.0).center_at(40.0, 40.0);
let clip = PlacedShape {
rect: Rect::new(0.0, 0.0, 80.0, 80.0),
radii: CornerRadii::uniform(0.0),
curvature: 1.0,
placement,
};
let back = clip.to_local(1.0).expect("a rotation is invertible");
let (wx, wy) = placement.transform_point(80.0, 80.0);
let (cx, cy) = back.transform_point(wx, wy);
assert!(
(cx - 80.0).abs() < 0.01 && (cy - 80.0).abs() < 0.01,
"the far corner comes back to the far corner, got ({cx}, {cy})"
);
let boxed = clip.world_aabb();
let (cx, cy) = back.transform_point(boxed.x + 1.0, boxed.y + 1.0);
assert!(
cx < 0.0 || cy < 0.0 || cx > 80.0 || cy > 80.0,
"a point in the box but not in the shape lands outside the clip \
rect, so the shader cuts it — got ({cx}, {cy})"
);
}
#[test]
fn the_surface_scale_is_undone_once() {
let clip = PlacedShape {
rect: Rect::new(0.0, 0.0, 100.0, 50.0),
radii: CornerRadii::uniform(0.0),
curvature: 1.0,
placement: Transform::translate(10.0, 20.0),
};
let back = clip.to_local(2.0).expect("a translation is invertible");
let (cx, cy) = back.transform_point(220.0, 140.0);
assert!(
(cx - 100.0).abs() < 0.01 && (cy - 50.0).abs() < 0.01,
"the far corner in physical pixels comes back to the far corner of \
the logical rect, got ({cx}, {cy})"
);
}
#[test]
fn the_identity_fast_path_still_undoes_the_scale() {
let unplaced = PlacedShape {
rect: Rect::new(0.0, 0.0, 100.0, 50.0),
radii: CornerRadii::uniform(0.0),
curvature: 1.0,
placement: Transform::IDENTITY,
};
for scale in [2.0, 1.5, 0.5] {
let back = unplaced.to_local(scale).expect("invertible");
let (x, y) = back.transform_point(100.0 * scale, 50.0 * scale);
assert!(
(x - 100.0).abs() < 0.01 && (y - 50.0).abs() < 0.01,
"the far corner in physical pixels comes back to the far corner \
of the logical rect at scale {scale}, got ({x}, {y})"
);
let placed = PlacedShape {
placement: Transform::translate(11.0, -3.0),
..unplaced
};
let long = placed.to_local(scale).expect("invertible");
assert_eq!(
(back.a(), back.b(), back.c(), back.d()),
(long.a(), long.b(), long.c(), long.d()),
"the two routes shrink by the same factor at scale {scale}; \
only the offset differs, because one clip has moved"
);
}
}
#[test]
fn a_collapsed_clip_has_no_way_back() {
let clip = PlacedShape {
rect: Rect::new(0.0, 0.0, 100.0, 100.0),
radii: CornerRadii::uniform(0.0),
curvature: 1.0,
placement: Transform::scale(0.0),
};
assert!(clip.to_local(1.0).is_none());
}
#[test]
fn an_overlay_is_clipped_in_the_space_of_the_node_it_belongs_to() {
let mut node = RenderNode::new(1);
node.bounds = Rect::new(0.0, 0.0, 80.0, 80.0);
node.local_transform = Transform::rotate_degrees(45.0);
node.overlay_clip = Some(ClipRegion {
rect: Rect::new(0.0, 0.0, 80.0, 80.0),
corner_radius: CornerRadii::uniform(16.0),
curvature: 1.0,
});
node.overlay_commands
.push(Rc::new(DrawCommand::rounded_rect(
Rect::new(10.0, 10.0, 20.0, 20.0),
Color::WHITE,
0.0,
)));
let (mut commands, mut layers) = (Vec::new(), Vec::new());
flatten_root_into(
&node,
&mut commands,
&mut layers,
&mut FlattenScratch::default(),
);
let overlay = commands
.iter()
.find(|c| c.layer == RenderLayer::Overlay)
.expect("the ripple is in the frame");
let clip = overlay.clip().expect("and it is clipped").first;
assert_eq!(
clip.rect,
Rect::new(0.0, 0.0, 80.0, 80.0),
"the clip as declared, in the node's own coordinates — not a box \
around it in world ones"
);
assert_eq!(clip.radii.top_left, 16.0, "and its corners, uncrushed");
assert_eq!(
clip.placement, overlay.world_transform,
"placed by the very transform the ripple is drawn through, which is \
what makes the ripple follow the shape instead of an axis-aligned \
box around it"
);
assert!(
clip.world_aabb().width > 112.0,
"80·√2 ≈ 113.1, got {}",
clip.world_aabb().width
);
}
#[test]
fn a_replayed_clip_moves_on_both_axes() {
let mut child = RenderNode::new(2);
child.bounds = Rect::new(0.0, 0.0, 40.0, 40.0);
child.clip = Some(ClipRegion {
rect: Rect::new(0.0, 0.0, 40.0, 40.0),
corner_radius: CornerRadii::uniform(0.0),
curvature: 1.0,
});
child.commands.push(Rc::new(DrawCommand::rounded_rect(
Rect::new(0.0, 0.0, 40.0, 40.0),
Color::WHITE,
0.0,
)));
let mut root = RenderNode::new(1);
root.bounds = Rect::new(0.0, 0.0, 200.0, 200.0);
root.local_transform = Transform::translate(10.0, 20.0);
root.children.push(Rc::new(child));
let replayed = |node: &RenderNode| {
let (mut commands, mut layers) = (Vec::new(), Vec::new());
flatten_root_into(
node,
&mut commands,
&mut layers,
&mut FlattenScratch::default(),
);
let cmd = commands.first().expect("the child draws");
let clip = cmd.clip().expect("the child clips").first;
(
(clip.placement.tx(), clip.placement.ty()),
(cmd.world_transform.tx(), cmd.world_transform.ty()),
)
};
assert_eq!(replayed(&root), ((10.0, 20.0), (10.0, 20.0)));
root.local_transform = Transform::translate(35.0, 65.0);
root.repainted.set(false);
for c in &root.children {
c.repainted.set(false);
}
assert_eq!(
replayed(&root),
((35.0, 65.0), (35.0, 65.0)),
"the clip and the command are both where the subtree now is, on \
each axis independently"
);
}
#[test]
fn a_replayed_subtree_puts_its_clips_back_under_each_other() {
fn clipped(id: u64, rect: Rect, children: Vec<Rc<RenderNode>>) -> RenderNode {
let mut node = RenderNode::new(id);
node.bounds = Rect::new(0.0, 0.0, 100.0, 100.0);
node.clip = Some(ClipRegion {
rect,
corner_radius: CornerRadii::uniform(0.0),
curvature: 1.0,
});
node.children = children;
node
}
let scene = |dx: f32, dy: f32| {
let mut drawn = RenderNode::new(5);
drawn.bounds = Rect::new(0.0, 0.0, 100.0, 100.0);
drawn.commands.push(Rc::new(DrawCommand::rounded_rect(
Rect::new(0.0, 0.0, 100.0, 100.0),
Color::WHITE,
0.0,
)));
let inner = clipped(4, Rect::new(0.0, 20.0, 100.0, 60.0), vec![Rc::new(drawn)]);
let outer = clipped(3, Rect::new(0.0, 0.0, 100.0, 60.0), vec![Rc::new(inner)]);
let beside = clipped(2, Rect::new(50.0, 0.0, 50.0, 100.0), Vec::new());
let mut subtree = RenderNode::new(1);
subtree.bounds = Rect::new(0.0, 0.0, 100.0, 100.0);
subtree.children = vec![Rc::new(beside), Rc::new(outer)];
let mut root = clipped(0, Rect::new(0.0, 0.0, 100.0, 100.0), vec![Rc::new(subtree)]);
root.local_transform = Transform::translate(dx, dy);
root
};
let cut = |node: &RenderNode| {
let (mut commands, mut layers) = (Vec::new(), Vec::new());
flatten_root_into(
node,
&mut commands,
&mut layers,
&mut FlattenScratch::default(),
);
commands
.iter()
.find(|c| matches!(&*c.command, DrawCommand::RoundedRect { .. }))
.expect("the box draws")
.clip()
.expect("under three clips")
.first
};
let mut replayed = scene(10.0, 20.0);
let at_rest = cut(&replayed);
assert_eq!(
at_rest.rect,
Rect::new(0.0, 20.0, 100.0, 40.0),
"the box is cut by the two clips over it and not by the one beside \
them"
);
replayed.local_transform = Transform::translate(35.0, 65.0);
fn mark_clean(node: &RenderNode) {
node.repainted.set(false);
for child in &node.children {
mark_clean(child);
}
}
for child in &replayed.children {
mark_clean(child);
}
let moved = cut(&replayed);
assert_ne!(
moved.placement, at_rest.placement,
"the subtree was replayed where it already was, so this says \
nothing about putting it back somewhere else"
);
assert_eq!(
moved,
cut(&scene(35.0, 65.0)),
"the replayed subtree's clips came back under the wrong ones: a \
replay and a flatten of the same scene in the same place are the \
same frame, or the cache is not a cache"
);
}
#[test]
fn an_overlay_is_cut_by_the_clip_above_it() {
let mut child = RenderNode::new(3);
child.bounds = Rect::new(0.0, 0.0, 40.0, 40.0);
child.overlay_clip = Some(ClipRegion {
rect: Rect::new(0.0, 0.0, 40.0, 40.0),
corner_radius: CornerRadii::uniform(0.0),
curvature: 1.0,
});
child
.overlay_commands
.push(Rc::new(DrawCommand::rounded_rect(
Rect::new(0.0, 0.0, 40.0, 40.0),
Color::WHITE,
0.0,
)));
let mut middle = RenderNode::new(2);
middle.bounds = Rect::new(0.0, 0.0, 40.0, 40.0);
middle.children.push(Rc::new(child));
let mut root = RenderNode::new(1);
root.bounds = Rect::new(0.0, 0.0, 200.0, 200.0);
root.clip = Some(ClipRegion {
rect: Rect::new(0.0, 0.0, 40.0, 20.0),
corner_radius: CornerRadii::uniform(10.0),
curvature: 1.0,
});
root.children.push(Rc::new(middle));
let overlay_clip = |node: &RenderNode| {
let (mut commands, mut layers) = (Vec::new(), Vec::new());
flatten_root_into(
node,
&mut commands,
&mut layers,
&mut FlattenScratch::default(),
);
commands
.iter()
.find(|c| c.layer == RenderLayer::Overlay)
.expect("the ripple is in the frame")
.clip()
.expect("and it is clipped")
};
let cut_by_both = |clip: ResolvedClip| {
clip.rest == crate::renderer::NO_CHAIN
&& clip.first.rect == Rect::new(0.0, 0.0, 40.0, 20.0)
&& clip.first.radii == CornerRadii::uniform(10.0)
};
let flattened = overlay_clip(&root);
assert!(
cut_by_both(flattened),
"flattened in full, the overlay is cut by the rounded clip above \
it as well as its own: {flattened:?}"
);
for c in &root.children {
c.repainted.set(false);
}
let replayed = overlay_clip(&root);
assert!(
cut_by_both(replayed),
"replayed, the overlay is cut by the clip above it just the same: \
{replayed:?}"
);
}
#[test]
fn a_translation_only_moves_it() {
let clip = PlacedShape {
rect: Rect::new(0.0, 0.0, 100.0, 60.0),
radii: CornerRadii::uniform(8.0),
curvature: 1.0,
placement: Transform::translate(20.0, 30.0),
};
let world = clip.world_aabb();
assert_eq!((world.x, world.y), (20.0, 30.0));
assert_eq!((world.width, world.height), (100.0, 60.0));
}
}
#[cfg(test)]
mod opacity_tests {
use super::*;
use crate::renderer::NodeId;
use crate::widgets::Color;
fn with_box(id: NodeId, opacity: f32) -> RenderNode {
let mut node = RenderNode::with_bounds(id, Rect::new(0.0, 0.0, 40.0, 40.0));
node.opacity = opacity;
node.commands.push(Rc::new(DrawCommand::rounded_rect(
Rect::new(0.0, 0.0, 40.0, 40.0),
Color::WHITE,
0.0,
)));
node
}
fn flatten(root: &RenderNode) -> Vec<FlattenedCommand> {
let (mut commands, mut layers) = (Vec::new(), Vec::new());
flatten_root_into(
root,
&mut commands,
&mut layers,
&mut FlattenScratch::default(),
);
commands
}
fn frame_under(child: &Rc<RenderNode>, opacity: f32) -> RenderNode {
let mut fading = RenderNode::new(2);
fading.opacity = opacity;
fading.children.push(Rc::clone(child));
let mut root = RenderNode::new(1);
root.children.push(Rc::new(fading));
root
}
fn opacities(commands: &[FlattenedCommand]) -> Vec<f32> {
commands.iter().map(|c| c.opacity).collect()
}
#[test]
fn nested_opacities_multiply() {
let mut inner = with_box(3, 0.5);
inner
.overlay_commands
.push(Rc::new(DrawCommand::circle((5.0, 5.0), 5.0, Color::WHITE)));
let mut outer = with_box(2, 0.5);
outer.children.push(Rc::new(inner));
outer
.overlay_commands
.push(Rc::new(DrawCommand::circle((5.0, 5.0), 5.0, Color::WHITE)));
let mut root = RenderNode::new(1);
root.children.push(Rc::new(outer));
let commands = flatten(&root);
let by_layer = |layer| {
commands
.iter()
.filter(|c| c.layer == layer)
.map(|c| c.opacity)
.collect::<Vec<_>>()
};
assert_eq!(
by_layer(RenderLayer::Shapes),
[0.5, 0.25],
"the outer box at a half, the inner one at a half of that"
);
let mut overlays = by_layer(RenderLayer::Overlay);
overlays.sort_by(f32::total_cmp);
assert_eq!(overlays, [0.25, 0.5], "each overlay at its own node's");
}
#[test]
fn a_clean_subtree_is_replayed_at_the_opacity_it_now_inherits() {
let child = Rc::new(with_box(3, 0.5));
let frame_under = |opacity| frame_under(&child, opacity);
assert_eq!(opacities(&flatten(&frame_under(0.8))), [0.4]);
let entry = child.cached_flatten.borrow().clone().expect("cached");
child.repainted.set(false);
assert_eq!(
opacities(&flatten(&frame_under(0.4))),
[0.2],
"replayed at the old opacity: the fade stopped at the child"
);
assert!(
Rc::ptr_eq(&entry, &child.cached_flatten.borrow().clone().unwrap()),
"flattened again rather than replayed — a replay writes nothing back"
);
}
#[test]
fn an_entry_made_at_zero_is_not_replayed() {
let child = Rc::new(with_box(3, 1.0));
let frame_under = |opacity| frame_under(&child, opacity);
assert_eq!(opacities(&flatten(&frame_under(0.0))), [0.0]);
child.repainted.set(false);
assert_eq!(opacities(&flatten(&frame_under(0.5))), [0.5]);
}
}