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
27impl Color {
28 pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
29 Self { r, g, b, a }
30 }
31
32 pub fn from_hex(hex: &str) -> Option<Self> {
34 let hex = hex.strip_prefix('#')?;
35 let bytes = hex.as_bytes();
36
37 fn hex_val(c: u8) -> Option<u8> {
38 match c {
39 b'0'..=b'9' => Some(c - b'0'),
40 b'a'..=b'f' => Some(c - b'a' + 10),
41 b'A'..=b'F' => Some(c - b'A' + 10),
42 _ => None,
43 }
44 }
45
46 match bytes.len() {
47 3 => {
48 let r = hex_val(bytes[0])?;
49 let g = hex_val(bytes[1])?;
50 let b = hex_val(bytes[2])?;
51 Some(Self::rgba(
52 (r * 17) as f32 / 255.0,
53 (g * 17) as f32 / 255.0,
54 (b * 17) as f32 / 255.0,
55 1.0,
56 ))
57 }
58 4 => {
59 let r = hex_val(bytes[0])?;
60 let g = hex_val(bytes[1])?;
61 let b = hex_val(bytes[2])?;
62 let a = hex_val(bytes[3])?;
63 Some(Self::rgba(
64 (r * 17) as f32 / 255.0,
65 (g * 17) as f32 / 255.0,
66 (b * 17) as f32 / 255.0,
67 (a * 17) as f32 / 255.0,
68 ))
69 }
70 6 => {
71 let r = hex_val(bytes[0])? << 4 | hex_val(bytes[1])?;
72 let g = hex_val(bytes[2])? << 4 | hex_val(bytes[3])?;
73 let b = hex_val(bytes[4])? << 4 | hex_val(bytes[5])?;
74 Some(Self::rgba(
75 r as f32 / 255.0,
76 g as f32 / 255.0,
77 b as f32 / 255.0,
78 1.0,
79 ))
80 }
81 8 => {
82 let r = hex_val(bytes[0])? << 4 | hex_val(bytes[1])?;
83 let g = hex_val(bytes[2])? << 4 | hex_val(bytes[3])?;
84 let b = hex_val(bytes[4])? << 4 | hex_val(bytes[5])?;
85 let a = hex_val(bytes[6])? << 4 | hex_val(bytes[7])?;
86 Some(Self::rgba(
87 r as f32 / 255.0,
88 g as f32 / 255.0,
89 b as f32 / 255.0,
90 a as f32 / 255.0,
91 ))
92 }
93 _ => None,
94 }
95 }
96
97 pub fn to_hex(&self) -> String {
99 let r = (self.r * 255.0).round() as u8;
100 let g = (self.g * 255.0).round() as u8;
101 let b = (self.b * 255.0).round() as u8;
102 let a = (self.a * 255.0).round() as u8;
103 if a == 255 {
104 format!("#{r:02X}{g:02X}{b:02X}")
105 } else {
106 format!("#{r:02X}{g:02X}{b:02X}{a:02X}")
107 }
108 }
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct GradientStop {
114 pub offset: f32, pub color: Color,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
120pub enum Paint {
121 Solid(Color),
122 LinearGradient {
123 angle: f32, stops: Vec<GradientStop>,
125 },
126 RadialGradient {
127 stops: Vec<GradientStop>,
128 },
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct Stroke {
135 pub paint: Paint,
136 pub width: f32,
137 pub cap: StrokeCap,
138 pub join: StrokeJoin,
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
142pub enum StrokeCap {
143 Butt,
144 Round,
145 Square,
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
149pub enum StrokeJoin {
150 Miter,
151 Round,
152 Bevel,
153}
154
155impl Default for Stroke {
156 fn default() -> Self {
157 Self {
158 paint: Paint::Solid(Color::rgba(0.0, 0.0, 0.0, 1.0)),
159 width: 1.0,
160 cap: StrokeCap::Butt,
161 join: StrokeJoin::Miter,
162 }
163 }
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct FontSpec {
170 pub family: String,
171 pub weight: u16, pub size: f32,
173}
174
175impl Default for FontSpec {
176 fn default() -> Self {
177 Self {
178 family: "Inter".into(),
179 weight: 400,
180 size: 14.0,
181 }
182 }
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize)]
189pub enum PathCmd {
190 MoveTo(f32, f32),
191 LineTo(f32, f32),
192 QuadTo(f32, f32, f32, f32), CubicTo(f32, f32, f32, f32, f32, f32), Close,
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct Shadow {
201 pub offset_x: f32,
202 pub offset_y: f32,
203 pub blur: f32,
204 pub color: Color,
205}
206
207#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
211pub enum TextAlign {
212 Left,
213 #[default]
214 Center,
215 Right,
216}
217
218#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
220pub enum TextVAlign {
221 Top,
222 #[default]
223 Middle,
224 Bottom,
225}
226
227#[derive(Debug, Clone, Default, Serialize, Deserialize)]
229pub struct Style {
230 pub fill: Option<Paint>,
231 pub stroke: Option<Stroke>,
232 pub font: Option<FontSpec>,
233 pub corner_radius: Option<f32>,
234 pub opacity: Option<f32>,
235 pub shadow: Option<Shadow>,
236
237 pub text_align: Option<TextAlign>,
239 pub text_valign: Option<TextVAlign>,
241
242 pub scale: Option<f32>,
244}
245
246#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
250pub enum AnimTrigger {
251 Hover,
252 Press,
253 Enter, Custom(String),
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize)]
259pub enum Easing {
260 Linear,
261 EaseIn,
262 EaseOut,
263 EaseInOut,
264 Spring,
265 CubicBezier(f32, f32, f32, f32),
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize)]
270pub struct AnimKeyframe {
271 pub trigger: AnimTrigger,
272 pub duration_ms: u32,
273 pub easing: Easing,
274 pub properties: AnimProperties,
275}
276
277#[derive(Debug, Clone, Default, Serialize, Deserialize)]
279pub struct AnimProperties {
280 pub fill: Option<Paint>,
281 pub opacity: Option<f32>,
282 pub scale: Option<f32>,
283 pub rotate: Option<f32>, pub translate: Option<(f32, f32)>,
285}
286
287#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
292pub enum Annotation {
293 Description(String),
295 Accept(String),
297 Status(String),
299 Priority(String),
301 Tag(String),
303}
304
305#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
309pub struct Import {
310 pub path: String,
312 pub namespace: String,
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize)]
320pub enum Constraint {
321 CenterIn(NodeId),
323 Offset { from: NodeId, dx: f32, dy: f32 },
325 FillParent { pad: f32 },
327 Position { x: f32, y: f32 },
330}
331
332#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
336pub enum ArrowKind {
337 #[default]
338 None,
339 Start,
340 End,
341 Both,
342}
343
344#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
346pub enum CurveKind {
347 #[default]
348 Straight,
349 Smooth,
350 Step,
351}
352
353#[derive(Debug, Clone, Serialize, Deserialize)]
355pub struct Edge {
356 pub id: NodeId,
357 pub from: NodeId,
358 pub to: NodeId,
359 pub label: Option<String>,
360 pub style: Style,
361 pub use_styles: SmallVec<[NodeId; 2]>,
362 pub arrow: ArrowKind,
363 pub curve: CurveKind,
364 pub annotations: Vec<Annotation>,
365 pub animations: SmallVec<[AnimKeyframe; 2]>,
366 pub flow: Option<FlowAnim>,
367}
368
369#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
371pub enum FlowKind {
372 Pulse,
374 Dash,
376}
377
378#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
380pub struct FlowAnim {
381 pub kind: FlowKind,
382 pub duration_ms: u32,
383}
384
385#[derive(Debug, Clone, Default, Serialize, Deserialize)]
387pub enum LayoutMode {
388 #[default]
390 Free,
391 Column { gap: f32, pad: f32 },
393 Row { gap: f32, pad: f32 },
395 Grid { cols: u32, gap: f32, pad: f32 },
397}
398
399#[derive(Debug, Clone, Serialize, Deserialize)]
403pub enum NodeKind {
404 Root,
406
407 Generic,
410
411 Group { layout: LayoutMode },
413
414 Frame {
417 width: f32,
418 height: f32,
419 clip: bool,
420 layout: LayoutMode,
421 },
422
423 Rect { width: f32, height: f32 },
425
426 Ellipse { rx: f32, ry: f32 },
428
429 Path { commands: Vec<PathCmd> },
431
432 Text { content: String },
434}
435
436#[derive(Debug, Clone, Serialize, Deserialize)]
438pub struct SceneNode {
439 pub id: NodeId,
441
442 pub kind: NodeKind,
444
445 pub style: Style,
447
448 pub use_styles: SmallVec<[NodeId; 2]>,
450
451 pub constraints: SmallVec<[Constraint; 2]>,
453
454 pub animations: SmallVec<[AnimKeyframe; 2]>,
456
457 pub annotations: Vec<Annotation>,
459
460 pub comments: Vec<String>,
463}
464
465impl SceneNode {
466 pub fn new(id: NodeId, kind: NodeKind) -> Self {
467 Self {
468 id,
469 kind,
470 style: Style::default(),
471 use_styles: SmallVec::new(),
472 constraints: SmallVec::new(),
473 animations: SmallVec::new(),
474 annotations: Vec::new(),
475 comments: Vec::new(),
476 }
477 }
478}
479
480#[derive(Debug, Clone)]
487pub struct SceneGraph {
488 pub graph: StableDiGraph<SceneNode, ()>,
490
491 pub root: NodeIndex,
493
494 pub styles: HashMap<NodeId, Style>,
496
497 pub id_index: HashMap<NodeId, NodeIndex>,
499
500 pub edges: Vec<Edge>,
502
503 pub imports: Vec<Import>,
505
506 pub sorted_child_order: HashMap<NodeIndex, Vec<NodeIndex>>,
510}
511
512impl SceneGraph {
513 #[must_use]
515 pub fn new() -> Self {
516 let mut graph = StableDiGraph::new();
517 let root_node = SceneNode::new(NodeId::intern("root"), NodeKind::Root);
518 let root = graph.add_node(root_node);
519
520 let mut id_index = HashMap::new();
521 id_index.insert(NodeId::intern("root"), root);
522
523 Self {
524 graph,
525 root,
526 styles: HashMap::new(),
527 id_index,
528 edges: Vec::new(),
529 imports: Vec::new(),
530 sorted_child_order: HashMap::new(),
531 }
532 }
533
534 pub fn add_node(&mut self, parent: NodeIndex, node: SceneNode) -> NodeIndex {
536 let id = node.id;
537 let idx = self.graph.add_node(node);
538 self.graph.add_edge(parent, idx, ());
539 self.id_index.insert(id, idx);
540 idx
541 }
542
543 pub fn remove_node(&mut self, idx: NodeIndex) -> Option<SceneNode> {
545 let removed = self.graph.remove_node(idx);
546 if let Some(removed_node) = &removed {
547 self.id_index.remove(&removed_node.id);
548 }
549 removed
550 }
551
552 pub fn get_by_id(&self, id: NodeId) -> Option<&SceneNode> {
554 self.id_index.get(&id).map(|idx| &self.graph[*idx])
555 }
556
557 pub fn get_by_id_mut(&mut self, id: NodeId) -> Option<&mut SceneNode> {
559 self.id_index
560 .get(&id)
561 .copied()
562 .map(|idx| &mut self.graph[idx])
563 }
564
565 pub fn index_of(&self, id: NodeId) -> Option<NodeIndex> {
567 self.id_index.get(&id).copied()
568 }
569
570 pub fn parent(&self, idx: NodeIndex) -> Option<NodeIndex> {
572 self.graph
573 .neighbors_directed(idx, petgraph::Direction::Incoming)
574 .next()
575 }
576
577 pub fn reparent_node(&mut self, child: NodeIndex, new_parent: NodeIndex) {
579 if let Some(old_parent) = self.parent(child)
580 && let Some(edge) = self.graph.find_edge(old_parent, child)
581 {
582 self.graph.remove_edge(edge);
583 }
584 self.graph.add_edge(new_parent, child, ());
585 }
586
587 pub fn children(&self, idx: NodeIndex) -> Vec<NodeIndex> {
593 if let Some(order) = self.sorted_child_order.get(&idx) {
595 return order.clone();
596 }
597
598 let mut children: Vec<NodeIndex> = self
599 .graph
600 .neighbors_directed(idx, petgraph::Direction::Outgoing)
601 .collect();
602 children.sort();
603 children
604 }
605
606 pub fn send_backward(&mut self, child: NodeIndex) -> bool {
609 let parent = match self.parent(child) {
610 Some(p) => p,
611 None => return false,
612 };
613 let siblings = self.children(parent);
614 let pos = match siblings.iter().position(|&s| s == child) {
615 Some(p) => p,
616 None => return false,
617 };
618 if pos == 0 {
619 return false; }
621 self.rebuild_child_order(parent, &siblings, pos, pos - 1)
623 }
624
625 pub fn bring_forward(&mut self, child: NodeIndex) -> bool {
628 let parent = match self.parent(child) {
629 Some(p) => p,
630 None => return false,
631 };
632 let siblings = self.children(parent);
633 let pos = match siblings.iter().position(|&s| s == child) {
634 Some(p) => p,
635 None => return false,
636 };
637 if pos >= siblings.len() - 1 {
638 return false; }
640 self.rebuild_child_order(parent, &siblings, pos, pos + 1)
641 }
642
643 pub fn send_to_back(&mut self, child: NodeIndex) -> bool {
645 let parent = match self.parent(child) {
646 Some(p) => p,
647 None => return false,
648 };
649 let siblings = self.children(parent);
650 let pos = match siblings.iter().position(|&s| s == child) {
651 Some(p) => p,
652 None => return false,
653 };
654 if pos == 0 {
655 return false;
656 }
657 self.rebuild_child_order(parent, &siblings, pos, 0)
658 }
659
660 pub fn bring_to_front(&mut self, child: NodeIndex) -> bool {
662 let parent = match self.parent(child) {
663 Some(p) => p,
664 None => return false,
665 };
666 let siblings = self.children(parent);
667 let pos = match siblings.iter().position(|&s| s == child) {
668 Some(p) => p,
669 None => return false,
670 };
671 let last = siblings.len() - 1;
672 if pos == last {
673 return false;
674 }
675 self.rebuild_child_order(parent, &siblings, pos, last)
676 }
677
678 fn rebuild_child_order(
680 &mut self,
681 parent: NodeIndex,
682 siblings: &[NodeIndex],
683 from: usize,
684 to: usize,
685 ) -> bool {
686 for &sib in siblings {
688 if let Some(edge) = self.graph.find_edge(parent, sib) {
689 self.graph.remove_edge(edge);
690 }
691 }
692 let mut new_order: Vec<NodeIndex> = siblings.to_vec();
694 let child = new_order.remove(from);
695 new_order.insert(to, child);
696 for &sib in &new_order {
698 self.graph.add_edge(parent, sib, ());
699 }
700 true
701 }
702
703 pub fn define_style(&mut self, name: NodeId, style: Style) {
705 self.styles.insert(name, style);
706 }
707
708 pub fn resolve_style(&self, node: &SceneNode, active_triggers: &[AnimTrigger]) -> Style {
710 let mut resolved = Style::default();
711
712 for style_id in &node.use_styles {
714 if let Some(base) = self.styles.get(style_id) {
715 merge_style(&mut resolved, base);
716 }
717 }
718
719 merge_style(&mut resolved, &node.style);
721
722 for anim in &node.animations {
724 if active_triggers.contains(&anim.trigger) {
725 if anim.properties.fill.is_some() {
726 resolved.fill = anim.properties.fill.clone();
727 }
728 if anim.properties.opacity.is_some() {
729 resolved.opacity = anim.properties.opacity;
730 }
731 if anim.properties.scale.is_some() {
732 resolved.scale = anim.properties.scale;
733 }
734 }
735 }
736
737 resolved
738 }
739
740 pub fn rebuild_index(&mut self) {
742 self.id_index.clear();
743 for idx in self.graph.node_indices() {
744 let id = self.graph[idx].id;
745 self.id_index.insert(id, idx);
746 }
747 }
748
749 pub fn resolve_style_for_edge(&self, edge: &Edge, active_triggers: &[AnimTrigger]) -> Style {
751 let mut resolved = Style::default();
752 for style_id in &edge.use_styles {
753 if let Some(base) = self.styles.get(style_id) {
754 merge_style(&mut resolved, base);
755 }
756 }
757 merge_style(&mut resolved, &edge.style);
758
759 for anim in &edge.animations {
760 if active_triggers.contains(&anim.trigger) {
761 if anim.properties.fill.is_some() {
762 resolved.fill = anim.properties.fill.clone();
763 }
764 if anim.properties.opacity.is_some() {
765 resolved.opacity = anim.properties.opacity;
766 }
767 if anim.properties.scale.is_some() {
768 resolved.scale = anim.properties.scale;
769 }
770 }
771 }
772
773 resolved
774 }
775
776 pub fn effective_target(&self, leaf_id: NodeId, _selected: &[NodeId]) -> NodeId {
780 leaf_id
781 }
782
783 pub fn is_ancestor_of(&self, ancestor_id: NodeId, descendant_id: NodeId) -> bool {
785 if ancestor_id == descendant_id {
786 return false;
787 }
788 let mut current_idx = match self.index_of(descendant_id) {
789 Some(idx) => idx,
790 None => return false,
791 };
792 while let Some(parent_idx) = self.parent(current_idx) {
793 if self.graph[parent_idx].id == ancestor_id {
794 return true;
795 }
796 if matches!(self.graph[parent_idx].kind, NodeKind::Root) {
797 break;
798 }
799 current_idx = parent_idx;
800 }
801 false
802 }
803}
804
805impl Default for SceneGraph {
806 fn default() -> Self {
807 Self::new()
808 }
809}
810
811fn merge_style(dst: &mut Style, src: &Style) {
813 if src.fill.is_some() {
814 dst.fill = src.fill.clone();
815 }
816 if src.stroke.is_some() {
817 dst.stroke = src.stroke.clone();
818 }
819 if src.font.is_some() {
820 dst.font = src.font.clone();
821 }
822 if src.corner_radius.is_some() {
823 dst.corner_radius = src.corner_radius;
824 }
825 if src.opacity.is_some() {
826 dst.opacity = src.opacity;
827 }
828 if src.shadow.is_some() {
829 dst.shadow = src.shadow.clone();
830 }
831
832 if src.text_align.is_some() {
833 dst.text_align = src.text_align;
834 }
835 if src.text_valign.is_some() {
836 dst.text_valign = src.text_valign;
837 }
838 if src.scale.is_some() {
839 dst.scale = src.scale;
840 }
841}
842
843#[derive(Debug, Clone, Copy, Default)]
847pub struct ResolvedBounds {
848 pub x: f32,
849 pub y: f32,
850 pub width: f32,
851 pub height: f32,
852}
853
854impl ResolvedBounds {
855 pub fn contains(&self, px: f32, py: f32) -> bool {
856 px >= self.x && px <= self.x + self.width && py >= self.y && py <= self.y + self.height
857 }
858
859 pub fn center(&self) -> (f32, f32) {
860 (self.x + self.width / 2.0, self.y + self.height / 2.0)
861 }
862
863 pub fn intersects_rect(&self, rx: f32, ry: f32, rw: f32, rh: f32) -> bool {
865 self.x < rx + rw
866 && self.x + self.width > rx
867 && self.y < ry + rh
868 && self.y + self.height > ry
869 }
870}
871
872#[cfg(test)]
873mod tests {
874 use super::*;
875
876 #[test]
877 fn scene_graph_basics() {
878 let mut sg = SceneGraph::new();
879 let rect = SceneNode::new(
880 NodeId::intern("box1"),
881 NodeKind::Rect {
882 width: 100.0,
883 height: 50.0,
884 },
885 );
886 let idx = sg.add_node(sg.root, rect);
887
888 assert!(sg.get_by_id(NodeId::intern("box1")).is_some());
889 assert_eq!(sg.children(sg.root).len(), 1);
890 assert_eq!(sg.children(sg.root)[0], idx);
891 }
892
893 #[test]
894 fn color_hex_roundtrip() {
895 let c = Color::from_hex("#6C5CE7").unwrap();
896 assert_eq!(c.to_hex(), "#6C5CE7");
897
898 let c2 = Color::from_hex("#FF000080").unwrap();
899 assert!((c2.a - 128.0 / 255.0).abs() < 0.01);
900 assert!(c2.to_hex().len() == 9); }
902
903 #[test]
904 fn style_merging() {
905 let mut sg = SceneGraph::new();
906 sg.define_style(
907 NodeId::intern("base"),
908 Style {
909 fill: Some(Paint::Solid(Color::rgba(0.0, 0.0, 0.0, 1.0))),
910 font: Some(FontSpec {
911 family: "Inter".into(),
912 weight: 400,
913 size: 14.0,
914 }),
915 ..Default::default()
916 },
917 );
918
919 let mut node = SceneNode::new(
920 NodeId::intern("txt"),
921 NodeKind::Text {
922 content: "hi".into(),
923 },
924 );
925 node.use_styles.push(NodeId::intern("base"));
926 node.style.font = Some(FontSpec {
927 family: "Inter".into(),
928 weight: 700,
929 size: 24.0,
930 });
931
932 let resolved = sg.resolve_style(&node, &[]);
933 assert!(resolved.fill.is_some());
935 let f = resolved.font.unwrap();
937 assert_eq!(f.weight, 700);
938 assert_eq!(f.size, 24.0);
939 }
940
941 #[test]
942 fn style_merging_align() {
943 let mut sg = SceneGraph::new();
944 sg.define_style(
945 NodeId::intern("centered"),
946 Style {
947 text_align: Some(TextAlign::Center),
948 text_valign: Some(TextVAlign::Middle),
949 ..Default::default()
950 },
951 );
952
953 let mut node = SceneNode::new(
955 NodeId::intern("overridden"),
956 NodeKind::Text {
957 content: "hello".into(),
958 },
959 );
960 node.use_styles.push(NodeId::intern("centered"));
961 node.style.text_align = Some(TextAlign::Right);
962
963 let resolved = sg.resolve_style(&node, &[]);
964 assert_eq!(resolved.text_align, Some(TextAlign::Right));
966 assert_eq!(resolved.text_valign, Some(TextVAlign::Middle));
968 }
969
970 #[test]
971 fn test_effective_target_returns_leaf() {
972 let mut sg = SceneGraph::new();
973
974 let group_id = NodeId::intern("my_group");
976 let rect_id = NodeId::intern("my_rect");
977
978 let group = SceneNode::new(
979 group_id,
980 NodeKind::Group {
981 layout: LayoutMode::Free,
982 },
983 );
984 let rect = SceneNode::new(
985 rect_id,
986 NodeKind::Rect {
987 width: 10.0,
988 height: 10.0,
989 },
990 );
991
992 let group_idx = sg.add_node(sg.root, group);
993 sg.add_node(group_idx, rect);
994
995 assert_eq!(sg.effective_target(rect_id, &[]), rect_id);
997 assert_eq!(sg.effective_target(rect_id, &[group_id]), rect_id);
998 assert_eq!(sg.effective_target(rect_id, &[rect_id]), rect_id);
999 }
1000
1001 #[test]
1002 fn test_effective_target_nested_returns_leaf() {
1003 let mut sg = SceneGraph::new();
1004
1005 let outer_id = NodeId::intern("group_outer");
1007 let inner_id = NodeId::intern("group_inner");
1008 let leaf_id = NodeId::intern("rect_leaf");
1009
1010 let outer = SceneNode::new(
1011 outer_id,
1012 NodeKind::Group {
1013 layout: LayoutMode::Free,
1014 },
1015 );
1016 let inner = SceneNode::new(
1017 inner_id,
1018 NodeKind::Group {
1019 layout: LayoutMode::Free,
1020 },
1021 );
1022 let leaf = SceneNode::new(
1023 leaf_id,
1024 NodeKind::Rect {
1025 width: 50.0,
1026 height: 50.0,
1027 },
1028 );
1029
1030 let outer_idx = sg.add_node(sg.root, outer);
1031 let inner_idx = sg.add_node(outer_idx, inner);
1032 sg.add_node(inner_idx, leaf);
1033
1034 assert_eq!(sg.effective_target(leaf_id, &[]), leaf_id);
1036 assert_eq!(sg.effective_target(leaf_id, &[outer_id]), leaf_id);
1037 assert_eq!(sg.effective_target(leaf_id, &[outer_id, inner_id]), leaf_id);
1038 }
1039
1040 #[test]
1041 fn test_is_ancestor_of() {
1042 let mut sg = SceneGraph::new();
1043
1044 let group_id = NodeId::intern("grp");
1046 let rect_id = NodeId::intern("r1");
1047 let other_id = NodeId::intern("other");
1048
1049 let group = SceneNode::new(
1050 group_id,
1051 NodeKind::Group {
1052 layout: LayoutMode::Free,
1053 },
1054 );
1055 let rect = SceneNode::new(
1056 rect_id,
1057 NodeKind::Rect {
1058 width: 10.0,
1059 height: 10.0,
1060 },
1061 );
1062 let other = SceneNode::new(
1063 other_id,
1064 NodeKind::Rect {
1065 width: 5.0,
1066 height: 5.0,
1067 },
1068 );
1069
1070 let group_idx = sg.add_node(sg.root, group);
1071 sg.add_node(group_idx, rect);
1072 sg.add_node(sg.root, other);
1073
1074 assert!(sg.is_ancestor_of(group_id, rect_id));
1076 assert!(sg.is_ancestor_of(NodeId::intern("root"), rect_id));
1078 assert!(!sg.is_ancestor_of(rect_id, group_id));
1080 assert!(!sg.is_ancestor_of(group_id, group_id));
1082 assert!(!sg.is_ancestor_of(other_id, rect_id));
1084 }
1085
1086 #[test]
1087 fn test_resolve_style_scale_animation() {
1088 let sg = SceneGraph::new();
1089
1090 let mut node = SceneNode::new(
1091 NodeId::intern("btn"),
1092 NodeKind::Rect {
1093 width: 100.0,
1094 height: 40.0,
1095 },
1096 );
1097 node.style.fill = Some(Paint::Solid(Color::rgba(1.0, 0.0, 0.0, 1.0)));
1098 node.animations.push(AnimKeyframe {
1099 trigger: AnimTrigger::Press,
1100 duration_ms: 100,
1101 easing: Easing::EaseOut,
1102 properties: AnimProperties {
1103 scale: Some(0.97),
1104 ..Default::default()
1105 },
1106 });
1107
1108 let resolved = sg.resolve_style(&node, &[]);
1110 assert!(resolved.scale.is_none());
1111
1112 let resolved = sg.resolve_style(&node, &[AnimTrigger::Press]);
1114 assert_eq!(resolved.scale, Some(0.97));
1115 assert!(resolved.fill.is_some());
1117 }
1118}