1use crate::id::NodeId;
10use petgraph::graph::NodeIndex;
11use petgraph::stable_graph::StableDiGraph;
12use serde::{Deserialize, Serialize};
13use smallvec::SmallVec;
14use std::collections::HashMap;
15
16#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
20pub struct Color {
21 pub r: f32,
22 pub g: f32,
23 pub b: f32,
24 pub a: f32,
25}
26
27pub fn hex_val(c: u8) -> Option<u8> {
29 match c {
30 b'0'..=b'9' => Some(c - b'0'),
31 b'a'..=b'f' => Some(c - b'a' + 10),
32 b'A'..=b'F' => Some(c - b'A' + 10),
33 _ => None,
34 }
35}
36
37impl Color {
38 pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
39 Self { r, g, b, a }
40 }
41
42 pub fn from_hex(hex: &str) -> Option<Self> {
45 let hex = hex.strip_prefix('#').unwrap_or(hex);
46 let bytes = hex.as_bytes();
47
48 match bytes.len() {
49 3 => {
50 let r = hex_val(bytes[0])?;
51 let g = hex_val(bytes[1])?;
52 let b = hex_val(bytes[2])?;
53 Some(Self::rgba(
54 (r * 17) as f32 / 255.0,
55 (g * 17) as f32 / 255.0,
56 (b * 17) as f32 / 255.0,
57 1.0,
58 ))
59 }
60 4 => {
61 let r = hex_val(bytes[0])?;
62 let g = hex_val(bytes[1])?;
63 let b = hex_val(bytes[2])?;
64 let a = hex_val(bytes[3])?;
65 Some(Self::rgba(
66 (r * 17) as f32 / 255.0,
67 (g * 17) as f32 / 255.0,
68 (b * 17) as f32 / 255.0,
69 (a * 17) as f32 / 255.0,
70 ))
71 }
72 6 => {
73 let r = hex_val(bytes[0])? << 4 | hex_val(bytes[1])?;
74 let g = hex_val(bytes[2])? << 4 | hex_val(bytes[3])?;
75 let b = hex_val(bytes[4])? << 4 | hex_val(bytes[5])?;
76 Some(Self::rgba(
77 r as f32 / 255.0,
78 g as f32 / 255.0,
79 b as f32 / 255.0,
80 1.0,
81 ))
82 }
83 8 => {
84 let r = hex_val(bytes[0])? << 4 | hex_val(bytes[1])?;
85 let g = hex_val(bytes[2])? << 4 | hex_val(bytes[3])?;
86 let b = hex_val(bytes[4])? << 4 | hex_val(bytes[5])?;
87 let a = hex_val(bytes[6])? << 4 | hex_val(bytes[7])?;
88 Some(Self::rgba(
89 r as f32 / 255.0,
90 g as f32 / 255.0,
91 b as f32 / 255.0,
92 a as f32 / 255.0,
93 ))
94 }
95 _ => None,
96 }
97 }
98
99 pub fn to_hex(&self) -> String {
101 let r = (self.r * 255.0).round() as u8;
102 let g = (self.g * 255.0).round() as u8;
103 let b = (self.b * 255.0).round() as u8;
104 let a = (self.a * 255.0).round() as u8;
105 if a == 255 {
106 format!("#{r:02X}{g:02X}{b:02X}")
107 } else {
108 format!("#{r:02X}{g:02X}{b:02X}{a:02X}")
109 }
110 }
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct GradientStop {
116 pub offset: f32, pub color: Color,
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
122pub enum Paint {
123 Solid(Color),
124 LinearGradient {
125 angle: f32, stops: Vec<GradientStop>,
127 },
128 RadialGradient {
129 stops: Vec<GradientStop>,
130 },
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct Stroke {
137 pub paint: Paint,
138 pub width: f32,
139 pub cap: StrokeCap,
140 pub join: StrokeJoin,
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
144pub enum StrokeCap {
145 Butt,
146 Round,
147 Square,
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
151pub enum StrokeJoin {
152 Miter,
153 Round,
154 Bevel,
155}
156
157impl Default for Stroke {
158 fn default() -> Self {
159 Self {
160 paint: Paint::Solid(Color::rgba(0.0, 0.0, 0.0, 1.0)),
161 width: 1.0,
162 cap: StrokeCap::Butt,
163 join: StrokeJoin::Miter,
164 }
165 }
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct FontSpec {
172 pub family: String,
173 pub weight: u16, pub size: f32,
175}
176
177impl Default for FontSpec {
178 fn default() -> Self {
179 Self {
180 family: "Inter".into(),
181 weight: 400,
182 size: 14.0,
183 }
184 }
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
191pub enum PathCmd {
192 MoveTo(f32, f32),
193 LineTo(f32, f32),
194 QuadTo(f32, f32, f32, f32), CubicTo(f32, f32, f32, f32, f32, f32), Close,
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct Shadow {
203 pub offset_x: f32,
204 pub offset_y: f32,
205 pub blur: f32,
206 pub color: Color,
207}
208
209#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
213pub enum TextAlign {
214 Left,
215 #[default]
216 Center,
217 Right,
218}
219
220#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
222pub enum TextVAlign {
223 Top,
224 #[default]
225 Middle,
226 Bottom,
227}
228
229#[derive(Debug, Clone, Default, Serialize, Deserialize)]
231pub struct Style {
232 pub fill: Option<Paint>,
233 pub stroke: Option<Stroke>,
234 pub font: Option<FontSpec>,
235 pub corner_radius: Option<f32>,
236 pub opacity: Option<f32>,
237 pub shadow: Option<Shadow>,
238
239 pub text_align: Option<TextAlign>,
241 pub text_valign: Option<TextVAlign>,
243
244 pub scale: Option<f32>,
246}
247
248#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
252pub enum AnimTrigger {
253 Hover,
254 Press,
255 Enter, Custom(String),
257}
258
259#[derive(Debug, Clone, Serialize, Deserialize)]
261pub enum Easing {
262 Linear,
263 EaseIn,
264 EaseOut,
265 EaseInOut,
266 Spring,
267 CubicBezier(f32, f32, f32, f32),
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct AnimKeyframe {
273 pub trigger: AnimTrigger,
274 pub duration_ms: u32,
275 pub easing: Easing,
276 pub properties: AnimProperties,
277}
278
279#[derive(Debug, Clone, Default, Serialize, Deserialize)]
281pub struct AnimProperties {
282 pub fill: Option<Paint>,
283 pub opacity: Option<f32>,
284 pub scale: Option<f32>,
285 pub rotate: Option<f32>, pub translate: Option<(f32, f32)>,
287}
288
289#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
294pub enum Annotation {
295 Description(String),
297 Accept(String),
299 Status(String),
301 Priority(String),
303 Tag(String),
305}
306
307#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
311pub struct Import {
312 pub path: String,
314 pub namespace: String,
316}
317
318#[derive(Debug, Clone, Serialize, Deserialize)]
322pub enum Constraint {
323 CenterIn(NodeId),
325 Offset { from: NodeId, dx: f32, dy: f32 },
327 FillParent { pad: f32 },
329 Position { x: f32, y: f32 },
332}
333
334#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
338pub enum ArrowKind {
339 #[default]
340 None,
341 Start,
342 End,
343 Both,
344}
345
346#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
348pub enum CurveKind {
349 #[default]
350 Straight,
351 Smooth,
352 Step,
353}
354
355#[derive(Debug, Clone, Serialize, Deserialize)]
357pub struct Edge {
358 pub id: NodeId,
359 pub from: NodeId,
360 pub to: NodeId,
361 pub label: Option<String>,
362 pub style: Style,
363 pub use_styles: SmallVec<[NodeId; 2]>,
364 pub arrow: ArrowKind,
365 pub curve: CurveKind,
366 pub annotations: Vec<Annotation>,
367 pub animations: SmallVec<[AnimKeyframe; 2]>,
368 pub flow: Option<FlowAnim>,
369}
370
371#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
373pub enum FlowKind {
374 Pulse,
376 Dash,
378}
379
380#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
382pub struct FlowAnim {
383 pub kind: FlowKind,
384 pub duration_ms: u32,
385}
386
387#[derive(Debug, Clone, Default, Serialize, Deserialize)]
389pub enum LayoutMode {
390 #[default]
392 Free,
393 Column { gap: f32, pad: f32 },
395 Row { gap: f32, pad: f32 },
397 Grid { cols: u32, gap: f32, pad: f32 },
399}
400
401#[derive(Debug, Clone, Serialize, Deserialize)]
405pub enum NodeKind {
406 Root,
408
409 Generic,
412
413 Group { layout: LayoutMode },
415
416 Frame {
419 width: f32,
420 height: f32,
421 clip: bool,
422 layout: LayoutMode,
423 },
424
425 Rect { width: f32, height: f32 },
427
428 Ellipse { rx: f32, ry: f32 },
430
431 Path { commands: Vec<PathCmd> },
433
434 Text { content: String },
436}
437
438impl NodeKind {
439 pub fn kind_name(&self) -> &'static str {
441 match self {
442 Self::Root => "root",
443 Self::Generic => "generic",
444 Self::Group { .. } => "group",
445 Self::Frame { .. } => "frame",
446 Self::Rect { .. } => "rect",
447 Self::Ellipse { .. } => "ellipse",
448 Self::Path { .. } => "path",
449 Self::Text { .. } => "text",
450 }
451 }
452}
453
454#[derive(Debug, Clone, Serialize, Deserialize)]
456pub struct SceneNode {
457 pub id: NodeId,
459
460 pub kind: NodeKind,
462
463 pub style: Style,
465
466 pub use_styles: SmallVec<[NodeId; 2]>,
468
469 pub constraints: SmallVec<[Constraint; 2]>,
471
472 pub animations: SmallVec<[AnimKeyframe; 2]>,
474
475 pub annotations: Vec<Annotation>,
477
478 pub comments: Vec<String>,
481}
482
483impl SceneNode {
484 pub fn new(id: NodeId, kind: NodeKind) -> Self {
485 Self {
486 id,
487 kind,
488 style: Style::default(),
489 use_styles: SmallVec::new(),
490 constraints: SmallVec::new(),
491 animations: SmallVec::new(),
492 annotations: Vec::new(),
493 comments: Vec::new(),
494 }
495 }
496}
497
498#[derive(Debug, Clone)]
505pub struct SceneGraph {
506 pub graph: StableDiGraph<SceneNode, ()>,
508
509 pub root: NodeIndex,
511
512 pub styles: HashMap<NodeId, Style>,
514
515 pub id_index: HashMap<NodeId, NodeIndex>,
517
518 pub edges: Vec<Edge>,
520
521 pub imports: Vec<Import>,
523
524 pub sorted_child_order: HashMap<NodeIndex, Vec<NodeIndex>>,
528}
529
530impl SceneGraph {
531 #[must_use]
533 pub fn new() -> Self {
534 let mut graph = StableDiGraph::new();
535 let root_node = SceneNode::new(NodeId::intern("root"), NodeKind::Root);
536 let root = graph.add_node(root_node);
537
538 let mut id_index = HashMap::new();
539 id_index.insert(NodeId::intern("root"), root);
540
541 Self {
542 graph,
543 root,
544 styles: HashMap::new(),
545 id_index,
546 edges: Vec::new(),
547 imports: Vec::new(),
548 sorted_child_order: HashMap::new(),
549 }
550 }
551
552 pub fn add_node(&mut self, parent: NodeIndex, node: SceneNode) -> NodeIndex {
554 let id = node.id;
555 let idx = self.graph.add_node(node);
556 self.graph.add_edge(parent, idx, ());
557 self.id_index.insert(id, idx);
558 idx
559 }
560
561 pub fn remove_node(&mut self, idx: NodeIndex) -> Option<SceneNode> {
563 let removed = self.graph.remove_node(idx);
564 if let Some(removed_node) = &removed {
565 self.id_index.remove(&removed_node.id);
566 }
567 removed
568 }
569
570 pub fn get_by_id(&self, id: NodeId) -> Option<&SceneNode> {
572 self.id_index.get(&id).map(|idx| &self.graph[*idx])
573 }
574
575 pub fn get_by_id_mut(&mut self, id: NodeId) -> Option<&mut SceneNode> {
577 self.id_index
578 .get(&id)
579 .copied()
580 .map(|idx| &mut self.graph[idx])
581 }
582
583 pub fn index_of(&self, id: NodeId) -> Option<NodeIndex> {
585 self.id_index.get(&id).copied()
586 }
587
588 pub fn parent(&self, idx: NodeIndex) -> Option<NodeIndex> {
590 self.graph
591 .neighbors_directed(idx, petgraph::Direction::Incoming)
592 .next()
593 }
594
595 pub fn reparent_node(&mut self, child: NodeIndex, new_parent: NodeIndex) {
597 if let Some(old_parent) = self.parent(child)
598 && let Some(edge) = self.graph.find_edge(old_parent, child)
599 {
600 self.graph.remove_edge(edge);
601 }
602 self.graph.add_edge(new_parent, child, ());
603 }
604
605 pub fn children(&self, idx: NodeIndex) -> Vec<NodeIndex> {
611 if let Some(order) = self.sorted_child_order.get(&idx) {
613 return order.clone();
614 }
615
616 let mut children: Vec<NodeIndex> = self
617 .graph
618 .neighbors_directed(idx, petgraph::Direction::Outgoing)
619 .collect();
620 children.sort();
621 children
622 }
623
624 pub fn send_backward(&mut self, child: NodeIndex) -> bool {
627 let parent = match self.parent(child) {
628 Some(p) => p,
629 None => return false,
630 };
631 let siblings = self.children(parent);
632 let pos = match siblings.iter().position(|&s| s == child) {
633 Some(p) => p,
634 None => return false,
635 };
636 if pos == 0 {
637 return false; }
639 self.rebuild_child_order(parent, &siblings, pos, pos - 1)
641 }
642
643 pub fn bring_forward(&mut self, child: NodeIndex) -> bool {
646 let parent = match self.parent(child) {
647 Some(p) => p,
648 None => return false,
649 };
650 let siblings = self.children(parent);
651 let pos = match siblings.iter().position(|&s| s == child) {
652 Some(p) => p,
653 None => return false,
654 };
655 if pos >= siblings.len() - 1 {
656 return false; }
658 self.rebuild_child_order(parent, &siblings, pos, pos + 1)
659 }
660
661 pub fn send_to_back(&mut self, child: NodeIndex) -> bool {
663 let parent = match self.parent(child) {
664 Some(p) => p,
665 None => return false,
666 };
667 let siblings = self.children(parent);
668 let pos = match siblings.iter().position(|&s| s == child) {
669 Some(p) => p,
670 None => return false,
671 };
672 if pos == 0 {
673 return false;
674 }
675 self.rebuild_child_order(parent, &siblings, pos, 0)
676 }
677
678 pub fn bring_to_front(&mut self, child: NodeIndex) -> bool {
680 let parent = match self.parent(child) {
681 Some(p) => p,
682 None => return false,
683 };
684 let siblings = self.children(parent);
685 let pos = match siblings.iter().position(|&s| s == child) {
686 Some(p) => p,
687 None => return false,
688 };
689 let last = siblings.len() - 1;
690 if pos == last {
691 return false;
692 }
693 self.rebuild_child_order(parent, &siblings, pos, last)
694 }
695
696 fn rebuild_child_order(
698 &mut self,
699 parent: NodeIndex,
700 siblings: &[NodeIndex],
701 from: usize,
702 to: usize,
703 ) -> bool {
704 for &sib in siblings {
706 if let Some(edge) = self.graph.find_edge(parent, sib) {
707 self.graph.remove_edge(edge);
708 }
709 }
710 let mut new_order: Vec<NodeIndex> = siblings.to_vec();
712 let child = new_order.remove(from);
713 new_order.insert(to, child);
714 for &sib in &new_order {
716 self.graph.add_edge(parent, sib, ());
717 }
718 true
719 }
720
721 pub fn define_style(&mut self, name: NodeId, style: Style) {
723 self.styles.insert(name, style);
724 }
725
726 pub fn resolve_style(&self, node: &SceneNode, active_triggers: &[AnimTrigger]) -> Style {
728 let mut resolved = Style::default();
729
730 for style_id in &node.use_styles {
732 if let Some(base) = self.styles.get(style_id) {
733 merge_style(&mut resolved, base);
734 }
735 }
736
737 merge_style(&mut resolved, &node.style);
739
740 for anim in &node.animations {
742 if active_triggers.contains(&anim.trigger) {
743 if anim.properties.fill.is_some() {
744 resolved.fill = anim.properties.fill.clone();
745 }
746 if anim.properties.opacity.is_some() {
747 resolved.opacity = anim.properties.opacity;
748 }
749 if anim.properties.scale.is_some() {
750 resolved.scale = anim.properties.scale;
751 }
752 }
753 }
754
755 resolved
756 }
757
758 pub fn rebuild_index(&mut self) {
760 self.id_index.clear();
761 for idx in self.graph.node_indices() {
762 let id = self.graph[idx].id;
763 self.id_index.insert(id, idx);
764 }
765 }
766
767 pub fn resolve_style_for_edge(&self, edge: &Edge, active_triggers: &[AnimTrigger]) -> Style {
769 let mut resolved = Style::default();
770 for style_id in &edge.use_styles {
771 if let Some(base) = self.styles.get(style_id) {
772 merge_style(&mut resolved, base);
773 }
774 }
775 merge_style(&mut resolved, &edge.style);
776
777 for anim in &edge.animations {
778 if active_triggers.contains(&anim.trigger) {
779 if anim.properties.fill.is_some() {
780 resolved.fill = anim.properties.fill.clone();
781 }
782 if anim.properties.opacity.is_some() {
783 resolved.opacity = anim.properties.opacity;
784 }
785 if anim.properties.scale.is_some() {
786 resolved.scale = anim.properties.scale;
787 }
788 }
789 }
790
791 resolved
792 }
793
794 pub fn effective_target(&self, leaf_id: NodeId, _selected: &[NodeId]) -> NodeId {
798 leaf_id
799 }
800
801 pub fn is_ancestor_of(&self, ancestor_id: NodeId, descendant_id: NodeId) -> bool {
803 if ancestor_id == descendant_id {
804 return false;
805 }
806 let mut current_idx = match self.index_of(descendant_id) {
807 Some(idx) => idx,
808 None => return false,
809 };
810 while let Some(parent_idx) = self.parent(current_idx) {
811 if self.graph[parent_idx].id == ancestor_id {
812 return true;
813 }
814 if matches!(self.graph[parent_idx].kind, NodeKind::Root) {
815 break;
816 }
817 current_idx = parent_idx;
818 }
819 false
820 }
821}
822
823impl Default for SceneGraph {
824 fn default() -> Self {
825 Self::new()
826 }
827}
828
829fn merge_style(dst: &mut Style, src: &Style) {
831 if src.fill.is_some() {
832 dst.fill = src.fill.clone();
833 }
834 if src.stroke.is_some() {
835 dst.stroke = src.stroke.clone();
836 }
837 if src.font.is_some() {
838 dst.font = src.font.clone();
839 }
840 if src.corner_radius.is_some() {
841 dst.corner_radius = src.corner_radius;
842 }
843 if src.opacity.is_some() {
844 dst.opacity = src.opacity;
845 }
846 if src.shadow.is_some() {
847 dst.shadow = src.shadow.clone();
848 }
849
850 if src.text_align.is_some() {
851 dst.text_align = src.text_align;
852 }
853 if src.text_valign.is_some() {
854 dst.text_valign = src.text_valign;
855 }
856 if src.scale.is_some() {
857 dst.scale = src.scale;
858 }
859}
860
861#[derive(Debug, Clone, Copy, Default)]
865pub struct ResolvedBounds {
866 pub x: f32,
867 pub y: f32,
868 pub width: f32,
869 pub height: f32,
870}
871
872impl ResolvedBounds {
873 pub fn contains(&self, px: f32, py: f32) -> bool {
874 px >= self.x && px <= self.x + self.width && py >= self.y && py <= self.y + self.height
875 }
876
877 pub fn center(&self) -> (f32, f32) {
878 (self.x + self.width / 2.0, self.y + self.height / 2.0)
879 }
880
881 pub fn intersects_rect(&self, rx: f32, ry: f32, rw: f32, rh: f32) -> bool {
883 self.x < rx + rw
884 && self.x + self.width > rx
885 && self.y < ry + rh
886 && self.y + self.height > ry
887 }
888}
889
890#[cfg(test)]
891mod tests {
892 use super::*;
893
894 #[test]
895 fn scene_graph_basics() {
896 let mut sg = SceneGraph::new();
897 let rect = SceneNode::new(
898 NodeId::intern("box1"),
899 NodeKind::Rect {
900 width: 100.0,
901 height: 50.0,
902 },
903 );
904 let idx = sg.add_node(sg.root, rect);
905
906 assert!(sg.get_by_id(NodeId::intern("box1")).is_some());
907 assert_eq!(sg.children(sg.root).len(), 1);
908 assert_eq!(sg.children(sg.root)[0], idx);
909 }
910
911 #[test]
912 fn color_hex_roundtrip() {
913 let c = Color::from_hex("#6C5CE7").unwrap();
914 assert_eq!(c.to_hex(), "#6C5CE7");
915
916 let c2 = Color::from_hex("#FF000080").unwrap();
917 assert!((c2.a - 128.0 / 255.0).abs() < 0.01);
918 assert!(c2.to_hex().len() == 9); }
920
921 #[test]
922 fn style_merging() {
923 let mut sg = SceneGraph::new();
924 sg.define_style(
925 NodeId::intern("base"),
926 Style {
927 fill: Some(Paint::Solid(Color::rgba(0.0, 0.0, 0.0, 1.0))),
928 font: Some(FontSpec {
929 family: "Inter".into(),
930 weight: 400,
931 size: 14.0,
932 }),
933 ..Default::default()
934 },
935 );
936
937 let mut node = SceneNode::new(
938 NodeId::intern("txt"),
939 NodeKind::Text {
940 content: "hi".into(),
941 },
942 );
943 node.use_styles.push(NodeId::intern("base"));
944 node.style.font = Some(FontSpec {
945 family: "Inter".into(),
946 weight: 700,
947 size: 24.0,
948 });
949
950 let resolved = sg.resolve_style(&node, &[]);
951 assert!(resolved.fill.is_some());
953 let f = resolved.font.unwrap();
955 assert_eq!(f.weight, 700);
956 assert_eq!(f.size, 24.0);
957 }
958
959 #[test]
960 fn style_merging_align() {
961 let mut sg = SceneGraph::new();
962 sg.define_style(
963 NodeId::intern("centered"),
964 Style {
965 text_align: Some(TextAlign::Center),
966 text_valign: Some(TextVAlign::Middle),
967 ..Default::default()
968 },
969 );
970
971 let mut node = SceneNode::new(
973 NodeId::intern("overridden"),
974 NodeKind::Text {
975 content: "hello".into(),
976 },
977 );
978 node.use_styles.push(NodeId::intern("centered"));
979 node.style.text_align = Some(TextAlign::Right);
980
981 let resolved = sg.resolve_style(&node, &[]);
982 assert_eq!(resolved.text_align, Some(TextAlign::Right));
984 assert_eq!(resolved.text_valign, Some(TextVAlign::Middle));
986 }
987
988 #[test]
989 fn test_effective_target_returns_leaf() {
990 let mut sg = SceneGraph::new();
991
992 let group_id = NodeId::intern("my_group");
994 let rect_id = NodeId::intern("my_rect");
995
996 let group = SceneNode::new(
997 group_id,
998 NodeKind::Group {
999 layout: LayoutMode::Free,
1000 },
1001 );
1002 let rect = SceneNode::new(
1003 rect_id,
1004 NodeKind::Rect {
1005 width: 10.0,
1006 height: 10.0,
1007 },
1008 );
1009
1010 let group_idx = sg.add_node(sg.root, group);
1011 sg.add_node(group_idx, rect);
1012
1013 assert_eq!(sg.effective_target(rect_id, &[]), rect_id);
1015 assert_eq!(sg.effective_target(rect_id, &[group_id]), rect_id);
1016 assert_eq!(sg.effective_target(rect_id, &[rect_id]), rect_id);
1017 }
1018
1019 #[test]
1020 fn test_effective_target_nested_returns_leaf() {
1021 let mut sg = SceneGraph::new();
1022
1023 let outer_id = NodeId::intern("group_outer");
1025 let inner_id = NodeId::intern("group_inner");
1026 let leaf_id = NodeId::intern("rect_leaf");
1027
1028 let outer = SceneNode::new(
1029 outer_id,
1030 NodeKind::Group {
1031 layout: LayoutMode::Free,
1032 },
1033 );
1034 let inner = SceneNode::new(
1035 inner_id,
1036 NodeKind::Group {
1037 layout: LayoutMode::Free,
1038 },
1039 );
1040 let leaf = SceneNode::new(
1041 leaf_id,
1042 NodeKind::Rect {
1043 width: 50.0,
1044 height: 50.0,
1045 },
1046 );
1047
1048 let outer_idx = sg.add_node(sg.root, outer);
1049 let inner_idx = sg.add_node(outer_idx, inner);
1050 sg.add_node(inner_idx, leaf);
1051
1052 assert_eq!(sg.effective_target(leaf_id, &[]), leaf_id);
1054 assert_eq!(sg.effective_target(leaf_id, &[outer_id]), leaf_id);
1055 assert_eq!(sg.effective_target(leaf_id, &[outer_id, inner_id]), leaf_id);
1056 }
1057
1058 #[test]
1059 fn test_is_ancestor_of() {
1060 let mut sg = SceneGraph::new();
1061
1062 let group_id = NodeId::intern("grp");
1064 let rect_id = NodeId::intern("r1");
1065 let other_id = NodeId::intern("other");
1066
1067 let group = SceneNode::new(
1068 group_id,
1069 NodeKind::Group {
1070 layout: LayoutMode::Free,
1071 },
1072 );
1073 let rect = SceneNode::new(
1074 rect_id,
1075 NodeKind::Rect {
1076 width: 10.0,
1077 height: 10.0,
1078 },
1079 );
1080 let other = SceneNode::new(
1081 other_id,
1082 NodeKind::Rect {
1083 width: 5.0,
1084 height: 5.0,
1085 },
1086 );
1087
1088 let group_idx = sg.add_node(sg.root, group);
1089 sg.add_node(group_idx, rect);
1090 sg.add_node(sg.root, other);
1091
1092 assert!(sg.is_ancestor_of(group_id, rect_id));
1094 assert!(sg.is_ancestor_of(NodeId::intern("root"), rect_id));
1096 assert!(!sg.is_ancestor_of(rect_id, group_id));
1098 assert!(!sg.is_ancestor_of(group_id, group_id));
1100 assert!(!sg.is_ancestor_of(other_id, rect_id));
1102 }
1103
1104 #[test]
1105 fn test_resolve_style_scale_animation() {
1106 let sg = SceneGraph::new();
1107
1108 let mut node = SceneNode::new(
1109 NodeId::intern("btn"),
1110 NodeKind::Rect {
1111 width: 100.0,
1112 height: 40.0,
1113 },
1114 );
1115 node.style.fill = Some(Paint::Solid(Color::rgba(1.0, 0.0, 0.0, 1.0)));
1116 node.animations.push(AnimKeyframe {
1117 trigger: AnimTrigger::Press,
1118 duration_ms: 100,
1119 easing: Easing::EaseOut,
1120 properties: AnimProperties {
1121 scale: Some(0.97),
1122 ..Default::default()
1123 },
1124 });
1125
1126 let resolved = sg.resolve_style(&node, &[]);
1128 assert!(resolved.scale.is_none());
1129
1130 let resolved = sg.resolve_style(&node, &[AnimTrigger::Press]);
1132 assert_eq!(resolved.scale, Some(0.97));
1133 assert!(resolved.fill.is_some());
1135 }
1136}