use kurbo::{Affine, BezPath, Point, Rect};
use peniko::{Brush, Color, ImageData};
use crate::glyph::GlyphRun;
use crate::scene::{Command, CornerRadii, DashPattern, PathStyle, Scene};
use crate::shader::ShaderProgram;
pub struct SceneBuilder<'a> {
scene: &'a mut Scene,
}
impl<'a> SceneBuilder<'a> {
pub fn new(scene: &'a mut Scene) -> Self {
scene.transform_stack.clear();
scene.transform_stack.push(Affine::IDENTITY);
Self { scene }
}
pub fn current_transform(&self) -> Affine {
*self
.scene
.transform_stack
.last()
.expect("transform stack is never empty")
}
pub fn push_transform(&mut self, transform: Affine) {
let composed = self.current_transform() * transform;
self.scene.transform_stack.push(composed);
}
pub fn pop_transform(&mut self) {
if self.scene.transform_stack.len() > 1 {
self.scene.transform_stack.pop();
}
}
pub fn fill_rect(&mut self, rect: Rect, brush: Brush) {
let transform = self.current_transform();
self.scene.push(Command::FillRect {
rect,
brush,
transform,
});
}
pub fn fill_rounded_rect(&mut self, rect: Rect, radius: f64, brush: Brush) {
self.fill_rounded_rect_radii(rect, CornerRadii::from(radius), brush);
}
pub fn fill_rounded_rect_radii(&mut self, rect: Rect, radii: CornerRadii, brush: Brush) {
let transform = self.current_transform();
self.scene.push(Command::RoundedRect {
rect,
radii,
brush,
transform,
});
}
pub fn stroke_line(&mut self, p0: Point, p1: Point, width: f64, brush: Brush) {
let transform = self.current_transform();
self.scene.push(Command::Line {
p0,
p1,
width,
brush,
transform,
});
}
pub fn draw_glyph_run(&mut self, mut glyph_run: GlyphRun) {
glyph_run.transform = self.current_transform() * glyph_run.transform;
self.scene.push(Command::GlyphRun(glyph_run));
}
pub fn push_clip(&mut self, rect: Rect) {
let transform = self.current_transform();
self.scene.push(Command::PushClip { rect, transform });
}
pub fn push_clip_rounded(&mut self, rect: Rect, radius: f64) {
self.push_clip_rounded_radii(rect, CornerRadii::from(radius));
}
pub fn push_clip_rounded_radii(&mut self, rect: Rect, radii: CornerRadii) {
let transform = self.current_transform();
self.scene.push(Command::PushClipRounded {
rect,
radii,
transform,
});
}
pub fn pop_clip(&mut self) {
self.scene.push(Command::PopClip);
}
pub fn draw_image(&mut self, data: &ImageData, dest: Rect) {
let transform = self.current_transform();
self.scene.push(Command::Image {
data: data.clone(),
dest,
transform,
});
}
pub fn draw_blurred_rounded_rect(
&mut self,
rect: Rect,
radius: f64,
std_dev: f64,
color: Color,
) {
self.draw_blurred_rounded_rect_radii(rect, CornerRadii::from(radius), std_dev, color);
}
pub fn draw_blurred_rounded_rect_radii(
&mut self,
rect: Rect,
radii: CornerRadii,
std_dev: f64,
color: Color,
) {
let transform = self.current_transform();
self.scene.push(Command::BlurredRoundedRect {
rect,
radii,
std_dev,
color,
transform,
});
}
pub fn push_layer(&mut self, rect: Rect, alpha: f32) {
let transform = self.current_transform();
self.scene.push(Command::PushLayer {
rect,
alpha,
transform,
});
}
pub fn pop_layer(&mut self) {
self.scene.push(Command::PopLayer);
}
pub fn clear_rect(&mut self, rect: Rect) {
let transform = self.current_transform();
self.scene.push(Command::ClearRect { rect, transform });
}
pub fn fill_path(&mut self, path: BezPath, brush: Brush) {
let transform = self.current_transform();
self.scene.push(Command::Path {
path,
style: PathStyle::Fill,
brush,
transform,
});
}
pub fn stroke_path(&mut self, path: BezPath, width: f64, brush: Brush) {
let transform = self.current_transform();
self.scene.push(Command::Path {
path,
style: PathStyle::Stroke { width, dash: None },
brush,
transform,
});
}
pub fn stroke_path_dashed(
&mut self,
path: BezPath,
width: f64,
dash: DashPattern,
brush: Brush,
) {
let transform = self.current_transform();
self.scene.push(Command::Path {
path,
style: PathStyle::Stroke {
width,
dash: Some(dash),
},
brush,
transform,
});
}
pub fn push_snapshot(&mut self, key: u64, rect: Rect, alpha: f32, scale: f64) {
let transform = self.current_transform();
self.scene.snapshot_depth += 1;
self.scene.push(Command::PushSnapshot {
key,
rect,
alpha,
scale,
transform,
});
}
pub fn pop_snapshot(&mut self) {
if self.scene.snapshot_depth > 0 {
self.scene.snapshot_depth -= 1;
self.scene.push(Command::PopSnapshot);
}
}
pub fn draw_shader(&mut self, program: &ShaderProgram, dest: Rect, time: f32) {
let transform = self.current_transform();
self.scene.push(Command::ShaderQuad {
program: program.clone(),
dest,
transform,
time,
});
}
pub fn scene_texture(&mut self, id: u64, dest: Rect) {
let transform = self.current_transform();
self.scene.push(Command::SceneTexture {
id,
dest,
transform,
});
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::glyph::{FontHandle, Glyph};
use peniko::FontData;
use peniko::color::palette::css::RED;
fn red_brush() -> Brush {
Brush::Solid(RED)
}
#[test]
fn fill_rect_uses_identity_transform_by_default() {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
let rect = Rect::new(0.0, 0.0, 10.0, 20.0);
builder.fill_rect(rect, red_brush());
let commands = scene.commands();
assert_eq!(commands.len(), 1);
match &commands[0] {
Command::FillRect {
rect: got_rect,
transform,
..
} => {
assert_eq!(*got_rect, rect);
assert_eq!(*transform, Affine::IDENTITY);
}
other => panic!("expected FillRect, got {other:?}"),
}
}
#[test]
fn transform_stack_composes_for_fill_rect() {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
let translate = Affine::translate((10.0, 5.0));
let scale = Affine::scale(2.0);
builder.push_transform(translate);
builder.push_transform(scale);
let rect = Rect::new(0.0, 0.0, 1.0, 1.0);
builder.fill_rect(rect, red_brush());
builder.pop_transform();
builder.fill_rect(rect, red_brush());
builder.pop_transform();
builder.pop_transform();
builder.fill_rect(rect, red_brush());
let expected = [translate * scale, translate, Affine::IDENTITY];
let commands = scene.commands();
assert_eq!(commands.len(), expected.len());
for (command, expected_transform) in commands.iter().zip(expected) {
match command {
Command::FillRect { transform, .. } => assert_eq!(*transform, expected_transform),
other => panic!("expected FillRect, got {other:?}"),
}
}
}
#[test]
fn glyph_run_round_trips_its_fields() {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
let font = FontHandle::new(FontData::new(peniko::Blob::from(Vec::<u8>::new()), 0));
let glyphs = vec![
Glyph {
id: 1,
x: 0.0,
y: 0.0,
},
Glyph {
id: 2,
x: 8.0,
y: 0.0,
},
];
let run = GlyphRun {
font: font.clone(),
font_size: 16.0,
brush: red_brush(),
transform: Affine::IDENTITY,
glyphs: glyphs.clone(),
};
builder.draw_glyph_run(run);
match &scene.commands()[0] {
Command::GlyphRun(got) => {
assert_eq!(got.font_size, 16.0);
assert_eq!(got.glyphs, glyphs);
assert_eq!(got.transform, Affine::IDENTITY);
}
other => panic!("expected GlyphRun, got {other:?}"),
}
}
#[test]
fn glyph_run_composes_with_current_transform() {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
let translate = Affine::translate((3.0, 4.0));
builder.push_transform(translate);
let font = FontHandle::new(FontData::new(peniko::Blob::from(Vec::<u8>::new()), 0));
let run = GlyphRun {
font,
font_size: 12.0,
brush: red_brush(),
transform: Affine::IDENTITY,
glyphs: vec![],
};
builder.draw_glyph_run(run);
match &scene.commands()[0] {
Command::GlyphRun(got) => assert_eq!(got.transform, translate),
other => panic!("expected GlyphRun, got {other:?}"),
}
}
#[test]
fn push_pop_clip_emit_commands() {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
let rect = Rect::new(0.0, 0.0, 5.0, 5.0);
builder.push_clip(rect);
builder.pop_clip();
let commands = scene.commands();
assert_eq!(commands.len(), 2);
assert!(matches!(commands[0], Command::PushClip { .. }));
assert!(matches!(commands[1], Command::PopClip));
}
#[test]
fn rounded_rect_round_trips_radius_and_transform() {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
let translate = Affine::translate((2.0, 3.0));
builder.push_transform(translate);
let rect = Rect::new(0.0, 0.0, 10.0, 8.0);
builder.fill_rounded_rect(rect, 4.0, red_brush());
match &scene.commands()[0] {
Command::RoundedRect {
rect: got,
radii,
transform,
..
} => {
assert_eq!(*got, rect);
assert_eq!(*radii, CornerRadii::uniform(4.0));
assert_eq!(*transform, translate);
}
other => panic!("expected RoundedRect, got {other:?}"),
}
}
#[test]
fn rounded_rect_radii_round_trips_each_corner() {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
let rect = Rect::new(0.0, 0.0, 10.0, 8.0);
let radii = CornerRadii::new(1.0, 2.0, 3.0, 4.0);
builder.fill_rounded_rect_radii(rect, radii, red_brush());
match &scene.commands()[0] {
Command::RoundedRect {
rect: got,
radii: r,
..
} => {
assert_eq!(*got, rect);
assert_eq!(*r, radii);
}
other => panic!("expected RoundedRect, got {other:?}"),
}
}
#[test]
fn stroke_line_round_trips_endpoints_and_width() {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
let p0 = Point::new(1.0, 2.0);
let p1 = Point::new(9.0, 12.0);
builder.stroke_line(p0, p1, 2.5, red_brush());
match &scene.commands()[0] {
Command::Line {
p0: g0,
p1: g1,
width,
transform,
..
} => {
assert_eq!(*g0, p0);
assert_eq!(*g1, p1);
assert_eq!(*width, 2.5);
assert_eq!(*transform, Affine::IDENTITY);
}
other => panic!("expected Line, got {other:?}"),
}
}
#[test]
fn push_clip_captures_current_transform() {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
let scale = Affine::scale(2.0);
builder.push_transform(scale);
builder.push_clip(Rect::new(0.0, 0.0, 5.0, 5.0));
match &scene.commands()[0] {
Command::PushClip { transform, .. } => assert_eq!(*transform, scale),
other => panic!("expected PushClip, got {other:?}"),
}
}
#[test]
fn push_clip_rounded_pops_through_the_shared_pop_clip() {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
builder.push_clip_rounded(Rect::new(0.0, 0.0, 5.0, 5.0), 2.0);
builder.pop_clip();
let commands = scene.commands();
assert_eq!(commands.len(), 2);
assert!(matches!(commands[0], Command::PushClipRounded { .. }));
assert!(matches!(commands[1], Command::PopClip));
}
#[test]
fn push_clip_rounded_round_trips_radius_and_captures_current_transform() {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
let scale = Affine::scale(2.0);
builder.push_transform(scale);
let rect = Rect::new(0.0, 0.0, 40.0, 40.0);
builder.push_clip_rounded(rect, 8.0);
match &scene.commands()[0] {
Command::PushClipRounded {
rect: got,
radii,
transform,
} => {
assert_eq!(*got, rect);
assert_eq!(*radii, CornerRadii::uniform(8.0));
assert_eq!(*transform, scale);
}
other => panic!("expected PushClipRounded, got {other:?}"),
}
}
#[test]
fn push_clip_rounded_radii_round_trips_each_corner() {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
let rect = Rect::new(0.0, 0.0, 40.0, 40.0);
let radii = CornerRadii::new(8.0, 8.0, 0.0, 0.0);
builder.push_clip_rounded_radii(rect, radii);
builder.pop_clip();
let commands = scene.commands();
assert_eq!(commands.len(), 2);
match &commands[0] {
Command::PushClipRounded { radii: r, .. } => assert_eq!(*r, radii),
other => panic!("expected PushClipRounded, got {other:?}"),
}
assert!(matches!(commands[1], Command::PopClip));
}
#[test]
fn nested_rounded_clip_and_layer_preserve_push_pop_ordering() {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
let clip_rect = Rect::new(0.0, 0.0, 100.0, 100.0);
let layer_rect = Rect::new(10.0, 10.0, 50.0, 50.0);
builder.push_clip_rounded(clip_rect, 12.0);
builder.push_layer(layer_rect, 0.75);
builder.fill_rect(Rect::new(0.0, 0.0, 1.0, 1.0), red_brush());
builder.pop_layer();
builder.pop_clip();
let commands = scene.commands();
assert_eq!(commands.len(), 5);
assert!(matches!(commands[0], Command::PushClipRounded { .. }));
assert!(matches!(commands[1], Command::PushLayer { .. }));
assert!(matches!(commands[2], Command::FillRect { .. }));
assert!(matches!(commands[3], Command::PopLayer));
assert!(matches!(commands[4], Command::PopClip));
}
fn two_by_two_image() -> ImageData {
ImageData {
data: peniko::Blob::from(vec![0u8; 2 * 2 * 4]),
format: peniko::ImageFormat::Rgba8,
alpha_type: peniko::ImageAlphaType::Alpha,
width: 2,
height: 2,
}
}
#[test]
fn draw_image_round_trips_data_and_dest_under_identity_transform() {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
let data = two_by_two_image();
let dest = Rect::new(0.0, 0.0, 20.0, 20.0);
builder.draw_image(&data, dest);
match &scene.commands()[0] {
Command::Image {
data: got_data,
dest: got_dest,
transform,
} => {
assert_eq!(*got_data, data);
assert_eq!(*got_dest, dest);
assert_eq!(*transform, Affine::IDENTITY);
}
other => panic!("expected Image, got {other:?}"),
}
}
#[test]
fn draw_image_composes_with_current_transform() {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
let translate = Affine::translate((5.0, 6.0));
builder.push_transform(translate);
let data = two_by_two_image();
builder.draw_image(&data, Rect::new(0.0, 0.0, 4.0, 4.0));
match &scene.commands()[0] {
Command::Image { transform, .. } => assert_eq!(*transform, translate),
other => panic!("expected Image, got {other:?}"),
}
}
#[test]
fn draw_shader_round_trips_program_dest_and_time_under_identity_transform() {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
let program = ShaderProgram::new("fn main() {}");
let dest = Rect::new(0.0, 0.0, 20.0, 20.0);
builder.draw_shader(&program, dest, 1.5);
match &scene.commands()[0] {
Command::ShaderQuad {
program: got_program,
dest: got_dest,
transform,
time,
} => {
assert_eq!(got_program.id(), program.id());
assert_eq!(*got_dest, dest);
assert_eq!(*transform, Affine::IDENTITY);
assert_eq!(*time, 1.5);
}
other => panic!("expected ShaderQuad, got {other:?}"),
}
}
#[test]
fn draw_shader_composes_with_current_transform() {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
let translate = Affine::translate((5.0, 6.0));
builder.push_transform(translate);
let program = ShaderProgram::new("fn main() {}");
builder.draw_shader(&program, Rect::new(0.0, 0.0, 4.0, 4.0), 0.0);
match &scene.commands()[0] {
Command::ShaderQuad { transform, .. } => assert_eq!(*transform, translate),
other => panic!("expected ShaderQuad, got {other:?}"),
}
}
#[test]
fn draw_shader_clones_program_sharing_id() {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
let program = ShaderProgram::new("fn main() {}");
builder.draw_shader(&program, Rect::new(0.0, 0.0, 4.0, 4.0), 0.0);
match &scene.commands()[0] {
Command::ShaderQuad {
program: got_program,
..
} => {
assert_eq!(got_program.id(), program.id());
assert_eq!(got_program.source(), program.source());
}
other => panic!("expected ShaderQuad, got {other:?}"),
}
}
#[test]
fn draw_blurred_rounded_rect_round_trips_fields_under_identity_transform() {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
let rect = Rect::new(0.0, 0.0, 10.0, 8.0);
builder.draw_blurred_rounded_rect(rect, 4.0, 2.5, RED);
match &scene.commands()[0] {
Command::BlurredRoundedRect {
rect: got_rect,
radii,
std_dev,
color,
transform,
} => {
assert_eq!(*got_rect, rect);
assert_eq!(*radii, CornerRadii::uniform(4.0));
assert_eq!(*std_dev, 2.5);
assert_eq!(*color, RED);
assert_eq!(*transform, Affine::IDENTITY);
}
other => panic!("expected BlurredRoundedRect, got {other:?}"),
}
}
#[test]
fn draw_blurred_rounded_rect_composes_with_current_transform() {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
let translate = Affine::translate((1.0, 2.0));
builder.push_transform(translate);
builder.draw_blurred_rounded_rect(Rect::new(0.0, 0.0, 5.0, 5.0), 2.0, 1.0, RED);
match &scene.commands()[0] {
Command::BlurredRoundedRect { transform, .. } => assert_eq!(*transform, translate),
other => panic!("expected BlurredRoundedRect, got {other:?}"),
}
}
#[test]
fn draw_blurred_rounded_rect_radii_round_trips_each_corner() {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
let radii = CornerRadii::new(12.0, 12.0, 0.0, 0.0);
builder.draw_blurred_rounded_rect_radii(Rect::new(0.0, 0.0, 10.0, 8.0), radii, 2.5, RED);
match &scene.commands()[0] {
Command::BlurredRoundedRect { radii: r, .. } => assert_eq!(*r, radii),
other => panic!("expected BlurredRoundedRect, got {other:?}"),
}
}
#[test]
fn push_pop_layer_emit_commands_with_alpha_and_transform() {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
let scale = Affine::scale(2.0);
builder.push_transform(scale);
let rect = Rect::new(0.0, 0.0, 5.0, 5.0);
builder.push_layer(rect, 0.5);
builder.pop_layer();
let commands = scene.commands();
assert_eq!(commands.len(), 2);
match &commands[0] {
Command::PushLayer {
rect: got_rect,
alpha,
transform,
} => {
assert_eq!(*got_rect, rect);
assert_eq!(*alpha, 0.5);
assert_eq!(*transform, scale);
}
other => panic!("expected PushLayer, got {other:?}"),
}
assert!(matches!(commands[1], Command::PopLayer));
}
#[test]
fn push_pop_snapshot_emit_commands_with_key_rect_alpha_scale_and_transform() {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
let translate = Affine::translate((2.0, 3.0));
builder.push_transform(translate);
let rect = Rect::new(0.0, 0.0, 40.0, 40.0);
builder.push_snapshot(7, rect, 0.5, 0.9);
builder.fill_rect(Rect::new(0.0, 0.0, 1.0, 1.0), red_brush());
builder.pop_snapshot();
let commands = scene.commands();
assert_eq!(commands.len(), 3);
match &commands[0] {
Command::PushSnapshot {
key,
rect: got_rect,
alpha,
scale,
transform,
} => {
assert_eq!(*key, 7);
assert_eq!(*got_rect, rect);
assert_eq!(*alpha, 0.5);
assert_eq!(*scale, 0.9);
assert_eq!(*transform, translate);
}
other => panic!("expected PushSnapshot, got {other:?}"),
}
assert!(matches!(commands[1], Command::FillRect { .. }));
assert!(matches!(commands[2], Command::PopSnapshot));
}
#[test]
fn pop_snapshot_without_a_push_is_a_no_op() {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
builder.pop_snapshot();
assert!(scene.commands().is_empty());
assert_eq!(scene.snapshot_depth, 0);
}
#[test]
fn nested_snapshot_brackets_balance() {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
let outer = Rect::new(0.0, 0.0, 100.0, 100.0);
let inner = Rect::new(10.0, 10.0, 50.0, 50.0);
builder.push_snapshot(1, outer, 1.0, 1.0);
builder.push_snapshot(2, inner, 0.8, 1.2);
builder.pop_snapshot();
builder.pop_snapshot();
builder.pop_snapshot();
let commands = scene.commands();
assert_eq!(commands.len(), 4);
assert!(matches!(commands[0], Command::PushSnapshot { key: 1, .. }));
assert!(matches!(commands[1], Command::PushSnapshot { key: 2, .. }));
assert!(matches!(commands[2], Command::PopSnapshot));
assert!(matches!(commands[3], Command::PopSnapshot));
assert_eq!(scene.snapshot_depth, 0);
}
#[test]
fn push_snapshot_increments_depth_and_pop_decrements_it() {
let mut scene = Scene::new();
{
let mut builder = SceneBuilder::new(&mut scene);
builder.push_snapshot(1, Rect::new(0.0, 0.0, 1.0, 1.0), 1.0, 1.0);
}
assert_eq!(scene.snapshot_depth, 1);
{
let mut builder = SceneBuilder::new(&mut scene);
builder.pop_snapshot();
}
assert_eq!(scene.snapshot_depth, 0);
}
#[test]
fn clear_rect_records_command_with_current_transform() {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
let translate = Affine::translate((7.0, 4.0));
builder.push_transform(translate);
let rect = Rect::new(0.0, 0.0, 80.0, 60.0);
builder.clear_rect(rect);
let commands = scene.commands();
assert_eq!(commands.len(), 1);
match &commands[0] {
Command::ClearRect {
rect: got_rect,
transform,
} => {
assert_eq!(*got_rect, rect);
assert_eq!(*transform, translate);
}
other => panic!("expected ClearRect, got {other:?}"),
}
}
#[test]
fn nested_clip_and_layer_preserve_push_pop_ordering() {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
let clip_rect = Rect::new(0.0, 0.0, 100.0, 100.0);
let layer_rect = Rect::new(10.0, 10.0, 50.0, 50.0);
builder.push_clip(clip_rect);
builder.push_layer(layer_rect, 0.75);
builder.fill_rect(Rect::new(0.0, 0.0, 1.0, 1.0), red_brush());
builder.pop_layer();
builder.pop_clip();
let commands = scene.commands();
assert_eq!(commands.len(), 5);
assert!(matches!(commands[0], Command::PushClip { .. }));
assert!(matches!(commands[1], Command::PushLayer { .. }));
assert!(matches!(commands[2], Command::FillRect { .. }));
assert!(matches!(commands[3], Command::PopLayer));
assert!(matches!(commands[4], Command::PopClip));
}
fn triangle_path() -> BezPath {
let mut path = BezPath::new();
path.move_to((0.0, 0.0));
path.line_to((10.0, 0.0));
path.line_to((5.0, 10.0));
path.close_path();
path
}
#[test]
fn fill_path_round_trips_path_and_style_under_identity_transform() {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
let path = triangle_path();
builder.fill_path(path.clone(), red_brush());
match &scene.commands()[0] {
Command::Path {
path: got_path,
style,
transform,
..
} => {
assert_eq!(*got_path, path);
assert_eq!(*style, PathStyle::Fill);
assert_eq!(*transform, Affine::IDENTITY);
}
other => panic!("expected Path, got {other:?}"),
}
}
#[test]
fn stroke_path_round_trips_width_and_style() {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
let path = triangle_path();
builder.stroke_path(path.clone(), 2.5, red_brush());
match &scene.commands()[0] {
Command::Path {
path: got_path,
style,
..
} => {
assert_eq!(*got_path, path);
assert_eq!(
*style,
PathStyle::Stroke {
width: 2.5,
dash: None
}
);
}
other => panic!("expected Path, got {other:?}"),
}
}
#[test]
fn stroke_path_dashed_round_trips_its_pattern() {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
let path = triangle_path();
let dash = DashPattern::new(4.0, 2.0).with_phase(1.0);
builder.stroke_path_dashed(path.clone(), 2.5, dash, red_brush());
match &scene.commands()[0] {
Command::Path {
path: got_path,
style,
..
} => {
assert_eq!(*got_path, path);
assert_eq!(
*style,
PathStyle::Stroke {
width: 2.5,
dash: Some(dash)
}
);
}
other => panic!("expected Path, got {other:?}"),
}
}
#[test]
fn scene_texture_round_trips_id_and_dest_under_identity_transform() {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
let dest = Rect::new(0.0, 0.0, 30.0, 40.0);
builder.scene_texture(42, dest);
match &scene.commands()[0] {
Command::SceneTexture {
id,
dest: got_dest,
transform,
} => {
assert_eq!(*id, 42);
assert_eq!(*got_dest, dest);
assert_eq!(*transform, Affine::IDENTITY);
}
other => panic!("expected SceneTexture, got {other:?}"),
}
}
#[test]
fn scene_texture_composes_with_current_transform() {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
let translate = Affine::translate((5.0, 6.0));
builder.push_transform(translate);
builder.scene_texture(7, Rect::new(0.0, 0.0, 4.0, 4.0));
match &scene.commands()[0] {
Command::SceneTexture { transform, .. } => assert_eq!(*transform, translate),
other => panic!("expected SceneTexture, got {other:?}"),
}
}
#[test]
fn fill_path_composes_with_current_transform() {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
let translate = Affine::translate((3.0, 4.0));
builder.push_transform(translate);
builder.fill_path(triangle_path(), red_brush());
match &scene.commands()[0] {
Command::Path { transform, .. } => assert_eq!(*transform, translate),
other => panic!("expected Path, got {other:?}"),
}
}
}