use std::ops::Range;
use std::rc::Rc;
use smallvec::SmallVec;
use crate::transform::Transform;
use crate::widgets::Rect;
use super::clip::{ClipIndex, ClipRef, ClipTree, Under};
use super::commands::{CornerRadii, 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 world_transform_origin: Option<(f32, f32)>,
pub layer: RenderLayer,
pub clip: Option<ClipRef>,
}
impl FlattenedCommand {
pub fn clip(&self) -> Option<PlacedShape> {
self.clip.as_ref().map(ClipRef::shape)
}
}
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,
}
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(),
}
}
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, None, 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_world_origin: Option<(f32, f32)>,
parent_clip: Option<ClipIndex>,
out: &mut FlattenScratch,
) -> bool {
let (origin_x, origin_y) = node.pivot.resolve(node.bounds);
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 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)
{
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);
if replayed.clip.is_none() {
replayed.clip = inherited.clone();
}
out.push(replayed);
}
crate::render_stats::record_flatten_cached();
return false;
}
let marks = world_transform
.is_translation_only()
.then(|| (out.mark(), out.clips.mark()));
let world_origin = if !node.local_transform.is_identity() {
let (world_ox, world_oy) = parent_world_transform.transform_point(origin_x, origin_y);
Some((world_ox, world_oy))
} else {
parent_world_origin
};
let (clip_index, effective_clip) = match &node.clip {
Some(clip) => {
let own = PlacedShape::from_clip(clip, world_transform);
let index = out.clips.place(own, Under::Inherited(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,
world_transform_origin: world_origin,
layer,
clip: effective_clip.clone(),
});
}
let mut subtree_partial = node.partial;
for child in &node.children {
subtree_partial |= flatten_node(child, world_transform, world_origin, clip_index, 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, Under::Nothing);
Some(out.clips.reference(index))
}
None => effective_clip,
};
for cmd in &node.overlay_commands {
out.push(FlattenedCommand {
command: Rc::clone(cmd),
world_transform,
world_transform_origin: world_origin,
layer: RenderLayer::Overlay,
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,
})
});
crate::render_stats::record_flatten_full();
subtree_partial
}
const EVERYTHING: Rect = Rect {
x: f32::NEG_INFINITY,
y: f32::NEG_INFINITY,
width: f32::INFINITY,
height: f32::INFINITY,
};
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.blur + shadow.spread);
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,
))
}
pub(super) fn intersect_clips(a: &PlacedShape, b: &PlacedShape) -> PlacedShape {
match rebase(b, a) {
Some(b) => tighten(a, &b),
None => tighten(&a.flattened(), &b.flattened()),
}
}
fn rebase(clip: &PlacedShape, onto: &PlacedShape) -> Option<PlacedShape> {
if clip.placement == onto.placement {
return Some(*clip);
}
let between = onto.placement.inverse()?.then(&clip.placement);
if !between.rect_stays_rect() {
return None;
}
Some(PlacedShape {
rect: between.map_rect(clip.rect),
radii: clip
.radii
.scaled(between.extract_scale())
.permuted(between.corner_permutation()),
curvature: clip.curvature,
placement: onto.placement,
})
}
fn tighten(base: &PlacedShape, other: &PlacedShape) -> PlacedShape {
let min_x = base.rect.x.max(other.rect.x);
let min_y = base.rect.y.max(other.rect.y);
let max_x = (base.rect.x + base.rect.width).min(other.rect.x + other.rect.width);
let max_y = (base.rect.y + base.rect.height).min(other.rect.y + other.rect.height);
let radii = CornerRadii {
top_left: base.radii.top_left.min(other.radii.top_left),
top_right: base.radii.top_right.min(other.radii.top_right),
bottom_right: base.radii.bottom_right.min(other.radii.bottom_right),
bottom_left: base.radii.bottom_left.min(other.radii.bottom_left),
};
let curvature = if base.radii.max() <= other.radii.max() {
base.curvature
} else {
other.curvature
};
PlacedShape {
rect: Rect::new(
min_x,
min_y,
(max_x - min_x).max(0.0),
(max_y - min_y).max(0.0),
),
radii,
curvature,
placement: base.placement,
}
}
#[cfg(test)]
mod tests {
use super::*;
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,
world_transform_origin: None,
layer,
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::{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 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 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(),
}));
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, Under::Nothing);
}
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::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 the_flatter_corner_supplies_the_curve() {
let circular = PlacedShape {
rect: Rect::new(0.0, 0.0, 100.0, 100.0),
radii: CornerRadii::uniform(8.0),
curvature: 1.0,
placement: Transform::IDENTITY,
};
let squircular = PlacedShape {
radii: CornerRadii::uniform(24.0),
curvature: 2.0,
..circular
};
assert_eq!(
intersect_clips(&circular, &squircular).curvature,
1.0,
"the rounder clip cuts less deeply, so its curve is the one on show"
);
assert_eq!(
intersect_clips(&squircular, &circular).curvature,
1.0,
"and the answer does not depend on which was asked first"
);
}
#[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");
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");
(
(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")
};
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 a_replayed_overlay_answers_to_no_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(0.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")
.rect
};
assert_eq!(
overlay_clip(&root),
Rect::new(0.0, 0.0, 40.0, 40.0),
"flattened in full, the overlay takes its own clip and not the \
one above it"
);
for c in &root.children {
c.repainted.set(false);
}
assert_eq!(
overlay_clip(&root),
Rect::new(0.0, 0.0, 40.0, 40.0),
"replayed, the overlay came back cut by the clip above it, which \
it never answered to when it was flattened"
);
}
#[test]
fn two_clips_in_one_space_stay_in_it() {
let placement = Transform::rotate_degrees(30.0);
let outer = PlacedShape {
rect: Rect::new(0.0, 0.0, 100.0, 100.0),
radii: CornerRadii::uniform(8.0),
curvature: 1.0,
placement,
};
let inner = PlacedShape {
rect: Rect::new(20.0, 0.0, 100.0, 60.0),
radii: CornerRadii::uniform(16.0),
curvature: 1.0,
placement,
};
let both = intersect_clips(&outer, &inner);
assert_eq!(both.placement, placement, "still turned by the same thing");
assert_eq!(
(both.rect.x, both.rect.width, both.rect.height),
(20.0, 80.0, 60.0),
"the overlap, exactly, in the space they were both written in"
);
assert_eq!(
both.radii.top_left, 8.0,
"an intersection is only as round as its rounder edge allows"
);
}
#[test]
fn a_clip_is_rebased_through_a_translation() {
let outer = PlacedShape {
rect: Rect::new(0.0, 0.0, 100.0, 100.0),
radii: CornerRadii::uniform(0.0),
curvature: 1.0,
placement: Transform::IDENTITY,
};
let inner = PlacedShape {
rect: Rect::new(0.0, 0.0, 100.0, 100.0),
radii: CornerRadii::uniform(0.0),
curvature: 1.0,
placement: Transform::translate(40.0, 0.0),
};
let both = intersect_clips(&outer, &inner);
assert_eq!(
both.placement,
Transform::IDENTITY,
"rebased onto the first"
);
assert_eq!(
(both.rect.x, both.rect.width),
(40.0, 60.0),
"the inner one, moved into the outer's coordinates, and cut there"
);
}
fn covers(rects: &[crate::region::RegionRect], x: i32, y: i32) -> bool {
rects
.iter()
.any(|r| x >= r.x && y >= r.y && x < r.x + r.width && y < r.y + r.height)
}
#[test]
fn a_region_under_two_clips_a_quarter_turn_apart_rounds_the_right_corner() {
let outer = PlacedShape::placed(
Rect::new(0.0, 0.0, 100.0, 100.0),
CornerRadii::uniform(40.0),
1.0,
Transform::IDENTITY,
);
let inner = PlacedShape::placed(
Rect::new(0.0, 0.0, 100.0, 100.0),
CornerRadii {
top_left: 40.0,
top_right: 0.0,
bottom_right: 0.0,
bottom_left: 0.0,
},
1.0,
Transform::translate(100.0, 0.0).then(&Transform::rotate_degrees(90.0)),
);
let both = intersect_clips(&outer, &inner);
assert!(
(both.rect.width - 100.0).abs() < 0.01 && (both.rect.height - 100.0).abs() < 0.01,
"the square survives the turn whichever way this is answered: {:?}",
both.rect
);
let rects = crate::region::placed_shape_to_rects(both, None);
assert!(
covers(&rects, 3, 3),
"the inner's rounding turned a quarter clockwise off the top-left, \
so this corner is square and the surface still claims it"
);
assert!(
!covers(&rects, 96, 3),
"and the top-right is where the rounding landed, so the region \
gives that wedge up — claim it instead and a click there is sent \
to a surface that does not draw it"
);
}
#[test]
fn a_clip_a_quarter_turn_away_is_rebased_not_boxed() {
let outer = PlacedShape {
rect: Rect::new(0.0, 0.0, 100.0, 60.0),
radii: CornerRadii::uniform(20.0),
curvature: 1.0,
placement: Transform::rotate_degrees(30.0),
};
let inner = PlacedShape {
rect: Rect::new(0.0, 0.0, 60.0, 100.0),
radii: CornerRadii {
top_left: 12.0,
top_right: 0.0,
bottom_right: 0.0,
bottom_left: 0.0,
},
curvature: 1.0,
placement: Transform::rotate_degrees(30.0).then(&Transform::rotate_degrees(90.0)),
};
let both = intersect_clips(&outer, &inner);
assert_eq!(
both.placement, outer.placement,
"rebased onto the first, not flattened to the box around both"
);
assert!(
(both.radii.top_right - 12.0).abs() < 1e-3,
"the inner's top-left corner turned a quarter clockwise, so it is \
the top-right one now: {:?}",
both.radii
);
assert_eq!(
(
both.radii.top_left,
both.radii.bottom_right,
both.radii.bottom_left
),
(0.0, 0.0, 0.0),
"and nothing else picked up a radius"
);
}
#[test]
fn a_mirrored_clip_is_rebased_not_boxed() {
let outer = PlacedShape {
rect: Rect::new(0.0, 0.0, 100.0, 100.0),
radii: CornerRadii::uniform(20.0),
curvature: 1.0,
placement: Transform::rotate_degrees(20.0),
};
let inner = PlacedShape {
rect: Rect::new(0.0, 0.0, 80.0, 80.0),
radii: CornerRadii {
top_left: 9.0,
top_right: 0.0,
bottom_right: 0.0,
bottom_left: 5.0,
},
curvature: 1.0,
placement: outer
.placement
.then(&Transform::scale_xy(-1.0, 1.0).then_translate(100.0, 0.0)),
};
let both = intersect_clips(&outer, &inner);
assert_eq!(
both.placement, outer.placement,
"rebased onto the first, not flattened to the box around both"
);
assert!(
(both.radii.top_right - 9.0).abs() < 1e-3
&& (both.radii.bottom_right - 5.0).abs() < 1e-3,
"mirrored across x: the left corners are the right ones now, {:?}",
both.radii
);
assert_eq!((both.radii.top_left, both.radii.bottom_left), (0.0, 0.0));
}
#[test]
fn a_flattened_clip_rounds_by_the_larger_axis() {
let clip = PlacedShape::placed(
Rect::new(0.0, 0.0, 100.0, 100.0),
CornerRadii::uniform(10.0),
1.0,
Transform::scale_xy(3.0, 1.0),
);
assert_eq!(
clip.world_radii().top_left,
30.0,
"the corner is 30 wide and 10 tall; cutting 30 stays inside it, and \
cutting the mean of 17.3 would round less than the shape does"
);
}
#[test]
fn two_clips_turned_differently_fall_back_to_their_boxes() {
let a = PlacedShape {
rect: Rect::new(0.0, 0.0, 80.0, 80.0),
radii: CornerRadii::uniform(0.0),
curvature: 1.0,
placement: Transform::rotate_degrees(45.0),
};
let b = PlacedShape {
rect: Rect::new(0.0, 0.0, 80.0, 80.0),
radii: CornerRadii::uniform(0.0),
curvature: 1.0,
placement: Transform::rotate_degrees(10.0),
};
let both = intersect_clips(&a, &b);
assert_eq!(
both.placement,
Transform::IDENTITY,
"world coordinates, where a box is a box"
);
let (a_box, b_box) = (a.world_aabb(), b.world_aabb());
let expected_right = (a_box.x + a_box.width).min(b_box.x + b_box.width);
assert!(
(both.rect.x + both.rect.width - expected_right).abs() < 0.01,
"the two boxes, intersected"
);
}
#[test]
fn two_clips_that_miss_leave_nothing() {
let here = PlacedShape {
rect: Rect::new(0.0, 0.0, 50.0, 50.0),
radii: CornerRadii::uniform(0.0),
curvature: 1.0,
placement: Transform::IDENTITY,
};
let far = PlacedShape {
rect: Rect::new(500.0, 500.0, 50.0, 50.0),
radii: CornerRadii::uniform(0.0),
curvature: 1.0,
placement: Transform::IDENTITY,
};
let both = intersect_clips(&here, &far);
assert_eq!((both.rect.width, both.rect.height), (0.0, 0.0));
}
#[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));
assert_eq!(clip.world_radii().max(), 8.0, "and the corners with it");
}
}