1use crate::components::{Color, ImageBlob, SvgPayload};
46use crate::native::ExtractedNative;
47use crate::render_world::{
48 BackgroundClip, Brush, ExtractedBorder, ExtractedClipBox, ExtractedImage,
49 ExtractedOpacityGroup, ExtractedOutline, ExtractedRect, ExtractedScrollbar, ExtractedShadow,
50 ExtractedText, PaintOrder, Rect,
51};
52use bevy_ecs::entity::Entity;
53use bevy_ecs::prelude::Resource;
54use glam::Vec2;
55use std::any::Any;
56use std::sync::Arc;
57
58#[derive(Clone, Copy, Debug, PartialEq)]
61pub struct Affine2 {
62 pub coeffs: [f64; 6],
64}
65
66impl Affine2 {
67 pub const IDENTITY: Affine2 = Affine2 {
69 coeffs: [1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
70 };
71
72 pub const fn translate(tx: f64, ty: f64) -> Self {
74 Self {
75 coeffs: [1.0, 0.0, 0.0, 1.0, tx, ty],
76 }
77 }
78
79 pub const fn scale(s: f64) -> Self {
81 Self {
82 coeffs: [s, 0.0, 0.0, s, 0.0, 0.0],
83 }
84 }
85}
86
87impl std::ops::Mul for Affine2 {
89 type Output = Affine2;
90
91 fn mul(self, rhs: Affine2) -> Affine2 {
92 let a = self.coeffs;
93 let b = rhs.coeffs;
94 Affine2 {
95 coeffs: [
96 a[0] * b[0] + a[2] * b[1],
97 a[1] * b[0] + a[3] * b[1],
98 a[0] * b[2] + a[2] * b[3],
99 a[1] * b[2] + a[3] * b[3],
100 a[0] * b[4] + a[2] * b[5] + a[4],
101 a[1] * b[4] + a[3] * b[5] + a[5],
102 ],
103 }
104 }
105}
106
107impl Default for Affine2 {
108 fn default() -> Self {
109 Self::IDENTITY
110 }
111}
112
113#[derive(Clone, Copy, Debug, PartialEq)]
117pub enum ClipShape {
118 Rect(Rect),
120 RoundedRect {
123 rect: Rect,
125 radii: [f32; 4],
127 },
128}
129
130impl ClipShape {
131 pub fn from_rect_radius(rect: Rect, radius: f32) -> Self {
133 if radius > 0.0 {
134 Self::RoundedRect {
135 rect,
136 radii: [radius; 4],
137 }
138 } else {
139 Self::Rect(rect)
140 }
141 }
142}
143
144impl From<&ExtractedClipBox> for ClipShape {
145 fn from(c: &ExtractedClipBox) -> Self {
146 let rect = Rect::new(c.origin, c.size);
147 Self::from_rect_radius(rect, c.radius)
148 }
149}
150
151#[derive(Clone)]
158pub enum Node {
159 Container {
163 children: Vec<Arc<Node>>,
165 },
166 Transform {
170 matrix: Affine2,
172 child: Arc<Node>,
174 },
175 Opacity {
185 alpha: f32,
187 child: Arc<Node>,
189 key: Option<u64>,
193 },
194 Clip {
201 shape: ClipShape,
203 child: Arc<Node>,
205 },
206 Rect {
210 bounds: Rect,
212 brush: Brush,
214 corner: f32,
216 corners: Option<[f32; 4]>,
218 },
219 Shadow {
224 origin: Vec2,
226 size: Vec2,
228 radius: f32,
230 spread: f32,
232 blur: f32,
234 color: Color,
236 inner: bool,
238 rect_origin: Vec2,
241 },
242 Border {
249 origin: Vec2,
251 size: Vec2,
253 widths: [f32; 4],
255 color: Color,
257 side_colors: Option<[Color; 4]>,
259 radius: f32,
261 corners: Option<[f32; 4]>,
263 },
264 Outline {
268 origin: Vec2,
270 size: Vec2,
272 stroke: Color,
274 width: f32,
276 radius: f32,
278 },
279 Text {
284 run: ExtractedText,
287 },
288 Image {
293 image: ExtractedImage,
295 blob: Option<Arc<dyn Any + Send + Sync>>,
298 },
299 Svg {
304 payload: Arc<dyn Any + Send + Sync>,
306 },
307 Native {
313 extension_id: Arc<str>,
315 payload: Arc<dyn Any + Send + Sync>,
317 bounds: Rect,
319 revision: u64,
321 clip_to_bounds: bool,
323 },
324}
325
326impl std::fmt::Debug for Node {
327 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
328 match self {
329 Node::Container { children } => f
330 .debug_struct("Container")
331 .field("children", &children.len())
332 .finish(),
333 Node::Transform { matrix, .. } => {
334 f.debug_struct("Transform").field("matrix", matrix).finish()
335 }
336 Node::Opacity { alpha, .. } => f.debug_struct("Opacity").field("alpha", alpha).finish(),
337 Node::Clip { shape, .. } => f.debug_struct("Clip").field("shape", shape).finish(),
338 Node::Rect { bounds, corner, .. } => f
339 .debug_struct("Rect")
340 .field("bounds", bounds)
341 .field("corner", corner)
342 .finish(),
343 Node::Shadow {
344 origin,
345 size,
346 inner,
347 ..
348 } => f
349 .debug_struct("Shadow")
350 .field("origin", origin)
351 .field("size", size)
352 .field("inner", inner)
353 .finish(),
354 Node::Border {
355 origin,
356 size,
357 widths,
358 ..
359 } => f
360 .debug_struct("Border")
361 .field("origin", origin)
362 .field("size", size)
363 .field("widths", widths)
364 .finish(),
365 Node::Outline { origin, size, .. } => f
366 .debug_struct("Outline")
367 .field("origin", origin)
368 .field("size", size)
369 .finish(),
370 Node::Text { run } => f
371 .debug_struct("Text")
372 .field("len", &run.text.len())
373 .finish(),
374 Node::Image { image, .. } => f
375 .debug_struct("Image")
376 .field("origin", &image.origin)
377 .field("size", &image.size)
378 .finish(),
379 Node::Svg { .. } => f.debug_struct("Svg").finish(),
380 Node::Native {
381 extension_id,
382 bounds,
383 revision,
384 ..
385 } => f
386 .debug_struct("Native")
387 .field("ext", extension_id)
388 .field("bounds", bounds)
389 .field("revision", revision)
390 .finish(),
391 }
392 }
393}
394
395impl From<&ExtractedRect> for Node {
396 fn from(r: &ExtractedRect) -> Self {
397 Node::Rect {
398 bounds: Rect::new(r.origin, r.size),
399 brush: r.brush.clone(),
400 corner: r.radius,
401 corners: r.corner_radii,
402 }
403 }
404}
405
406impl From<&ExtractedShadow> for Node {
407 fn from(s: &ExtractedShadow) -> Self {
408 Node::Shadow {
409 origin: s.origin,
410 size: s.size,
411 radius: s.radius,
412 spread: s.spread,
413 blur: s.blur,
414 color: s.color,
415 inner: s.inner,
416 rect_origin: s.rect_origin,
417 }
418 }
419}
420
421impl From<&ExtractedBorder> for Node {
422 fn from(b: &ExtractedBorder) -> Self {
423 Node::Border {
424 origin: b.origin,
425 size: b.size,
426 widths: b.widths,
427 color: b.color,
428 side_colors: b.side_colors,
429 radius: b.radius,
430 corners: b.corner_radii,
431 }
432 }
433}
434
435impl From<&ExtractedOutline> for Node {
436 fn from(o: &ExtractedOutline) -> Self {
437 Node::Outline {
438 origin: o.origin,
439 size: o.size,
440 stroke: o.stroke,
441 width: o.width,
442 radius: o.radius,
443 }
444 }
445}
446
447impl From<&ExtractedText> for Node {
448 fn from(t: &ExtractedText) -> Self {
449 Node::Text { run: t.clone() }
450 }
451}
452
453impl From<&ExtractedImage> for Node {
454 fn from(i: &ExtractedImage) -> Self {
455 Node::Image {
456 image: i.clone(),
457 blob: None,
458 }
459 }
460}
461
462impl From<(&ExtractedImage, &ImageBlob)> for Node {
463 fn from((i, b): (&ExtractedImage, &ImageBlob)) -> Self {
464 Node::Image {
465 image: i.clone(),
466 blob: Some(b.0.clone()),
467 }
468 }
469}
470
471impl From<&SvgPayload> for Node {
472 fn from(s: &SvgPayload) -> Self {
473 Node::Svg {
474 payload: s.payload.clone(),
475 }
476 }
477}
478
479impl From<&ExtractedNative> for Node {
480 fn from(n: &ExtractedNative) -> Self {
481 Node::Native {
482 extension_id: n.extension_id.clone(),
483 payload: n.payload.clone(),
484 bounds: n.bounds,
485 revision: n.revision,
486 clip_to_bounds: n.clip_to_bounds,
487 }
488 }
489}
490
491#[derive(Clone)]
493pub enum DrawEntry {
494 Rect(Arc<Node>),
496 Shadow(Arc<Node>),
498 Border(Arc<Node>),
500 Outline(Arc<Node>),
502 Text(Arc<Node>),
504 Image(Arc<Node>),
506 Svg(Arc<Node>),
508 PushClip(ClipShape),
510 PopClip,
512}
513
514#[derive(Resource, Default)]
519pub struct RetainedScene {
520 pub root: Option<Arc<Node>>,
522}
523
524impl std::fmt::Debug for RetainedScene {
525 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
526 f.debug_struct("RetainedScene")
527 .field("has_root", &self.root.is_some())
528 .finish()
529 }
530}
531
532#[derive(Resource, Default)]
537pub struct PreviousScene {
538 pub root: Option<Arc<Node>>,
540}
541
542impl std::fmt::Debug for PreviousScene {
543 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
544 f.debug_struct("PreviousScene")
545 .field("has_root", &self.root.is_some())
546 .finish()
547 }
548}
549
550fn background_clip(clip: BackgroundClip, leaf: Node) -> Arc<Node> {
553 let rect = Rect::new(clip.origin, clip.size);
554 let shape = if clip.radii.iter().any(|r| *r > 0.0) {
555 ClipShape::RoundedRect {
556 rect,
557 radii: clip.radii,
558 }
559 } else {
560 ClipShape::Rect(rect)
561 };
562 Arc::new(Node::Clip {
563 shape,
564 child: Arc::new(Node::Container {
565 children: vec![Arc::new(leaf)],
566 }),
567 })
568}
569
570type EntryOrder = u64;
576
577fn leaf_key(order: PaintOrder) -> EntryOrder {
579 u64::from(order) * 2 + 1
580}
581
582fn scrollbar_key(order: PaintOrder) -> EntryOrder {
584 u64::from(order) * 2
585}
586
587#[derive(Clone, Copy)]
589enum Bracket {
590 Clip(ClipShape),
592 Opacity(f32, u64),
594}
595
596impl Bracket {
597 fn wrap(self, children: Vec<Arc<Node>>) -> Arc<Node> {
598 let child = Arc::new(Node::Container { children });
599 Arc::new(match self {
600 Bracket::Clip(shape) => Node::Clip { shape, child },
601 Bracket::Opacity(alpha, key) => Node::Opacity {
602 alpha,
603 child,
604 key: Some(key),
605 },
606 })
607 }
608}
609
610#[allow(clippy::too_many_arguments)]
619pub fn transform_extracted_to_nodes(
620 mut retained: bevy_ecs::system::ResMut<RetainedScene>,
621 mut previous: bevy_ecs::system::ResMut<PreviousScene>,
622 rects: bevy_ecs::system::Query<&ExtractedRect>,
623 borders: bevy_ecs::system::Query<&ExtractedBorder>,
624 shadows: bevy_ecs::system::Query<&ExtractedShadow>,
625 outlines: bevy_ecs::system::Query<&ExtractedOutline>,
626 texts: bevy_ecs::system::Query<&ExtractedText>,
627 images: bevy_ecs::system::Query<(&ExtractedImage, Option<&ImageBlob>)>,
628 svgs: bevy_ecs::system::Query<&SvgPayload>,
629 natives: bevy_ecs::system::Query<(bevy_ecs::entity::Entity, &ExtractedNative)>,
630 clips: bevy_ecs::system::Query<&ExtractedClipBox>,
631 groups: bevy_ecs::system::Query<&ExtractedOpacityGroup>,
632 scrollbars: bevy_ecs::system::Query<&ExtractedScrollbar>,
633) {
634 previous.root = retained.root.take();
636
637 let mut entries: Vec<(EntryOrder, Arc<Node>)> = Vec::with_capacity(
639 rects.iter().len()
640 + borders.iter().len()
641 + shadows.iter().len()
642 + outlines.iter().len()
643 + texts.iter().len()
644 + images.iter().len()
645 + svgs.iter().len()
646 + natives.iter().len(),
647 );
648 for r in &rects {
649 entries.push((leaf_key(r.order), Arc::new(Node::from(r))));
650 }
651 for (i, maybe_blob) in &images {
655 if let Some(clip) = i.background {
656 let leaf = match maybe_blob {
657 Some(blob) => Node::from((i, blob)),
658 None => Node::from(i),
659 };
660 entries.push((leaf_key(i.order), background_clip(clip, leaf)));
661 }
662 }
663 for s in &svgs {
664 if let Some(clip) = s.background {
665 entries.push((leaf_key(s.order), background_clip(clip, Node::from(s))));
666 }
667 }
668 for s in shadows.iter().filter(|s| s.inner) {
671 entries.push((leaf_key(s.order), Arc::new(Node::from(s))));
672 }
673 for b in &borders {
677 entries.push((leaf_key(b.order), Arc::new(Node::from(b))));
678 }
679 for s in shadows.iter().filter(|s| !s.inner) {
680 entries.push((leaf_key(s.order), Arc::new(Node::from(s))));
681 }
682 for o in &outlines {
683 entries.push((leaf_key(o.order), Arc::new(Node::from(o))));
684 }
685 for t in &texts {
686 entries.push((leaf_key(t.order), Arc::new(Node::from(t))));
687 }
688 for (i, maybe_blob) in images.iter().filter(|(i, _)| i.background.is_none()) {
689 let node = match maybe_blob {
694 Some(blob) => Node::from((i, blob)),
695 None => Node::from(i),
696 };
697 entries.push((leaf_key(i.order), Arc::new(node)));
698 }
699 for s in svgs.iter().filter(|s| s.background.is_none()) {
700 entries.push((leaf_key(s.order), Arc::new(Node::from(s))));
701 }
702 let mut native_leaves: Vec<(Entity, &ExtractedNative)> = natives.iter().collect();
709 native_leaves.sort_by(|(ae, a), (be, b)| {
710 a.order
711 .cmp(&b.order)
712 .then_with(|| a.extension_id.cmp(&b.extension_id))
713 .then_with(|| ae.cmp(be))
714 });
715 for (_, n) in native_leaves {
716 entries.push((leaf_key(n.order), Arc::new(Node::from(n))));
717 }
718 for sb in &scrollbars {
722 for d in &sb.draws {
723 entries.push((
724 scrollbar_key(sb.order),
725 Arc::new(Node::Rect {
726 bounds: Rect::new(d.origin, d.size),
727 brush: Brush::Solid(d.color),
728 corner: d.radius,
729 corners: None,
730 }),
731 ));
732 }
733 }
734 entries.sort_by_key(|(k, _)| *k);
735
736 let mut ranges: Vec<(EntryOrder, EntryOrder, Bracket)> = clips
742 .iter()
743 .map(|c| {
744 (
745 leaf_key(c.start_order),
746 leaf_key(c.end_order),
747 Bracket::Clip(ClipShape::from(c)),
748 )
749 })
750 .chain(groups.iter().map(|g| {
751 (
752 leaf_key(g.start_order),
753 leaf_key(g.end_order),
754 Bracket::Opacity(g.alpha, g.key),
755 )
756 }))
757 .collect();
758 ranges.sort_by(|a, b| a.0.cmp(&b.0).then(b.1.cmp(&a.1)));
759
760 let mut next_range = 0usize;
764 let mut open: Vec<(EntryOrder, Bracket, Vec<Arc<Node>>)> = Vec::new();
765 let mut roots: Vec<Arc<Node>> = Vec::new();
766
767 fn flush_open(
768 open: &mut Vec<(EntryOrder, Bracket, Vec<Arc<Node>>)>,
769 roots: &mut Vec<Arc<Node>>,
770 until: EntryOrder,
771 ) {
772 while let Some((end, _, _)) = open.last() {
773 if *end >= until {
774 break;
775 }
776 let (_, bracket, children) = open.pop().expect("checked above");
777 push_into(open, roots, bracket.wrap(children));
778 }
779 }
780
781 fn push_into(
782 open: &mut [(EntryOrder, Bracket, Vec<Arc<Node>>)],
783 roots: &mut Vec<Arc<Node>>,
784 child: Arc<Node>,
785 ) {
786 if let Some(top) = open.last_mut() {
787 top.2.push(child);
788 } else {
789 roots.push(child);
790 }
791 }
792
793 for (key, leaf) in entries {
794 flush_open(&mut open, &mut roots, key);
796 while next_range < ranges.len() && ranges[next_range].0 <= key {
798 let (_, end, bracket) = ranges[next_range];
799 next_range += 1;
800 if end < key {
804 continue;
805 }
806 open.push((end, bracket, Vec::new()));
807 }
808 push_into(&mut open, &mut roots, leaf);
809 }
810 while let Some((_, bracket, children)) = open.pop() {
812 let node = bracket.wrap(children);
813 if let Some(top) = open.last_mut() {
814 top.2.push(node);
815 } else {
816 roots.push(node);
817 }
818 }
819
820 retained.root = Some(Arc::new(Node::Container { children: roots }));
821}
822
823#[cfg(test)]
824mod tests {
825 use super::*;
826 use crate::components::Color;
827 use crate::render_world::{OVERLAY_ORDER_BASE, ScrollbarDrawRect};
828
829 fn solid_rect(order: PaintOrder, origin: Vec2, size: Vec2) -> ExtractedRect {
830 ExtractedRect {
831 origin,
832 size,
833 brush: Brush::Solid(Color::rgba(1.0, 0.0, 0.0, 1.0)),
834 radius: 0.0,
835 corner_radii: None,
836 order,
837 }
838 }
839
840 #[test]
841 fn node_from_rect_round_trips_fields() {
842 let r = solid_rect(0, Vec2::new(1.0, 2.0), Vec2::new(10.0, 20.0));
843 let node = Node::from(&r);
844 match node {
845 Node::Rect { bounds, corner, .. } => {
846 assert_eq!(bounds.origin, Vec2::new(1.0, 2.0));
847 assert_eq!(bounds.size, Vec2::new(10.0, 20.0));
848 assert_eq!(corner, 0.0);
849 }
850 _ => panic!("expected Rect"),
851 }
852 }
853
854 #[test]
858 fn border_assembles_after_rect_at_same_order() {
859 let mut retained = RetainedScene::default();
860 let mut previous = PreviousScene::default();
861 let mut world = bevy_ecs::world::World::new();
862 world.spawn(solid_rect(4, Vec2::ZERO, Vec2::new(10.0, 10.0)));
863 world.spawn(ExtractedBorder {
864 origin: Vec2::ZERO,
865 size: Vec2::new(10.0, 10.0),
866 widths: [1.0; 4],
867 color: Color::rgba(0.0, 0.0, 1.0, 1.0),
868 side_colors: None,
869 radius: 0.0,
870 corner_radii: None,
871 order: 4,
872 });
873 world.spawn(solid_rect(6, Vec2::ZERO, Vec2::new(4.0, 4.0)));
874
875 let mut schedule = bevy_ecs::schedule::Schedule::default();
876 schedule.add_systems(transform_extracted_to_nodes);
877 world.insert_resource(std::mem::take(&mut retained));
878 world.insert_resource(std::mem::take(&mut previous));
879 schedule.run(&mut world);
880
881 let retained = world.resource::<RetainedScene>();
882 let root = retained.root.as_ref().expect("root");
883 let Node::Container { children } = root.as_ref() else {
884 panic!("root is a container");
885 };
886 assert_eq!(children.len(), 3);
887 assert!(matches!(children[0].as_ref(), Node::Rect { .. }));
888 assert!(matches!(children[1].as_ref(), Node::Border { .. }));
889 assert!(matches!(children[2].as_ref(), Node::Rect { .. }));
890 }
891
892 #[test]
897 fn inset_shadow_sits_between_fill_and_border() {
898 let shadow = |order: PaintOrder, inner: bool| ExtractedShadow {
899 origin: Vec2::ZERO,
900 size: Vec2::new(10.0, 10.0),
901 radius: 0.0,
902 spread: 0.0,
903 blur: 2.0,
904 color: Color::rgba(1.0, 0.0, 0.0, 1.0),
905 order,
906 inner,
907 rect_origin: Vec2::ZERO,
908 };
909 let mut world = bevy_ecs::world::World::new();
910 world.spawn(ExtractedBorder {
911 origin: Vec2::ZERO,
912 size: Vec2::new(10.0, 10.0),
913 widths: [1.0; 4],
914 color: Color::rgba(0.0, 0.0, 1.0, 1.0),
915 side_colors: None,
916 radius: 0.0,
917 corner_radii: None,
918 order: 4,
919 });
920 world.spawn(shadow(4, true));
921 world.spawn(shadow(3, false));
922 world.spawn(solid_rect(4, Vec2::ZERO, Vec2::new(10.0, 10.0)));
923 world.insert_resource(RetainedScene::default());
924 world.insert_resource(PreviousScene::default());
925 let mut schedule = bevy_ecs::schedule::Schedule::default();
926 schedule.add_systems(transform_extracted_to_nodes);
927 schedule.run(&mut world);
928
929 let root = world
930 .resource::<RetainedScene>()
931 .root
932 .clone()
933 .expect("root");
934 let Node::Container { children } = root.as_ref() else {
935 panic!("root is a container");
936 };
937 let kinds: Vec<&str> = children
938 .iter()
939 .map(|n| match n.as_ref() {
940 Node::Shadow { inner: true, .. } => "inset",
941 Node::Shadow { .. } => "drop",
942 Node::Rect { .. } => "fill",
943 Node::Border { .. } => "border",
944 _ => "other",
945 })
946 .collect();
947 assert_eq!(kinds, ["drop", "fill", "inset", "border"]);
948 }
949
950 fn image(order: PaintOrder, background: Option<BackgroundClip>) -> ExtractedImage {
951 ExtractedImage {
952 origin: Vec2::ZERO,
953 size: Vec2::new(10.0, 10.0),
954 width: 1,
955 height: 1,
956 rgba: Arc::from(vec![255u8; 4]),
957 fit: crate::components::ImageFit::Cover,
958 order,
959 alpha: 1.0,
960 background,
961 }
962 }
963
964 #[test]
969 fn a_background_image_sits_between_fill_and_border_inside_its_clip() {
970 let mut world = bevy_ecs::world::World::new();
971 world.spawn(solid_rect(4, Vec2::ZERO, Vec2::new(10.0, 10.0)));
972 world.spawn(ExtractedBorder {
973 origin: Vec2::ZERO,
974 size: Vec2::new(10.0, 10.0),
975 widths: [1.0; 4],
976 color: Color::rgba(0.0, 0.0, 1.0, 1.0),
977 side_colors: None,
978 radius: 3.0,
979 corner_radii: None,
980 order: 4,
981 });
982 world.spawn(image(
983 4,
984 Some(BackgroundClip {
985 origin: Vec2::ZERO,
986 size: Vec2::new(10.0, 10.0),
987 radii: [3.0; 4],
988 }),
989 ));
990 world.spawn(image(4, None));
991 world.spawn(solid_rect(6, Vec2::ZERO, Vec2::new(4.0, 4.0)));
992
993 let children = children_of(&assemble(&mut world));
994 assert_eq!(children.len(), 5);
995 assert!(matches!(children[0].as_ref(), Node::Rect { .. }));
996 let Node::Clip { shape, child } = children[1].as_ref() else {
997 panic!("the background is clipped, got {:?}", children[1]);
998 };
999 assert_eq!(
1000 *shape,
1001 ClipShape::RoundedRect {
1002 rect: Rect::new(Vec2::ZERO, Vec2::new(10.0, 10.0)),
1003 radii: [3.0; 4],
1004 }
1005 );
1006 assert!(matches!(
1007 children_of(child)[0].as_ref(),
1008 Node::Image { image, .. } if image.background.is_some()
1009 ));
1010 assert!(matches!(children[2].as_ref(), Node::Border { .. }));
1011 assert!(matches!(
1012 children[3].as_ref(),
1013 Node::Image { image, .. } if image.background.is_none()
1014 ));
1015 assert!(matches!(children[4].as_ref(), Node::Rect { .. }));
1016 }
1017
1018 fn native(
1019 order: PaintOrder,
1020 extension_id: &str,
1021 origin: Vec2,
1022 revision: u64,
1023 ) -> ExtractedNative {
1024 ExtractedNative {
1025 extension_id: extension_id.into(),
1026 payload: Arc::new(revision),
1027 bounds: Rect::new(origin, Vec2::new(30.0, 30.0)),
1028 order,
1029 revision,
1030 clip_to_bounds: false,
1031 }
1032 }
1033
1034 fn assemble(world: &mut bevy_ecs::world::World) -> Arc<Node> {
1035 world.insert_resource(RetainedScene::default());
1036 world.insert_resource(PreviousScene::default());
1037 let mut schedule = bevy_ecs::schedule::Schedule::default();
1038 schedule.add_systems(transform_extracted_to_nodes);
1039 schedule.run(world);
1040 world
1041 .resource::<RetainedScene>()
1042 .root
1043 .as_ref()
1044 .expect("root")
1045 .clone()
1046 }
1047
1048 fn children_of(node: &Arc<Node>) -> Vec<Arc<Node>> {
1049 match node.as_ref() {
1050 Node::Container { children } => children.clone(),
1051 other => panic!("expected a container, got {other:?}"),
1052 }
1053 }
1054
1055 #[test]
1059 fn a_native_leaf_carries_its_bounds_and_revision_into_the_tree() {
1060 let mut world = bevy_ecs::world::World::new();
1061 let mut leaf = native(4, "demo.chart", Vec2::new(12.0, 8.0), 77);
1062 leaf.clip_to_bounds = true;
1063 world.spawn(leaf);
1064
1065 let children = children_of(&assemble(&mut world));
1066 assert_eq!(children.len(), 1);
1067 match children[0].as_ref() {
1068 Node::Native {
1069 extension_id,
1070 bounds,
1071 revision,
1072 clip_to_bounds,
1073 ..
1074 } => {
1075 assert_eq!(&**extension_id, "demo.chart");
1076 assert_eq!(bounds.origin, Vec2::new(12.0, 8.0));
1077 assert_eq!(*revision, 77);
1078 assert!(clip_to_bounds);
1079 }
1080 other => panic!("expected Native, got {other:?}"),
1081 }
1082 }
1083
1084 #[test]
1088 fn a_native_leaf_paints_over_the_box_it_sits_in() {
1089 let mut world = bevy_ecs::world::World::new();
1090 world.spawn(solid_rect(4, Vec2::ZERO, Vec2::new(40.0, 40.0)));
1091 world.spawn(ExtractedBorder {
1092 origin: Vec2::ZERO,
1093 size: Vec2::new(40.0, 40.0),
1094 widths: [1.0; 4],
1095 color: Color::rgba(0.0, 0.0, 1.0, 1.0),
1096 side_colors: None,
1097 radius: 0.0,
1098 corner_radii: None,
1099 order: 4,
1100 });
1101 world.spawn(native(4, "demo.chart", Vec2::ZERO, 1));
1102
1103 let children = children_of(&assemble(&mut world));
1104 assert_eq!(children.len(), 3);
1105 assert!(matches!(children[0].as_ref(), Node::Rect { .. }));
1106 assert!(matches!(children[1].as_ref(), Node::Border { .. }));
1107 assert!(matches!(children[2].as_ref(), Node::Native { .. }));
1108 }
1109
1110 #[test]
1113 fn native_leaves_at_one_order_stack_by_extension_id() {
1114 let mut world = bevy_ecs::world::World::new();
1115 world.spawn(native(4, "zeta.overlay", Vec2::ZERO, 1));
1116 world.spawn(native(4, "alpha.grid", Vec2::ZERO, 1));
1117
1118 let children = children_of(&assemble(&mut world));
1119 let ids: Vec<String> = children
1120 .iter()
1121 .map(|c| match c.as_ref() {
1122 Node::Native { extension_id, .. } => extension_id.to_string(),
1123 other => panic!("expected Native, got {other:?}"),
1124 })
1125 .collect();
1126 assert_eq!(ids, ["alpha.grid", "zeta.overlay"]);
1127 }
1128
1129 #[test]
1133 fn an_enclosing_clip_brackets_a_native_leaf() {
1134 let mut world = bevy_ecs::world::World::new();
1135 world.spawn(ExtractedClipBox {
1136 origin: Vec2::new(5.0, 5.0),
1137 size: Vec2::new(50.0, 50.0),
1138 radius: 0.0,
1139 start_order: 4,
1140 end_order: 8,
1141 });
1142 world.spawn(native(6, "demo.chart", Vec2::ZERO, 1));
1143
1144 let children = children_of(&assemble(&mut world));
1145 assert_eq!(children.len(), 1);
1146 let Node::Clip { child, .. } = children[0].as_ref() else {
1147 panic!("expected the leaf wrapped in a clip, got {:?}", children[0]);
1148 };
1149 let inner = children_of(child);
1150 assert!(matches!(inner[0].as_ref(), Node::Native { .. }));
1151 }
1152
1153 #[test]
1157 fn an_empty_clip_range_leaves_the_following_leaf_alone() {
1158 let mut world = bevy_ecs::world::World::new();
1159 world.spawn(ExtractedClipBox {
1160 origin: Vec2::new(5.0, 5.0),
1161 size: Vec2::new(10.0, 10.0),
1162 radius: 0.0,
1163 start_order: 4,
1164 end_order: 4,
1165 });
1166 world.spawn(solid_rect(6, Vec2::ZERO, Vec2::new(40.0, 40.0)));
1167
1168 let children = children_of(&assemble(&mut world));
1169 assert_eq!(children.len(), 1);
1170 assert!(
1171 matches!(children[0].as_ref(), Node::Rect { .. }),
1172 "expected an unclipped rect, got {:?}",
1173 children[0]
1174 );
1175 }
1176
1177 #[test]
1180 fn an_empty_clip_range_nested_in_a_live_one_wraps_nothing() {
1181 let mut world = bevy_ecs::world::World::new();
1182 world.spawn(ExtractedClipBox {
1183 origin: Vec2::ZERO,
1184 size: Vec2::new(80.0, 80.0),
1185 radius: 0.0,
1186 start_order: 1,
1187 end_order: 10,
1188 });
1189 world.spawn(ExtractedClipBox {
1190 origin: Vec2::new(2.0, 2.0),
1191 size: Vec2::new(4.0, 4.0),
1192 radius: 0.0,
1193 start_order: 3,
1194 end_order: 4,
1195 });
1196 world.spawn(solid_rect(5, Vec2::ZERO, Vec2::new(40.0, 40.0)));
1197
1198 let children = children_of(&assemble(&mut world));
1199 assert_eq!(children.len(), 1);
1200 let Node::Clip { child, .. } = children[0].as_ref() else {
1201 panic!("expected the outer clip, got {:?}", children[0]);
1202 };
1203 let inner = children_of(child);
1204 assert_eq!(inner.len(), 1);
1205 assert!(
1206 matches!(inner[0].as_ref(), Node::Rect { .. }),
1207 "expected the rect directly under the outer clip, got {:?}",
1208 inner[0]
1209 );
1210 }
1211
1212 #[test]
1216 fn the_overlay_band_and_scrollbars_keep_their_places_around_a_native_leaf() {
1217 let mut world = bevy_ecs::world::World::new();
1218 world.spawn(solid_rect(6, Vec2::ZERO, Vec2::new(40.0, 40.0)));
1219 world.spawn(native(
1220 OVERLAY_ORDER_BASE + 2,
1221 "demo.tooltip",
1222 Vec2::ZERO,
1223 1,
1224 ));
1225 world.spawn(ExtractedScrollbar {
1226 draws: vec![ScrollbarDrawRect {
1227 origin: Vec2::new(90.0, 0.0),
1228 size: Vec2::new(6.0, 40.0),
1229 color: Color::rgba(0.0, 0.0, 0.0, 0.4),
1230 radius: 3.0,
1231 }],
1232 order: OVERLAY_ORDER_BASE + 3,
1233 });
1234
1235 let children = children_of(&assemble(&mut world));
1236 assert_eq!(children.len(), 3);
1237 assert!(matches!(children[0].as_ref(), Node::Rect { .. }));
1238 assert!(matches!(children[1].as_ref(), Node::Native { .. }));
1239 assert!(matches!(children[2].as_ref(), Node::Rect { .. }));
1240 }
1241
1242 #[test]
1245 fn an_opacity_group_wraps_the_faded_subtree_and_nothing_else() {
1246 let mut world = bevy_ecs::world::World::new();
1247 world.spawn(solid_rect(2, Vec2::ZERO, Vec2::new(40.0, 40.0)));
1249 world.spawn(solid_rect(4, Vec2::ZERO, Vec2::new(40.0, 40.0)));
1250 world.spawn(ExtractedScrollbar {
1251 draws: vec![ScrollbarDrawRect {
1252 origin: Vec2::new(34.0, 0.0),
1253 size: Vec2::new(6.0, 40.0),
1254 color: Color::rgba(0.0, 0.0, 0.0, 0.4),
1255 radius: 3.0,
1256 }],
1257 order: 5,
1258 });
1259 world.spawn(solid_rect(6, Vec2::ZERO, Vec2::new(40.0, 40.0)));
1261 world.spawn(solid_rect(8, Vec2::ZERO, Vec2::new(20.0, 20.0)));
1262 world.spawn(solid_rect(10, Vec2::ZERO, Vec2::new(10.0, 10.0)));
1263 world.spawn(ExtractedOpacityGroup {
1264 alpha: 0.3,
1265 start_order: 5,
1266 end_order: 8,
1267 key: 1,
1268 });
1269
1270 let children = children_of(&assemble(&mut world));
1271 assert_eq!(
1272 children.len(),
1273 5,
1274 "two rects, the bar, the group, the last rect"
1275 );
1276 assert!(
1277 matches!(children[2].as_ref(), Node::Rect { .. }),
1278 "the bar stays outside"
1279 );
1280 let Node::Opacity { alpha, child, .. } = children[3].as_ref() else {
1281 panic!("expected the group, got {:?}", children[3]);
1282 };
1283 assert_eq!(*alpha, 0.3);
1284 assert_eq!(children_of(child).len(), 2, "the element and its child");
1285 assert!(matches!(children[4].as_ref(), Node::Rect { .. }));
1286 }
1287
1288 #[test]
1289 fn clipshape_from_extracted_clipbox_routes_radius() {
1290 let sharp = ExtractedClipBox {
1291 origin: Vec2::ZERO,
1292 size: Vec2::new(10.0, 10.0),
1293 radius: 0.0,
1294 start_order: 0,
1295 end_order: 1,
1296 };
1297 let rounded = ExtractedClipBox {
1298 origin: Vec2::ZERO,
1299 size: Vec2::new(10.0, 10.0),
1300 radius: 5.0,
1301 start_order: 0,
1302 end_order: 1,
1303 };
1304 assert!(matches!(ClipShape::from(&sharp), ClipShape::Rect(_)));
1305 assert!(matches!(
1306 ClipShape::from(&rounded),
1307 ClipShape::RoundedRect { .. }
1308 ));
1309 }
1310}