1use crate::geometry::{Affine, Point, Rect, Size};
10use crate::render::convert::{to_ox_point, to_transform2d};
11use crate::render::{Color, Pixmap};
12use oxideav_core::{
13 Group, ImageRef, Node, Paint, Path, PathNode, Point as OxPoint, Rect as OxRect, Rgba, Stroke,
14 Transform2D, VectorFrame, ViewBox,
15};
16use std::sync::Arc;
17
18const KAPPA: f64 = 0.552_284_749_830_793_4;
21
22#[derive(Clone, Debug, PartialEq)]
27pub enum DrawCmd {
28 Rect {
31 rect: Rect,
32 color: Color,
33 radius: f64,
34 border: f64,
35 },
36 Text {
38 text: String,
39 origin: Point,
40 size: f64,
41 color: Color,
42 },
43 PushClip(Rect),
48 PopClip,
50 PushLayer { transform: Affine, opacity: f32 },
58 PopLayer,
60 Viewport { rect: Rect, id: u64 },
67 Image { rect: Rect, image: Arc<Pixmap> },
69}
70
71#[derive(Clone, Debug)]
77struct ClipFrame {
78 clip: Option<Path>,
79 layer: bool,
81 transform: Transform2D,
82 opacity: f32,
83 nodes: Vec<Node>,
84}
85
86impl ClipFrame {
87 fn base() -> Self {
88 Self {
89 clip: None,
90 layer: false,
91 transform: Transform2D::identity(),
92 opacity: 1.0,
93 nodes: Vec::new(),
94 }
95 }
96}
97
98#[derive(Clone, Debug)]
100pub struct Scene {
101 logical_size: Size,
102 stack: Vec<ClipFrame>,
106 commands: Vec<DrawCmd>,
107 scale: f64,
110}
111
112impl Scene {
113 pub fn new(logical_size: Size) -> Self {
115 Self {
116 logical_size,
117 stack: vec![ClipFrame::base()],
118 commands: Vec::new(),
119 scale: 1.0,
120 }
121 }
122
123 pub fn set_scale(&mut self, scale: f64) {
125 self.scale = scale;
126 }
127
128 pub fn scale(&self) -> f64 {
130 self.scale
131 }
132
133 pub fn draw_image(&mut self, rect: Rect, image: Arc<Pixmap>) {
135 self.emit(Node::Image(ImageRef {
136 frame: Box::new(image.to_video_frame()),
137 bounds: OxRect {
138 x: rect.min_x() as f32,
139 y: rect.min_y() as f32,
140 width: rect.width() as f32,
141 height: rect.height() as f32,
142 },
143 transform: Transform2D::identity(),
144 }));
145 self.commands.push(DrawCmd::Image { rect, image });
146 }
147
148 fn emit(&mut self, node: Node) {
150 self.stack.last_mut().unwrap().nodes.push(node);
152 }
153
154 pub fn push_clip(&mut self, rect: Rect) {
158 self.stack.push(ClipFrame {
159 clip: Some(rect_path(rect)),
160 ..ClipFrame::base()
161 });
162 self.commands.push(DrawCmd::PushClip(rect));
163 }
164
165 pub fn pop_clip(&mut self) {
169 if self.stack.len() <= 1 || self.stack.last().is_some_and(|f| f.layer) {
170 return; }
172 self.close_frame();
173 self.commands.push(DrawCmd::PopClip);
174 }
175
176 pub fn push_layer(&mut self, transform: Affine, opacity: f32) {
185 let opacity = if opacity.is_nan() {
186 1.0
187 } else {
188 opacity.clamp(0.0, 1.0)
189 };
190 self.stack.push(ClipFrame {
191 layer: true,
192 transform: to_transform2d(transform),
193 opacity,
194 ..ClipFrame::base()
195 });
196 self.commands
197 .push(DrawCmd::PushLayer { transform, opacity });
198 }
199
200 pub fn pop_layer(&mut self) {
203 if self.stack.len() <= 1 || self.stack.last().is_some_and(|f| !f.layer) {
204 return;
205 }
206 self.close_frame();
207 self.commands.push(DrawCmd::PopLayer);
208 }
209
210 pub fn has_effects(&self) -> bool {
214 self.commands.iter().any(|c| {
215 matches!(c, DrawCmd::PushLayer { transform, opacity }
216 if !transform.is_identity() || *opacity < 1.0)
217 })
218 }
219
220 fn close_frame(&mut self) {
223 let frame = self.stack.pop().unwrap();
224 let group = Group {
225 children: frame.nodes,
226 clip: frame.clip,
227 transform: frame.transform,
228 opacity: frame.opacity,
229 ..Group::new()
230 };
231 self.emit(Node::Group(group));
232 }
233
234 pub fn commands(&self) -> &[DrawCmd] {
237 &self.commands
238 }
239
240 #[inline]
242 pub fn logical_size(&self) -> Size {
243 self.logical_size
244 }
245
246 #[inline]
248 pub fn len(&self) -> usize {
249 self.stack[0].nodes.len()
250 }
251
252 #[inline]
253 pub fn is_empty(&self) -> bool {
254 self.stack.iter().all(|f| f.nodes.is_empty())
255 }
256
257 pub fn stroke_round_rect(&mut self, rect: Rect, radius: f64, color: Color, width: f64) {
259 let path = round_rect_path(rect, radius);
260 let stroke = Stroke::solid(width as f32, Rgba::from(color));
261 self.emit(Node::Path(PathNode::new(path).with_stroke(stroke)));
262 self.commands.push(DrawCmd::Rect {
263 rect,
264 color,
265 radius,
266 border: width,
267 });
268 }
269
270 pub fn fill_rect(&mut self, rect: Rect, color: Color) {
272 let path = rect_path(rect);
273 self.emit(Node::Path(
274 PathNode::new(path).with_fill(Paint::Solid(color.into())),
275 ));
276 self.commands.push(DrawCmd::Rect {
277 rect,
278 color,
279 radius: 0.0,
280 border: 0.0,
281 });
282 }
283
284 pub fn fill_round_rect(&mut self, rect: Rect, radius: f64, color: Color) {
287 let path = round_rect_path(rect, radius);
288 self.emit(Node::Path(
289 PathNode::new(path).with_fill(Paint::Solid(color.into())),
290 ));
291 self.commands.push(DrawCmd::Rect {
292 rect,
293 color,
294 radius,
295 border: 0.0,
296 });
297 }
298
299 pub fn stroke_rect(&mut self, rect: Rect, color: Color, width: f64) {
301 let path = rect_path(rect);
302 let stroke = Stroke::solid(width as f32, Rgba::from(color));
303 self.emit(Node::Path(PathNode::new(path).with_stroke(stroke)));
304 self.commands.push(DrawCmd::Rect {
305 rect,
306 color,
307 radius: 0.0,
308 border: width,
309 });
310 }
311
312 pub fn fill_viewport(&mut self, rect: Rect, id: u64, placeholder: Color) {
318 let path = rect_path(rect);
319 self.emit(Node::Path(
320 PathNode::new(path).with_fill(Paint::Solid(placeholder.into())),
321 ));
322 self.commands.push(DrawCmd::Viewport { rect, id });
323 }
324
325 pub(crate) fn record_text(&mut self, text: &str, origin: Point, size: f64, color: Color) {
328 self.commands.push(DrawCmd::Text {
329 text: text.to_string(),
330 origin,
331 size,
332 color,
333 });
334 }
335
336 pub fn push_node(&mut self, node: Node) {
340 self.emit(node);
341 }
342
343 pub fn into_vector_frame(self) -> VectorFrame {
349 let size = self.logical_size;
350 self.into_vector_frame_view(Rect::from_xywh(0.0, 0.0, size.width, size.height))
351 }
352
353 pub fn into_vector_frame_region(self, view: Rect) -> VectorFrame {
358 self.into_vector_frame_view(view)
359 }
360
361 fn into_vector_frame_view(mut self, view: Rect) -> VectorFrame {
364 while self.stack.len() > 1 {
366 if self.stack.last().is_some_and(|f| f.layer) {
367 self.pop_layer();
368 } else {
369 self.pop_clip();
370 }
371 }
372 let (w, h) = (view.width() as f32, view.height() as f32);
373 let root = Group {
374 children: self.stack.pop().unwrap().nodes,
375 ..Group::new()
376 };
377 VectorFrame::new(w, h)
378 .with_view_box(ViewBox {
379 min_x: view.min_x() as f32,
380 min_y: view.min_y() as f32,
381 width: w,
382 height: h,
383 })
384 .with_root(root)
385 }
386}
387
388fn rect_path(rect: Rect) -> Path {
389 let (x0, y0, x1, y1) = (rect.min_x(), rect.min_y(), rect.max_x(), rect.max_y());
390 let mut p = Path::new();
391 p.move_to(OxPoint {
392 x: x0 as f32,
393 y: y0 as f32,
394 });
395 p.line_to(OxPoint {
396 x: x1 as f32,
397 y: y0 as f32,
398 });
399 p.line_to(OxPoint {
400 x: x1 as f32,
401 y: y1 as f32,
402 });
403 p.line_to(OxPoint {
404 x: x0 as f32,
405 y: y1 as f32,
406 });
407 p.close();
408 p
409}
410
411fn round_rect_path(rect: Rect, radius: f64) -> Path {
412 let r = radius.max(0.0).min(rect.width().min(rect.height()) / 2.0);
413 if r <= 0.0 {
414 return rect_path(rect);
415 }
416 let (x0, y0, x1, y1) = (rect.min_x(), rect.min_y(), rect.max_x(), rect.max_y());
417 let k = r * KAPPA;
418 use crate::geometry::Point as P;
419 let mut p = Path::new();
420 p.move_to(to_ox_point(P::new(x0 + r, y0)));
422 p.line_to(to_ox_point(P::new(x1 - r, y0)));
423 p.cubic_to(
424 to_ox_point(P::new(x1 - r + k, y0)),
425 to_ox_point(P::new(x1, y0 + r - k)),
426 to_ox_point(P::new(x1, y0 + r)),
427 );
428 p.line_to(to_ox_point(P::new(x1, y1 - r)));
429 p.cubic_to(
430 to_ox_point(P::new(x1, y1 - r + k)),
431 to_ox_point(P::new(x1 - r + k, y1)),
432 to_ox_point(P::new(x1 - r, y1)),
433 );
434 p.line_to(to_ox_point(P::new(x0 + r, y1)));
435 p.cubic_to(
436 to_ox_point(P::new(x0 + r - k, y1)),
437 to_ox_point(P::new(x0, y1 - r + k)),
438 to_ox_point(P::new(x0, y1 - r)),
439 );
440 p.line_to(to_ox_point(P::new(x0, y0 + r)));
441 p.cubic_to(
442 to_ox_point(P::new(x0, y0 + r - k)),
443 to_ox_point(P::new(x0 + r - k, y0)),
444 to_ox_point(P::new(x0 + r, y0)),
445 );
446 p.close();
447 p
448}
449
450#[cfg(test)]
451mod tests {
452 use super::*;
453 use crate::geometry::Point;
454
455 #[test]
458 fn draw_image_composites_straight_alpha() {
459 use crate::geometry::{PhysicalSize, ScaleFactor};
460 use crate::render::software::SoftwareRenderer;
461
462 let size = PhysicalSize::new(4, 4);
463 let mut data = Vec::new();
464 for _ in 0..8 {
465 data.extend_from_slice(&[200, 100, 50, 255]); }
467 for _ in 0..8 {
468 data.extend_from_slice(&[200, 100, 50, 0]); }
470 let image = Arc::new(Pixmap::from_rgba8(size, data));
471 let mut scene = Scene::new(Size::new(8.0, 8.0));
472 scene.fill_rect(Rect::from_xywh(0.0, 0.0, 8.0, 8.0), Color::rgb(0, 0, 255));
473 scene.draw_image(Rect::from_xywh(2.0, 2.0, 4.0, 4.0), image);
474 assert!(matches!(
475 scene.commands().last(),
476 Some(DrawCmd::Image { .. })
477 ));
478
479 let out = SoftwareRenderer::new().render(scene, ScaleFactor::IDENTITY);
480 assert_eq!(out.pixel(0, 0), Some([0, 0, 255, 255]), "outside the image");
481 assert_eq!(out.pixel(3, 2), Some([200, 100, 50, 255]), "opaque part");
482 assert_eq!(out.pixel(3, 5), Some([0, 0, 255, 255]), "clear part");
483 }
484
485 #[test]
486 fn scene_lowers_to_frame_with_viewbox() {
487 let mut scene = Scene::new(Size::new(200.0, 100.0));
488 scene.fill_rect(Rect::from_xywh(10.0, 10.0, 50.0, 50.0), Color::WHITE);
489 assert_eq!(scene.len(), 1);
490 let frame = scene.into_vector_frame();
491 assert_eq!((frame.width, frame.height), (200.0, 100.0));
492 let vb = frame.view_box.expect("view box set");
493 assert_eq!((vb.width, vb.height), (200.0, 100.0));
494 assert_eq!(frame.root.children.len(), 1);
495 }
496
497 #[test]
498 fn records_structured_draw_commands() {
499 let mut scene = Scene::new(Size::new(100.0, 100.0));
500 scene.fill_rect(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::WHITE);
501 scene.fill_round_rect(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), 4.0, Color::BLACK);
502 scene.stroke_rect(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::WHITE, 2.0);
503 let cmds = scene.commands();
504 assert_eq!(cmds.len(), 3);
505 assert!(
506 matches!(cmds[0], DrawCmd::Rect { radius, border, .. } if radius == 0.0 && border == 0.0)
507 );
508 assert!(matches!(cmds[1], DrawCmd::Rect { radius, .. } if radius == 4.0));
509 assert!(matches!(cmds[2], DrawCmd::Rect { border, .. } if border == 2.0));
510 }
511
512 #[test]
513 fn push_pop_clip_nests_a_clipped_group() {
514 let mut scene = Scene::new(Size::new(200.0, 200.0));
515 scene.fill_rect(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::WHITE); scene.push_clip(Rect::from_xywh(20.0, 20.0, 50.0, 50.0));
517 scene.fill_rect(Rect::from_xywh(25.0, 25.0, 100.0, 100.0), Color::BLACK); scene.fill_rect(Rect::from_xywh(30.0, 30.0, 5.0, 5.0), Color::BLACK); scene.pop_clip();
520 assert_eq!(scene.len(), 2);
522 let cmds = scene.commands();
524 assert!(matches!(cmds[1], DrawCmd::PushClip(_)));
525 assert!(matches!(cmds[4], DrawCmd::PopClip));
526 let frame = scene.into_vector_frame();
527 assert_eq!(frame.root.children.len(), 2);
529 let clipped = match &frame.root.children[1] {
530 Node::Group(g) => g,
531 _ => panic!("expected a clipped group as the second child"),
532 };
533 assert!(clipped.clip.is_some(), "nested group carries the clip path");
534 assert_eq!(clipped.children.len(), 2, "two clipped rects");
535 }
536
537 fn raster(scene: Scene) -> crate::render::Pixmap {
539 use crate::geometry::ScaleFactor;
540 use crate::render::software::SoftwareRenderer;
541 SoftwareRenderer::new()
542 .with_background(Color::BLACK)
543 .render(scene, ScaleFactor::IDENTITY)
544 }
545
546 const RED: Color = Color::rgb(255, 0, 0);
547
548 #[test]
549 fn a_layer_translates_what_it_holds() {
550 use crate::geometry::Vec2;
551 let mut scene = Scene::new(Size::new(40.0, 40.0));
552 scene.push_layer(Affine::translate(Vec2::new(20.0, 20.0)), 1.0);
553 scene.fill_rect(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), RED);
554 scene.pop_layer();
555 assert!(scene.has_effects());
556 let out = raster(scene);
557 assert_eq!(out.pixel(25, 25), Some([255, 0, 0, 255]));
558 assert_eq!(out.pixel(5, 5), Some([0, 0, 0, 255]));
559 }
560
561 #[test]
562 fn a_layer_rotates_about_a_point() {
563 use crate::geometry::Vec2;
564 let about = Affine::translate(Vec2::new(-20.0, -20.0))
567 .then(Affine::rotate(std::f64::consts::FRAC_PI_2))
568 .then(Affine::translate(Vec2::new(20.0, 20.0)));
569 let mut scene = Scene::new(Size::new(40.0, 40.0));
570 scene.push_layer(about, 1.0);
571 scene.fill_rect(Rect::from_xywh(10.0, 18.0, 20.0, 4.0), RED);
572 scene.pop_layer();
573 let out = raster(scene);
574 assert_eq!(out.pixel(20, 12), Some([255, 0, 0, 255]), "now vertical");
575 assert_eq!(out.pixel(20, 27), Some([255, 0, 0, 255]), "now vertical");
576 assert_eq!(
577 out.pixel(12, 20),
578 Some([0, 0, 0, 255]),
579 "no longer horizontal"
580 );
581 }
582
583 #[test]
584 fn nested_layers_compose_inner_first() {
585 use crate::geometry::Vec2;
586 let mut scene = Scene::new(Size::new(40.0, 40.0));
587 scene.push_layer(Affine::translate(Vec2::new(20.0, 0.0)), 1.0);
588 scene.push_layer(Affine::scale(2.0, 2.0), 1.0);
589 scene.fill_rect(Rect::from_xywh(1.0, 1.0, 2.0, 2.0), RED);
591 scene.pop_layer();
592 scene.pop_layer();
593 let out = raster(scene);
594 assert_eq!(out.pixel(23, 3), Some([255, 0, 0, 255]));
595 assert_eq!(out.pixel(25, 5), Some([255, 0, 0, 255]));
596 assert_eq!(out.pixel(1, 1), Some([0, 0, 0, 255]));
597 assert_eq!(out.pixel(27, 7), Some([0, 0, 0, 255]));
598 }
599
600 #[test]
603 fn layer_opacity_composites_as_a_group() {
604 let mut scene = Scene::new(Size::new(40.0, 20.0));
605 scene.push_layer(Affine::IDENTITY, 0.5);
606 scene.fill_rect(Rect::from_xywh(0.0, 0.0, 25.0, 20.0), RED);
607 scene.fill_rect(Rect::from_xywh(15.0, 0.0, 25.0, 20.0), RED);
608 scene.pop_layer();
609 assert!(scene.has_effects());
610 let out = raster(scene);
611 let alone = out.pixel(5, 10).unwrap();
612 let overlap = out.pixel(20, 10).unwrap();
613 assert_eq!(alone, overlap, "no seam where the children overlap");
614 assert!(
615 (120..=136).contains(&alone[0]),
616 "half red over black: {alone:?}"
617 );
618 }
619
620 #[test]
621 fn a_clip_inside_a_layer_moves_with_it() {
622 use crate::geometry::Vec2;
623 let mut scene = Scene::new(Size::new(40.0, 40.0));
624 scene.push_layer(Affine::translate(Vec2::new(20.0, 0.0)), 1.0);
625 scene.push_clip(Rect::from_xywh(0.0, 0.0, 10.0, 10.0));
626 scene.fill_rect(Rect::from_xywh(0.0, 0.0, 40.0, 40.0), RED);
627 scene.pop_clip();
628 scene.pop_layer();
629 let out = raster(scene);
630 assert_eq!(
631 out.pixel(25, 5),
632 Some([255, 0, 0, 255]),
633 "inside the moved clip"
634 );
635 assert_eq!(out.pixel(5, 5), Some([0, 0, 0, 255]), "where the clip was");
636 assert_eq!(out.pixel(25, 15), Some([0, 0, 0, 255]), "below the clip");
637 }
638
639 #[test]
640 fn mismatched_pops_are_ignored_and_open_layers_close() {
641 let mut scene = Scene::new(Size::new(10.0, 10.0));
642 scene.push_layer(Affine::IDENTITY, 0.5);
643 scene.pop_clip(); scene.fill_rect(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), RED);
645 assert_eq!(scene.commands().len(), 2, "the stray pop records nothing");
646 let frame = scene.into_vector_frame();
647 let Node::Group(g) = &frame.root.children[0] else {
648 panic!("the open layer closes into a group");
649 };
650 assert_eq!(g.opacity, 0.5);
651 assert_eq!(g.children.len(), 1);
652 }
653
654 #[test]
655 fn fill_viewport_paints_placeholder_and_records_command() {
656 let mut scene = Scene::new(Size::new(200.0, 100.0));
657 let r = Rect::from_xywh(10.0, 10.0, 80.0, 60.0);
658 scene.fill_viewport(r, 7, Color::rgb(0x20, 0x24, 0x2c));
659 assert_eq!(scene.len(), 1);
661 let cmds = scene.commands();
663 assert_eq!(cmds.len(), 1);
664 assert!(matches!(cmds[0], DrawCmd::Viewport { id, rect } if id == 7 && rect == r));
665 }
666
667 #[test]
668 fn zero_radius_round_rect_is_plain_rect() {
669 let path = round_rect_path(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), 0.0);
670 assert_eq!(path.commands.len(), 5);
672 }
673
674 #[test]
675 fn round_rect_has_corner_curves() {
676 let path = round_rect_path(Rect::from_xywh(0.0, 0.0, 20.0, 20.0), 4.0);
677 let cubics = path
678 .commands
679 .iter()
680 .filter(|c| matches!(c, oxideav_core::PathCommand::CubicCurveTo { .. }))
681 .count();
682 assert_eq!(cubics, 4);
683 let _ = Point::ORIGIN;
684 }
685}