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 pub label_offset: Option<(f32, f32)>,
371}
372
373#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
375pub enum FlowKind {
376 Pulse,
378 Dash,
380}
381
382#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
384pub struct FlowAnim {
385 pub kind: FlowKind,
386 pub duration_ms: u32,
387}
388
389#[derive(Debug, Clone, Default, Serialize, Deserialize)]
391pub enum LayoutMode {
392 #[default]
394 Free,
395 Column { gap: f32, pad: f32 },
397 Row { gap: f32, pad: f32 },
399 Grid { cols: u32, gap: f32, pad: f32 },
401}
402
403#[derive(Debug, Clone, Serialize, Deserialize)]
407pub enum NodeKind {
408 Root,
410
411 Generic,
414
415 Group { layout: LayoutMode },
417
418 Frame {
421 width: f32,
422 height: f32,
423 clip: bool,
424 layout: LayoutMode,
425 },
426
427 Rect { width: f32, height: f32 },
429
430 Ellipse { rx: f32, ry: f32 },
432
433 Path { commands: Vec<PathCmd> },
435
436 Text { content: String },
438}
439
440impl NodeKind {
441 pub fn kind_name(&self) -> &'static str {
443 match self {
444 Self::Root => "root",
445 Self::Generic => "generic",
446 Self::Group { .. } => "group",
447 Self::Frame { .. } => "frame",
448 Self::Rect { .. } => "rect",
449 Self::Ellipse { .. } => "ellipse",
450 Self::Path { .. } => "path",
451 Self::Text { .. } => "text",
452 }
453 }
454}
455
456#[derive(Debug, Clone, Serialize, Deserialize)]
458pub struct SceneNode {
459 pub id: NodeId,
461
462 pub kind: NodeKind,
464
465 pub style: Style,
467
468 pub use_styles: SmallVec<[NodeId; 2]>,
470
471 pub constraints: SmallVec<[Constraint; 2]>,
473
474 pub animations: SmallVec<[AnimKeyframe; 2]>,
476
477 pub annotations: Vec<Annotation>,
479
480 pub comments: Vec<String>,
483}
484
485impl SceneNode {
486 pub fn new(id: NodeId, kind: NodeKind) -> Self {
487 Self {
488 id,
489 kind,
490 style: Style::default(),
491 use_styles: SmallVec::new(),
492 constraints: SmallVec::new(),
493 animations: SmallVec::new(),
494 annotations: Vec::new(),
495 comments: Vec::new(),
496 }
497 }
498}
499
500#[derive(Debug, Clone)]
507pub struct SceneGraph {
508 pub graph: StableDiGraph<SceneNode, ()>,
510
511 pub root: NodeIndex,
513
514 pub styles: HashMap<NodeId, Style>,
516
517 pub id_index: HashMap<NodeId, NodeIndex>,
519
520 pub edges: Vec<Edge>,
522
523 pub imports: Vec<Import>,
525
526 pub sorted_child_order: HashMap<NodeIndex, Vec<NodeIndex>>,
530}
531
532impl SceneGraph {
533 #[must_use]
535 pub fn new() -> Self {
536 let mut graph = StableDiGraph::new();
537 let root_node = SceneNode::new(NodeId::intern("root"), NodeKind::Root);
538 let root = graph.add_node(root_node);
539
540 let mut id_index = HashMap::new();
541 id_index.insert(NodeId::intern("root"), root);
542
543 Self {
544 graph,
545 root,
546 styles: HashMap::new(),
547 id_index,
548 edges: Vec::new(),
549 imports: Vec::new(),
550 sorted_child_order: HashMap::new(),
551 }
552 }
553
554 pub fn add_node(&mut self, parent: NodeIndex, node: SceneNode) -> NodeIndex {
556 let id = node.id;
557 let idx = self.graph.add_node(node);
558 self.graph.add_edge(parent, idx, ());
559 self.id_index.insert(id, idx);
560 idx
561 }
562
563 pub fn remove_node(&mut self, idx: NodeIndex) -> Option<SceneNode> {
565 let removed = self.graph.remove_node(idx);
566 if let Some(removed_node) = &removed {
567 self.id_index.remove(&removed_node.id);
568 }
569 removed
570 }
571
572 pub fn get_by_id(&self, id: NodeId) -> Option<&SceneNode> {
574 self.id_index.get(&id).map(|idx| &self.graph[*idx])
575 }
576
577 pub fn get_by_id_mut(&mut self, id: NodeId) -> Option<&mut SceneNode> {
579 self.id_index
580 .get(&id)
581 .copied()
582 .map(|idx| &mut self.graph[idx])
583 }
584
585 pub fn index_of(&self, id: NodeId) -> Option<NodeIndex> {
587 self.id_index.get(&id).copied()
588 }
589
590 pub fn parent(&self, idx: NodeIndex) -> Option<NodeIndex> {
592 self.graph
593 .neighbors_directed(idx, petgraph::Direction::Incoming)
594 .next()
595 }
596
597 pub fn reparent_node(&mut self, child: NodeIndex, new_parent: NodeIndex) {
599 if let Some(old_parent) = self.parent(child)
600 && let Some(edge) = self.graph.find_edge(old_parent, child)
601 {
602 self.graph.remove_edge(edge);
603 }
604 self.graph.add_edge(new_parent, child, ());
605 }
606
607 pub fn children(&self, idx: NodeIndex) -> Vec<NodeIndex> {
613 if let Some(order) = self.sorted_child_order.get(&idx) {
615 return order.clone();
616 }
617
618 let mut children: Vec<NodeIndex> = self
619 .graph
620 .neighbors_directed(idx, petgraph::Direction::Outgoing)
621 .collect();
622 children.sort();
623 children
624 }
625
626 pub fn send_backward(&mut self, child: NodeIndex) -> bool {
629 let parent = match self.parent(child) {
630 Some(p) => p,
631 None => return false,
632 };
633 let siblings = self.children(parent);
634 let pos = match siblings.iter().position(|&s| s == child) {
635 Some(p) => p,
636 None => return false,
637 };
638 if pos == 0 {
639 return false; }
641 self.rebuild_child_order(parent, &siblings, pos, pos - 1)
643 }
644
645 pub fn bring_forward(&mut self, child: NodeIndex) -> bool {
648 let parent = match self.parent(child) {
649 Some(p) => p,
650 None => return false,
651 };
652 let siblings = self.children(parent);
653 let pos = match siblings.iter().position(|&s| s == child) {
654 Some(p) => p,
655 None => return false,
656 };
657 if pos >= siblings.len() - 1 {
658 return false; }
660 self.rebuild_child_order(parent, &siblings, pos, pos + 1)
661 }
662
663 pub fn send_to_back(&mut self, child: NodeIndex) -> bool {
665 let parent = match self.parent(child) {
666 Some(p) => p,
667 None => return false,
668 };
669 let siblings = self.children(parent);
670 let pos = match siblings.iter().position(|&s| s == child) {
671 Some(p) => p,
672 None => return false,
673 };
674 if pos == 0 {
675 return false;
676 }
677 self.rebuild_child_order(parent, &siblings, pos, 0)
678 }
679
680 pub fn bring_to_front(&mut self, child: NodeIndex) -> bool {
682 let parent = match self.parent(child) {
683 Some(p) => p,
684 None => return false,
685 };
686 let siblings = self.children(parent);
687 let pos = match siblings.iter().position(|&s| s == child) {
688 Some(p) => p,
689 None => return false,
690 };
691 let last = siblings.len() - 1;
692 if pos == last {
693 return false;
694 }
695 self.rebuild_child_order(parent, &siblings, pos, last)
696 }
697
698 fn rebuild_child_order(
700 &mut self,
701 parent: NodeIndex,
702 siblings: &[NodeIndex],
703 from: usize,
704 to: usize,
705 ) -> bool {
706 for &sib in siblings {
708 if let Some(edge) = self.graph.find_edge(parent, sib) {
709 self.graph.remove_edge(edge);
710 }
711 }
712 let mut new_order: Vec<NodeIndex> = siblings.to_vec();
714 let child = new_order.remove(from);
715 new_order.insert(to, child);
716 for &sib in &new_order {
718 self.graph.add_edge(parent, sib, ());
719 }
720 true
721 }
722
723 pub fn define_style(&mut self, name: NodeId, style: Style) {
725 self.styles.insert(name, style);
726 }
727
728 pub fn resolve_style(&self, node: &SceneNode, active_triggers: &[AnimTrigger]) -> Style {
730 let mut resolved = Style::default();
731
732 for style_id in &node.use_styles {
734 if let Some(base) = self.styles.get(style_id) {
735 merge_style(&mut resolved, base);
736 }
737 }
738
739 merge_style(&mut resolved, &node.style);
741
742 for anim in &node.animations {
744 if active_triggers.contains(&anim.trigger) {
745 if anim.properties.fill.is_some() {
746 resolved.fill = anim.properties.fill.clone();
747 }
748 if anim.properties.opacity.is_some() {
749 resolved.opacity = anim.properties.opacity;
750 }
751 if anim.properties.scale.is_some() {
752 resolved.scale = anim.properties.scale;
753 }
754 }
755 }
756
757 resolved
758 }
759
760 pub fn rebuild_index(&mut self) {
762 self.id_index.clear();
763 for idx in self.graph.node_indices() {
764 let id = self.graph[idx].id;
765 self.id_index.insert(id, idx);
766 }
767 }
768
769 pub fn resolve_style_for_edge(&self, edge: &Edge, active_triggers: &[AnimTrigger]) -> Style {
771 let mut resolved = Style::default();
772 for style_id in &edge.use_styles {
773 if let Some(base) = self.styles.get(style_id) {
774 merge_style(&mut resolved, base);
775 }
776 }
777 merge_style(&mut resolved, &edge.style);
778
779 for anim in &edge.animations {
780 if active_triggers.contains(&anim.trigger) {
781 if anim.properties.fill.is_some() {
782 resolved.fill = anim.properties.fill.clone();
783 }
784 if anim.properties.opacity.is_some() {
785 resolved.opacity = anim.properties.opacity;
786 }
787 if anim.properties.scale.is_some() {
788 resolved.scale = anim.properties.scale;
789 }
790 }
791 }
792
793 resolved
794 }
795
796 pub fn effective_target(&self, leaf_id: NodeId, _selected: &[NodeId]) -> NodeId {
800 leaf_id
801 }
802
803 pub fn is_ancestor_of(&self, ancestor_id: NodeId, descendant_id: NodeId) -> bool {
805 if ancestor_id == descendant_id {
806 return false;
807 }
808 let mut current_idx = match self.index_of(descendant_id) {
809 Some(idx) => idx,
810 None => return false,
811 };
812 while let Some(parent_idx) = self.parent(current_idx) {
813 if self.graph[parent_idx].id == ancestor_id {
814 return true;
815 }
816 if matches!(self.graph[parent_idx].kind, NodeKind::Root) {
817 break;
818 }
819 current_idx = parent_idx;
820 }
821 false
822 }
823}
824
825impl Default for SceneGraph {
826 fn default() -> Self {
827 Self::new()
828 }
829}
830
831fn merge_style(dst: &mut Style, src: &Style) {
833 if src.fill.is_some() {
834 dst.fill = src.fill.clone();
835 }
836 if src.stroke.is_some() {
837 dst.stroke = src.stroke.clone();
838 }
839 if src.font.is_some() {
840 dst.font = src.font.clone();
841 }
842 if src.corner_radius.is_some() {
843 dst.corner_radius = src.corner_radius;
844 }
845 if src.opacity.is_some() {
846 dst.opacity = src.opacity;
847 }
848 if src.shadow.is_some() {
849 dst.shadow = src.shadow.clone();
850 }
851
852 if src.text_align.is_some() {
853 dst.text_align = src.text_align;
854 }
855 if src.text_valign.is_some() {
856 dst.text_valign = src.text_valign;
857 }
858 if src.scale.is_some() {
859 dst.scale = src.scale;
860 }
861}
862
863#[derive(Debug, Clone, Copy, Default, PartialEq)]
867pub struct ResolvedBounds {
868 pub x: f32,
869 pub y: f32,
870 pub width: f32,
871 pub height: f32,
872}
873
874impl ResolvedBounds {
875 pub fn contains(&self, px: f32, py: f32) -> bool {
876 px >= self.x && px <= self.x + self.width && py >= self.y && py <= self.y + self.height
877 }
878
879 pub fn center(&self) -> (f32, f32) {
880 (self.x + self.width / 2.0, self.y + self.height / 2.0)
881 }
882
883 pub fn intersects_rect(&self, rx: f32, ry: f32, rw: f32, rh: f32) -> bool {
885 self.x < rx + rw
886 && self.x + self.width > rx
887 && self.y < ry + rh
888 && self.y + self.height > ry
889 }
890}
891
892#[cfg(test)]
893mod tests {
894 use super::*;
895
896 #[test]
897 fn scene_graph_basics() {
898 let mut sg = SceneGraph::new();
899 let rect = SceneNode::new(
900 NodeId::intern("box1"),
901 NodeKind::Rect {
902 width: 100.0,
903 height: 50.0,
904 },
905 );
906 let idx = sg.add_node(sg.root, rect);
907
908 assert!(sg.get_by_id(NodeId::intern("box1")).is_some());
909 assert_eq!(sg.children(sg.root).len(), 1);
910 assert_eq!(sg.children(sg.root)[0], idx);
911 }
912
913 #[test]
914 fn color_hex_roundtrip() {
915 let c = Color::from_hex("#6C5CE7").unwrap();
916 assert_eq!(c.to_hex(), "#6C5CE7");
917
918 let c2 = Color::from_hex("#FF000080").unwrap();
919 assert!((c2.a - 128.0 / 255.0).abs() < 0.01);
920 assert!(c2.to_hex().len() == 9); }
922
923 #[test]
924 fn style_merging() {
925 let mut sg = SceneGraph::new();
926 sg.define_style(
927 NodeId::intern("base"),
928 Style {
929 fill: Some(Paint::Solid(Color::rgba(0.0, 0.0, 0.0, 1.0))),
930 font: Some(FontSpec {
931 family: "Inter".into(),
932 weight: 400,
933 size: 14.0,
934 }),
935 ..Default::default()
936 },
937 );
938
939 let mut node = SceneNode::new(
940 NodeId::intern("txt"),
941 NodeKind::Text {
942 content: "hi".into(),
943 },
944 );
945 node.use_styles.push(NodeId::intern("base"));
946 node.style.font = Some(FontSpec {
947 family: "Inter".into(),
948 weight: 700,
949 size: 24.0,
950 });
951
952 let resolved = sg.resolve_style(&node, &[]);
953 assert!(resolved.fill.is_some());
955 let f = resolved.font.unwrap();
957 assert_eq!(f.weight, 700);
958 assert_eq!(f.size, 24.0);
959 }
960
961 #[test]
962 fn style_merging_align() {
963 let mut sg = SceneGraph::new();
964 sg.define_style(
965 NodeId::intern("centered"),
966 Style {
967 text_align: Some(TextAlign::Center),
968 text_valign: Some(TextVAlign::Middle),
969 ..Default::default()
970 },
971 );
972
973 let mut node = SceneNode::new(
975 NodeId::intern("overridden"),
976 NodeKind::Text {
977 content: "hello".into(),
978 },
979 );
980 node.use_styles.push(NodeId::intern("centered"));
981 node.style.text_align = Some(TextAlign::Right);
982
983 let resolved = sg.resolve_style(&node, &[]);
984 assert_eq!(resolved.text_align, Some(TextAlign::Right));
986 assert_eq!(resolved.text_valign, Some(TextVAlign::Middle));
988 }
989
990 #[test]
991 fn test_effective_target_returns_leaf() {
992 let mut sg = SceneGraph::new();
993
994 let group_id = NodeId::intern("my_group");
996 let rect_id = NodeId::intern("my_rect");
997
998 let group = SceneNode::new(
999 group_id,
1000 NodeKind::Group {
1001 layout: LayoutMode::Free,
1002 },
1003 );
1004 let rect = SceneNode::new(
1005 rect_id,
1006 NodeKind::Rect {
1007 width: 10.0,
1008 height: 10.0,
1009 },
1010 );
1011
1012 let group_idx = sg.add_node(sg.root, group);
1013 sg.add_node(group_idx, rect);
1014
1015 assert_eq!(sg.effective_target(rect_id, &[]), rect_id);
1017 assert_eq!(sg.effective_target(rect_id, &[group_id]), rect_id);
1018 assert_eq!(sg.effective_target(rect_id, &[rect_id]), rect_id);
1019 }
1020
1021 #[test]
1022 fn test_effective_target_nested_returns_leaf() {
1023 let mut sg = SceneGraph::new();
1024
1025 let outer_id = NodeId::intern("group_outer");
1027 let inner_id = NodeId::intern("group_inner");
1028 let leaf_id = NodeId::intern("rect_leaf");
1029
1030 let outer = SceneNode::new(
1031 outer_id,
1032 NodeKind::Group {
1033 layout: LayoutMode::Free,
1034 },
1035 );
1036 let inner = SceneNode::new(
1037 inner_id,
1038 NodeKind::Group {
1039 layout: LayoutMode::Free,
1040 },
1041 );
1042 let leaf = SceneNode::new(
1043 leaf_id,
1044 NodeKind::Rect {
1045 width: 50.0,
1046 height: 50.0,
1047 },
1048 );
1049
1050 let outer_idx = sg.add_node(sg.root, outer);
1051 let inner_idx = sg.add_node(outer_idx, inner);
1052 sg.add_node(inner_idx, leaf);
1053
1054 assert_eq!(sg.effective_target(leaf_id, &[]), leaf_id);
1056 assert_eq!(sg.effective_target(leaf_id, &[outer_id]), leaf_id);
1057 assert_eq!(sg.effective_target(leaf_id, &[outer_id, inner_id]), leaf_id);
1058 }
1059
1060 #[test]
1061 fn test_is_ancestor_of() {
1062 let mut sg = SceneGraph::new();
1063
1064 let group_id = NodeId::intern("grp");
1066 let rect_id = NodeId::intern("r1");
1067 let other_id = NodeId::intern("other");
1068
1069 let group = SceneNode::new(
1070 group_id,
1071 NodeKind::Group {
1072 layout: LayoutMode::Free,
1073 },
1074 );
1075 let rect = SceneNode::new(
1076 rect_id,
1077 NodeKind::Rect {
1078 width: 10.0,
1079 height: 10.0,
1080 },
1081 );
1082 let other = SceneNode::new(
1083 other_id,
1084 NodeKind::Rect {
1085 width: 5.0,
1086 height: 5.0,
1087 },
1088 );
1089
1090 let group_idx = sg.add_node(sg.root, group);
1091 sg.add_node(group_idx, rect);
1092 sg.add_node(sg.root, other);
1093
1094 assert!(sg.is_ancestor_of(group_id, rect_id));
1096 assert!(sg.is_ancestor_of(NodeId::intern("root"), rect_id));
1098 assert!(!sg.is_ancestor_of(rect_id, group_id));
1100 assert!(!sg.is_ancestor_of(group_id, group_id));
1102 assert!(!sg.is_ancestor_of(other_id, rect_id));
1104 }
1105
1106 #[test]
1107 fn test_resolve_style_scale_animation() {
1108 let sg = SceneGraph::new();
1109
1110 let mut node = SceneNode::new(
1111 NodeId::intern("btn"),
1112 NodeKind::Rect {
1113 width: 100.0,
1114 height: 40.0,
1115 },
1116 );
1117 node.style.fill = Some(Paint::Solid(Color::rgba(1.0, 0.0, 0.0, 1.0)));
1118 node.animations.push(AnimKeyframe {
1119 trigger: AnimTrigger::Press,
1120 duration_ms: 100,
1121 easing: Easing::EaseOut,
1122 properties: AnimProperties {
1123 scale: Some(0.97),
1124 ..Default::default()
1125 },
1126 });
1127
1128 let resolved = sg.resolve_style(&node, &[]);
1130 assert!(resolved.scale.is_none());
1131
1132 let resolved = sg.resolve_style(&node, &[AnimTrigger::Press]);
1134 assert_eq!(resolved.scale, Some(0.97));
1135 assert!(resolved.fill.is_some());
1137 }
1138}