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, PartialEq, Serialize, Deserialize)]
357pub enum EdgeAnchor {
358 Node(NodeId),
360 Point(f32, f32),
362}
363
364impl EdgeAnchor {
365 pub fn node_id(&self) -> Option<NodeId> {
367 match self {
368 Self::Node(id) => Some(*id),
369 Self::Point(_, _) => None,
370 }
371 }
372}
373
374#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct Edge {
377 pub id: NodeId,
378 pub from: EdgeAnchor,
379 pub to: EdgeAnchor,
380 pub text_child: Option<NodeId>,
382 pub style: Style,
383 pub use_styles: SmallVec<[NodeId; 2]>,
384 pub arrow: ArrowKind,
385 pub curve: CurveKind,
386 pub annotations: Vec<Annotation>,
387 pub animations: SmallVec<[AnimKeyframe; 2]>,
388 pub flow: Option<FlowAnim>,
389 pub label_offset: Option<(f32, f32)>,
391}
392
393#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
395pub enum FlowKind {
396 Pulse,
398 Dash,
400}
401
402#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
404pub struct FlowAnim {
405 pub kind: FlowKind,
406 pub duration_ms: u32,
407}
408
409#[derive(Debug, Clone, Default, Serialize, Deserialize)]
411pub enum LayoutMode {
412 #[default]
414 Free,
415 Column { gap: f32, pad: f32 },
417 Row { gap: f32, pad: f32 },
419 Grid { cols: u32, gap: f32, pad: f32 },
421}
422
423#[derive(Debug, Clone, Serialize, Deserialize)]
427pub enum NodeKind {
428 Root,
430
431 Generic,
434
435 Group,
438
439 Frame {
442 width: f32,
443 height: f32,
444 clip: bool,
445 layout: LayoutMode,
446 },
447
448 Rect { width: f32, height: f32 },
450
451 Ellipse { rx: f32, ry: f32 },
453
454 Path { commands: Vec<PathCmd> },
456
457 Text { content: String },
459}
460
461impl NodeKind {
462 pub fn kind_name(&self) -> &'static str {
464 match self {
465 Self::Root => "root",
466 Self::Generic => "generic",
467 Self::Group => "group",
468 Self::Frame { .. } => "frame",
469 Self::Rect { .. } => "rect",
470 Self::Ellipse { .. } => "ellipse",
471 Self::Path { .. } => "path",
472 Self::Text { .. } => "text",
473 }
474 }
475}
476
477#[derive(Debug, Clone, Serialize, Deserialize)]
479pub struct SceneNode {
480 pub id: NodeId,
482
483 pub kind: NodeKind,
485
486 pub style: Style,
488
489 pub use_styles: SmallVec<[NodeId; 2]>,
491
492 pub constraints: SmallVec<[Constraint; 2]>,
494
495 pub animations: SmallVec<[AnimKeyframe; 2]>,
497
498 pub annotations: Vec<Annotation>,
500
501 pub comments: Vec<String>,
504}
505
506impl SceneNode {
507 pub fn new(id: NodeId, kind: NodeKind) -> Self {
508 Self {
509 id,
510 kind,
511 style: Style::default(),
512 use_styles: SmallVec::new(),
513 constraints: SmallVec::new(),
514 animations: SmallVec::new(),
515 annotations: Vec::new(),
516 comments: Vec::new(),
517 }
518 }
519}
520
521#[derive(Debug, Clone)]
528pub struct SceneGraph {
529 pub graph: StableDiGraph<SceneNode, ()>,
531
532 pub root: NodeIndex,
534
535 pub styles: HashMap<NodeId, Style>,
537
538 pub id_index: HashMap<NodeId, NodeIndex>,
540
541 pub edges: Vec<Edge>,
543
544 pub imports: Vec<Import>,
546
547 pub sorted_child_order: HashMap<NodeIndex, Vec<NodeIndex>>,
551}
552
553impl SceneGraph {
554 #[must_use]
556 pub fn new() -> Self {
557 let mut graph = StableDiGraph::new();
558 let root_node = SceneNode::new(NodeId::intern("root"), NodeKind::Root);
559 let root = graph.add_node(root_node);
560
561 let mut id_index = HashMap::new();
562 id_index.insert(NodeId::intern("root"), root);
563
564 Self {
565 graph,
566 root,
567 styles: HashMap::new(),
568 id_index,
569 edges: Vec::new(),
570 imports: Vec::new(),
571 sorted_child_order: HashMap::new(),
572 }
573 }
574
575 pub fn add_node(&mut self, parent: NodeIndex, node: SceneNode) -> NodeIndex {
577 let id = node.id;
578 let idx = self.graph.add_node(node);
579 self.graph.add_edge(parent, idx, ());
580 self.id_index.insert(id, idx);
581 idx
582 }
583
584 pub fn remove_node(&mut self, idx: NodeIndex) -> Option<SceneNode> {
586 let removed = self.graph.remove_node(idx);
587 if let Some(removed_node) = &removed {
588 self.id_index.remove(&removed_node.id);
589 }
590 removed
591 }
592
593 pub fn get_by_id(&self, id: NodeId) -> Option<&SceneNode> {
595 self.id_index.get(&id).map(|idx| &self.graph[*idx])
596 }
597
598 pub fn get_by_id_mut(&mut self, id: NodeId) -> Option<&mut SceneNode> {
600 self.id_index
601 .get(&id)
602 .copied()
603 .map(|idx| &mut self.graph[idx])
604 }
605
606 pub fn index_of(&self, id: NodeId) -> Option<NodeIndex> {
608 self.id_index.get(&id).copied()
609 }
610
611 pub fn parent(&self, idx: NodeIndex) -> Option<NodeIndex> {
613 self.graph
614 .neighbors_directed(idx, petgraph::Direction::Incoming)
615 .next()
616 }
617
618 pub fn reparent_node(&mut self, child: NodeIndex, new_parent: NodeIndex) {
620 if let Some(old_parent) = self.parent(child)
621 && let Some(edge) = self.graph.find_edge(old_parent, child)
622 {
623 self.graph.remove_edge(edge);
624 }
625 self.graph.add_edge(new_parent, child, ());
626 }
627
628 pub fn children(&self, idx: NodeIndex) -> Vec<NodeIndex> {
634 if let Some(order) = self.sorted_child_order.get(&idx) {
636 return order.clone();
637 }
638
639 let mut children: Vec<NodeIndex> = self
640 .graph
641 .neighbors_directed(idx, petgraph::Direction::Outgoing)
642 .collect();
643 children.sort();
644 children
645 }
646
647 pub fn send_backward(&mut self, child: NodeIndex) -> bool {
650 let parent = match self.parent(child) {
651 Some(p) => p,
652 None => return false,
653 };
654 let siblings = self.children(parent);
655 let pos = match siblings.iter().position(|&s| s == child) {
656 Some(p) => p,
657 None => return false,
658 };
659 if pos == 0 {
660 return false; }
662 self.rebuild_child_order(parent, &siblings, pos, pos - 1)
664 }
665
666 pub fn bring_forward(&mut self, child: NodeIndex) -> bool {
669 let parent = match self.parent(child) {
670 Some(p) => p,
671 None => return false,
672 };
673 let siblings = self.children(parent);
674 let pos = match siblings.iter().position(|&s| s == child) {
675 Some(p) => p,
676 None => return false,
677 };
678 if pos >= siblings.len() - 1 {
679 return false; }
681 self.rebuild_child_order(parent, &siblings, pos, pos + 1)
682 }
683
684 pub fn send_to_back(&mut self, child: NodeIndex) -> bool {
686 let parent = match self.parent(child) {
687 Some(p) => p,
688 None => return false,
689 };
690 let siblings = self.children(parent);
691 let pos = match siblings.iter().position(|&s| s == child) {
692 Some(p) => p,
693 None => return false,
694 };
695 if pos == 0 {
696 return false;
697 }
698 self.rebuild_child_order(parent, &siblings, pos, 0)
699 }
700
701 pub fn bring_to_front(&mut self, child: NodeIndex) -> bool {
703 let parent = match self.parent(child) {
704 Some(p) => p,
705 None => return false,
706 };
707 let siblings = self.children(parent);
708 let pos = match siblings.iter().position(|&s| s == child) {
709 Some(p) => p,
710 None => return false,
711 };
712 let last = siblings.len() - 1;
713 if pos == last {
714 return false;
715 }
716 self.rebuild_child_order(parent, &siblings, pos, last)
717 }
718
719 fn rebuild_child_order(
721 &mut self,
722 parent: NodeIndex,
723 siblings: &[NodeIndex],
724 from: usize,
725 to: usize,
726 ) -> bool {
727 for &sib in siblings {
729 if let Some(edge) = self.graph.find_edge(parent, sib) {
730 self.graph.remove_edge(edge);
731 }
732 }
733 let mut new_order: Vec<NodeIndex> = siblings.to_vec();
735 let child = new_order.remove(from);
736 new_order.insert(to, child);
737 for &sib in &new_order {
739 self.graph.add_edge(parent, sib, ());
740 }
741 true
742 }
743
744 pub fn define_style(&mut self, name: NodeId, style: Style) {
746 self.styles.insert(name, style);
747 }
748
749 pub fn resolve_style(&self, node: &SceneNode, active_triggers: &[AnimTrigger]) -> Style {
751 let mut resolved = Style::default();
752
753 for style_id in &node.use_styles {
755 if let Some(base) = self.styles.get(style_id) {
756 merge_style(&mut resolved, base);
757 }
758 }
759
760 merge_style(&mut resolved, &node.style);
762
763 for anim in &node.animations {
765 if active_triggers.contains(&anim.trigger) {
766 if anim.properties.fill.is_some() {
767 resolved.fill = anim.properties.fill.clone();
768 }
769 if anim.properties.opacity.is_some() {
770 resolved.opacity = anim.properties.opacity;
771 }
772 if anim.properties.scale.is_some() {
773 resolved.scale = anim.properties.scale;
774 }
775 }
776 }
777
778 resolved
779 }
780
781 pub fn rebuild_index(&mut self) {
783 self.id_index.clear();
784 for idx in self.graph.node_indices() {
785 let id = self.graph[idx].id;
786 self.id_index.insert(id, idx);
787 }
788 }
789
790 pub fn resolve_style_for_edge(&self, edge: &Edge, active_triggers: &[AnimTrigger]) -> Style {
792 let mut resolved = Style::default();
793 for style_id in &edge.use_styles {
794 if let Some(base) = self.styles.get(style_id) {
795 merge_style(&mut resolved, base);
796 }
797 }
798 merge_style(&mut resolved, &edge.style);
799
800 for anim in &edge.animations {
801 if active_triggers.contains(&anim.trigger) {
802 if anim.properties.fill.is_some() {
803 resolved.fill = anim.properties.fill.clone();
804 }
805 if anim.properties.opacity.is_some() {
806 resolved.opacity = anim.properties.opacity;
807 }
808 if anim.properties.scale.is_some() {
809 resolved.scale = anim.properties.scale;
810 }
811 }
812 }
813
814 resolved
815 }
816
817 pub fn effective_target(&self, leaf_id: NodeId, selected: &[NodeId]) -> NodeId {
824 let leaf_idx = match self.index_of(leaf_id) {
825 Some(idx) => idx,
826 None => return leaf_id,
827 };
828
829 let mut groups_bottom_up: Vec<NodeId> = Vec::new();
832 let mut cursor = self.parent(leaf_idx);
833 while let Some(parent_idx) = cursor {
834 if parent_idx == self.root {
835 break;
836 }
837 if matches!(self.graph[parent_idx].kind, NodeKind::Group) {
838 groups_bottom_up.push(self.graph[parent_idx].id);
839 }
840 cursor = self.parent(parent_idx);
841 }
842
843 groups_bottom_up.reverse();
845
846 for group_id in &groups_bottom_up {
848 if !selected.contains(group_id) {
849 return *group_id;
850 }
851 }
852
853 leaf_id
855 }
856
857 pub fn is_ancestor_of(&self, ancestor_id: NodeId, descendant_id: NodeId) -> bool {
859 if ancestor_id == descendant_id {
860 return false;
861 }
862 let mut current_idx = match self.index_of(descendant_id) {
863 Some(idx) => idx,
864 None => return false,
865 };
866 while let Some(parent_idx) = self.parent(current_idx) {
867 if self.graph[parent_idx].id == ancestor_id {
868 return true;
869 }
870 if matches!(self.graph[parent_idx].kind, NodeKind::Root) {
871 break;
872 }
873 current_idx = parent_idx;
874 }
875 false
876 }
877}
878
879impl Default for SceneGraph {
880 fn default() -> Self {
881 Self::new()
882 }
883}
884
885fn merge_style(dst: &mut Style, src: &Style) {
887 if src.fill.is_some() {
888 dst.fill = src.fill.clone();
889 }
890 if src.stroke.is_some() {
891 dst.stroke = src.stroke.clone();
892 }
893 if src.font.is_some() {
894 dst.font = src.font.clone();
895 }
896 if src.corner_radius.is_some() {
897 dst.corner_radius = src.corner_radius;
898 }
899 if src.opacity.is_some() {
900 dst.opacity = src.opacity;
901 }
902 if src.shadow.is_some() {
903 dst.shadow = src.shadow.clone();
904 }
905
906 if src.text_align.is_some() {
907 dst.text_align = src.text_align;
908 }
909 if src.text_valign.is_some() {
910 dst.text_valign = src.text_valign;
911 }
912 if src.scale.is_some() {
913 dst.scale = src.scale;
914 }
915}
916
917#[derive(Debug, Clone, Copy, Default, PartialEq)]
921pub struct ResolvedBounds {
922 pub x: f32,
923 pub y: f32,
924 pub width: f32,
925 pub height: f32,
926}
927
928impl ResolvedBounds {
929 pub fn contains(&self, px: f32, py: f32) -> bool {
930 px >= self.x && px <= self.x + self.width && py >= self.y && py <= self.y + self.height
931 }
932
933 pub fn center(&self) -> (f32, f32) {
934 (self.x + self.width / 2.0, self.y + self.height / 2.0)
935 }
936
937 pub fn intersects_rect(&self, rx: f32, ry: f32, rw: f32, rh: f32) -> bool {
939 self.x < rx + rw
940 && self.x + self.width > rx
941 && self.y < ry + rh
942 && self.y + self.height > ry
943 }
944}
945
946#[cfg(test)]
947mod tests {
948 use super::*;
949
950 #[test]
951 fn scene_graph_basics() {
952 let mut sg = SceneGraph::new();
953 let rect = SceneNode::new(
954 NodeId::intern("box1"),
955 NodeKind::Rect {
956 width: 100.0,
957 height: 50.0,
958 },
959 );
960 let idx = sg.add_node(sg.root, rect);
961
962 assert!(sg.get_by_id(NodeId::intern("box1")).is_some());
963 assert_eq!(sg.children(sg.root).len(), 1);
964 assert_eq!(sg.children(sg.root)[0], idx);
965 }
966
967 #[test]
968 fn color_hex_roundtrip() {
969 let c = Color::from_hex("#6C5CE7").unwrap();
970 assert_eq!(c.to_hex(), "#6C5CE7");
971
972 let c2 = Color::from_hex("#FF000080").unwrap();
973 assert!((c2.a - 128.0 / 255.0).abs() < 0.01);
974 assert!(c2.to_hex().len() == 9); }
976
977 #[test]
978 fn style_merging() {
979 let mut sg = SceneGraph::new();
980 sg.define_style(
981 NodeId::intern("base"),
982 Style {
983 fill: Some(Paint::Solid(Color::rgba(0.0, 0.0, 0.0, 1.0))),
984 font: Some(FontSpec {
985 family: "Inter".into(),
986 weight: 400,
987 size: 14.0,
988 }),
989 ..Default::default()
990 },
991 );
992
993 let mut node = SceneNode::new(
994 NodeId::intern("txt"),
995 NodeKind::Text {
996 content: "hi".into(),
997 },
998 );
999 node.use_styles.push(NodeId::intern("base"));
1000 node.style.font = Some(FontSpec {
1001 family: "Inter".into(),
1002 weight: 700,
1003 size: 24.0,
1004 });
1005
1006 let resolved = sg.resolve_style(&node, &[]);
1007 assert!(resolved.fill.is_some());
1009 let f = resolved.font.unwrap();
1011 assert_eq!(f.weight, 700);
1012 assert_eq!(f.size, 24.0);
1013 }
1014
1015 #[test]
1016 fn style_merging_align() {
1017 let mut sg = SceneGraph::new();
1018 sg.define_style(
1019 NodeId::intern("centered"),
1020 Style {
1021 text_align: Some(TextAlign::Center),
1022 text_valign: Some(TextVAlign::Middle),
1023 ..Default::default()
1024 },
1025 );
1026
1027 let mut node = SceneNode::new(
1029 NodeId::intern("overridden"),
1030 NodeKind::Text {
1031 content: "hello".into(),
1032 },
1033 );
1034 node.use_styles.push(NodeId::intern("centered"));
1035 node.style.text_align = Some(TextAlign::Right);
1036
1037 let resolved = sg.resolve_style(&node, &[]);
1038 assert_eq!(resolved.text_align, Some(TextAlign::Right));
1040 assert_eq!(resolved.text_valign, Some(TextVAlign::Middle));
1042 }
1043
1044 #[test]
1045 fn test_effective_target_group_selects_group_first() {
1046 let mut sg = SceneGraph::new();
1047
1048 let group_id = NodeId::intern("my_group");
1050 let rect_id = NodeId::intern("my_rect");
1051
1052 let group = SceneNode::new(group_id, NodeKind::Group);
1053 let rect = SceneNode::new(
1054 rect_id,
1055 NodeKind::Rect {
1056 width: 10.0,
1057 height: 10.0,
1058 },
1059 );
1060
1061 let group_idx = sg.add_node(sg.root, group);
1062 sg.add_node(group_idx, rect);
1063
1064 assert_eq!(sg.effective_target(rect_id, &[]), group_id);
1066 assert_eq!(sg.effective_target(rect_id, &[group_id]), rect_id);
1068 assert_eq!(sg.effective_target(group_id, &[]), group_id);
1070 }
1071
1072 #[test]
1073 fn test_effective_target_nested_groups_selects_topmost() {
1074 let mut sg = SceneGraph::new();
1075
1076 let outer_id = NodeId::intern("group_outer");
1078 let inner_id = NodeId::intern("group_inner");
1079 let leaf_id = NodeId::intern("rect_leaf");
1080
1081 let outer = SceneNode::new(outer_id, NodeKind::Group);
1082 let inner = SceneNode::new(inner_id, NodeKind::Group);
1083 let leaf = SceneNode::new(
1084 leaf_id,
1085 NodeKind::Rect {
1086 width: 50.0,
1087 height: 50.0,
1088 },
1089 );
1090
1091 let outer_idx = sg.add_node(sg.root, outer);
1092 let inner_idx = sg.add_node(outer_idx, inner);
1093 sg.add_node(inner_idx, leaf);
1094
1095 assert_eq!(sg.effective_target(leaf_id, &[]), outer_id);
1097 assert_eq!(sg.effective_target(leaf_id, &[outer_id]), inner_id);
1099 assert_eq!(sg.effective_target(leaf_id, &[outer_id, inner_id]), leaf_id);
1101 }
1102
1103 #[test]
1104 fn test_effective_target_no_group() {
1105 let mut sg = SceneGraph::new();
1106
1107 let rect_id = NodeId::intern("standalone_rect");
1109 let rect = SceneNode::new(
1110 rect_id,
1111 NodeKind::Rect {
1112 width: 10.0,
1113 height: 10.0,
1114 },
1115 );
1116 sg.add_node(sg.root, rect);
1117
1118 assert_eq!(sg.effective_target(rect_id, &[]), rect_id);
1120 }
1121
1122 #[test]
1123 fn test_is_ancestor_of() {
1124 let mut sg = SceneGraph::new();
1125
1126 let group_id = NodeId::intern("grp");
1128 let rect_id = NodeId::intern("r1");
1129 let other_id = NodeId::intern("other");
1130
1131 let group = SceneNode::new(group_id, NodeKind::Group);
1132 let rect = SceneNode::new(
1133 rect_id,
1134 NodeKind::Rect {
1135 width: 10.0,
1136 height: 10.0,
1137 },
1138 );
1139 let other = SceneNode::new(
1140 other_id,
1141 NodeKind::Rect {
1142 width: 5.0,
1143 height: 5.0,
1144 },
1145 );
1146
1147 let group_idx = sg.add_node(sg.root, group);
1148 sg.add_node(group_idx, rect);
1149 sg.add_node(sg.root, other);
1150
1151 assert!(sg.is_ancestor_of(group_id, rect_id));
1153 assert!(sg.is_ancestor_of(NodeId::intern("root"), rect_id));
1155 assert!(!sg.is_ancestor_of(rect_id, group_id));
1157 assert!(!sg.is_ancestor_of(group_id, group_id));
1159 assert!(!sg.is_ancestor_of(other_id, rect_id));
1161 }
1162
1163 #[test]
1164 fn test_resolve_style_scale_animation() {
1165 let sg = SceneGraph::new();
1166
1167 let mut node = SceneNode::new(
1168 NodeId::intern("btn"),
1169 NodeKind::Rect {
1170 width: 100.0,
1171 height: 40.0,
1172 },
1173 );
1174 node.style.fill = Some(Paint::Solid(Color::rgba(1.0, 0.0, 0.0, 1.0)));
1175 node.animations.push(AnimKeyframe {
1176 trigger: AnimTrigger::Press,
1177 duration_ms: 100,
1178 easing: Easing::EaseOut,
1179 properties: AnimProperties {
1180 scale: Some(0.97),
1181 ..Default::default()
1182 },
1183 });
1184
1185 let resolved = sg.resolve_style(&node, &[]);
1187 assert!(resolved.scale.is_none());
1188
1189 let resolved = sg.resolve_style(&node, &[AnimTrigger::Press]);
1191 assert_eq!(resolved.scale, Some(0.97));
1192 assert!(resolved.fill.is_some());
1194 }
1195}