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, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
231pub enum HPlace {
232 Left,
233 #[default]
234 Center,
235 Right,
236}
237
238#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
240pub enum VPlace {
241 Top,
242 #[default]
243 Middle,
244 Bottom,
245}
246
247#[derive(Debug, Clone, Default, Serialize, Deserialize)]
249pub struct Style {
250 pub fill: Option<Paint>,
251 pub stroke: Option<Stroke>,
252 pub font: Option<FontSpec>,
253 pub corner_radius: Option<f32>,
254 pub opacity: Option<f32>,
255 pub shadow: Option<Shadow>,
256
257 pub text_align: Option<TextAlign>,
259 pub text_valign: Option<TextVAlign>,
261
262 pub scale: Option<f32>,
264}
265
266#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
270pub enum AnimTrigger {
271 Hover,
272 Press,
273 Enter, Custom(String),
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize)]
279pub enum Easing {
280 Linear,
281 EaseIn,
282 EaseOut,
283 EaseInOut,
284 Spring,
285 CubicBezier(f32, f32, f32, f32),
286}
287
288#[derive(Debug, Clone, Serialize, Deserialize)]
290pub struct AnimKeyframe {
291 pub trigger: AnimTrigger,
292 pub duration_ms: u32,
293 pub easing: Easing,
294 pub properties: AnimProperties,
295}
296
297#[derive(Debug, Clone, Default, Serialize, Deserialize)]
299pub struct AnimProperties {
300 pub fill: Option<Paint>,
301 pub opacity: Option<f32>,
302 pub scale: Option<f32>,
303 pub rotate: Option<f32>, pub translate: Option<(f32, f32)>,
305}
306
307#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
312pub enum Annotation {
313 Description(String),
315 Accept(String),
317 Status(String),
319 Priority(String),
321 Tag(String),
323}
324
325#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
329pub struct Import {
330 pub path: String,
332 pub namespace: String,
334}
335
336#[derive(Debug, Clone, Serialize, Deserialize)]
340pub enum Constraint {
341 CenterIn(NodeId),
343 Offset { from: NodeId, dx: f32, dy: f32 },
345 FillParent { pad: f32 },
347 Position { x: f32, y: f32 },
350}
351
352#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
356pub enum ArrowKind {
357 #[default]
358 None,
359 Start,
360 End,
361 Both,
362}
363
364#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
366pub enum CurveKind {
367 #[default]
368 Straight,
369 Smooth,
370 Step,
371}
372
373#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
375pub enum EdgeAnchor {
376 Node(NodeId),
378 Point(f32, f32),
380}
381
382impl EdgeAnchor {
383 pub fn node_id(&self) -> Option<NodeId> {
385 match self {
386 Self::Node(id) => Some(*id),
387 Self::Point(_, _) => None,
388 }
389 }
390}
391
392#[derive(Debug, Clone, Serialize, Deserialize)]
394pub struct Edge {
395 pub id: NodeId,
396 pub from: EdgeAnchor,
397 pub to: EdgeAnchor,
398 pub text_child: Option<NodeId>,
400 pub style: Style,
401 pub use_styles: SmallVec<[NodeId; 2]>,
402 pub arrow: ArrowKind,
403 pub curve: CurveKind,
404 pub annotations: Vec<Annotation>,
405 pub animations: SmallVec<[AnimKeyframe; 2]>,
406 pub flow: Option<FlowAnim>,
407 pub label_offset: Option<(f32, f32)>,
409}
410
411#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
413pub enum FlowKind {
414 Pulse,
416 Dash,
418}
419
420#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
422pub struct FlowAnim {
423 pub kind: FlowKind,
424 pub duration_ms: u32,
425}
426
427#[derive(Debug, Clone, Default, Serialize, Deserialize)]
429pub enum LayoutMode {
430 #[default]
432 Free,
433 Column { gap: f32, pad: f32 },
435 Row { gap: f32, pad: f32 },
437 Grid { cols: u32, gap: f32, pad: f32 },
439}
440
441#[derive(Debug, Clone, Serialize, Deserialize)]
445pub enum NodeKind {
446 Root,
448
449 Generic,
452
453 Group,
456
457 Frame {
460 width: f32,
461 height: f32,
462 clip: bool,
463 layout: LayoutMode,
464 },
465
466 Rect { width: f32, height: f32 },
468
469 Ellipse { rx: f32, ry: f32 },
471
472 Path { commands: Vec<PathCmd> },
474
475 Text {
478 content: String,
479 max_width: Option<f32>,
480 },
481}
482
483impl NodeKind {
484 pub fn kind_name(&self) -> &'static str {
486 match self {
487 Self::Root => "root",
488 Self::Generic => "generic",
489 Self::Group => "group",
490 Self::Frame { .. } => "frame",
491 Self::Rect { .. } => "rect",
492 Self::Ellipse { .. } => "ellipse",
493 Self::Path { .. } => "path",
494 Self::Text { .. } => "text",
495 }
496 }
497}
498
499#[derive(Debug, Clone, Serialize, Deserialize)]
501pub struct SceneNode {
502 pub id: NodeId,
504
505 pub kind: NodeKind,
507
508 pub style: Style,
510
511 pub use_styles: SmallVec<[NodeId; 2]>,
513
514 pub constraints: SmallVec<[Constraint; 2]>,
516
517 pub animations: SmallVec<[AnimKeyframe; 2]>,
519
520 pub annotations: Vec<Annotation>,
522
523 pub comments: Vec<String>,
526
527 pub place: Option<(HPlace, VPlace)>,
530}
531
532impl SceneNode {
533 pub fn new(id: NodeId, kind: NodeKind) -> Self {
534 Self {
535 id,
536 kind,
537 style: Style::default(),
538 use_styles: SmallVec::new(),
539 constraints: SmallVec::new(),
540 animations: SmallVec::new(),
541 annotations: Vec::new(),
542 comments: Vec::new(),
543 place: None,
544 }
545 }
546}
547
548#[derive(Debug, Clone)]
555pub struct SceneGraph {
556 pub graph: StableDiGraph<SceneNode, ()>,
558
559 pub root: NodeIndex,
561
562 pub styles: HashMap<NodeId, Style>,
564
565 pub id_index: HashMap<NodeId, NodeIndex>,
567
568 pub edges: Vec<Edge>,
570
571 pub imports: Vec<Import>,
573
574 pub sorted_child_order: HashMap<NodeIndex, Vec<NodeIndex>>,
578}
579
580impl SceneGraph {
581 #[must_use]
583 pub fn new() -> Self {
584 let mut graph = StableDiGraph::new();
585 let root_node = SceneNode::new(NodeId::intern("root"), NodeKind::Root);
586 let root = graph.add_node(root_node);
587
588 let mut id_index = HashMap::new();
589 id_index.insert(NodeId::intern("root"), root);
590
591 Self {
592 graph,
593 root,
594 styles: HashMap::new(),
595 id_index,
596 edges: Vec::new(),
597 imports: Vec::new(),
598 sorted_child_order: HashMap::new(),
599 }
600 }
601
602 pub fn add_node(&mut self, parent: NodeIndex, node: SceneNode) -> NodeIndex {
604 let id = node.id;
605 let idx = self.graph.add_node(node);
606 self.graph.add_edge(parent, idx, ());
607 self.id_index.insert(id, idx);
608 idx
609 }
610
611 pub fn remove_node(&mut self, idx: NodeIndex) -> Option<SceneNode> {
613 let removed = self.graph.remove_node(idx);
614 if let Some(removed_node) = &removed {
615 self.id_index.remove(&removed_node.id);
616 }
617 removed
618 }
619
620 pub fn get_by_id(&self, id: NodeId) -> Option<&SceneNode> {
622 self.id_index.get(&id).map(|idx| &self.graph[*idx])
623 }
624
625 pub fn get_by_id_mut(&mut self, id: NodeId) -> Option<&mut SceneNode> {
627 self.id_index
628 .get(&id)
629 .copied()
630 .map(|idx| &mut self.graph[idx])
631 }
632
633 pub fn index_of(&self, id: NodeId) -> Option<NodeIndex> {
635 self.id_index.get(&id).copied()
636 }
637
638 pub fn parent(&self, idx: NodeIndex) -> Option<NodeIndex> {
640 self.graph
641 .neighbors_directed(idx, petgraph::Direction::Incoming)
642 .next()
643 }
644
645 pub fn reparent_node(&mut self, child: NodeIndex, new_parent: NodeIndex) {
647 if let Some(old_parent) = self.parent(child)
648 && let Some(edge) = self.graph.find_edge(old_parent, child)
649 {
650 self.graph.remove_edge(edge);
651 }
652 self.graph.add_edge(new_parent, child, ());
653 }
654
655 pub fn children(&self, idx: NodeIndex) -> Vec<NodeIndex> {
661 if let Some(order) = self.sorted_child_order.get(&idx) {
663 return order.clone();
664 }
665
666 let mut children: Vec<NodeIndex> = self
667 .graph
668 .neighbors_directed(idx, petgraph::Direction::Outgoing)
669 .collect();
670 children.sort();
671 children
672 }
673
674 pub fn send_backward(&mut self, child: NodeIndex) -> bool {
677 let parent = match self.parent(child) {
678 Some(p) => p,
679 None => return false,
680 };
681 let siblings = self.children(parent);
682 let pos = match siblings.iter().position(|&s| s == child) {
683 Some(p) => p,
684 None => return false,
685 };
686 if pos == 0 {
687 return false; }
689 self.rebuild_child_order(parent, &siblings, pos, pos - 1)
691 }
692
693 pub fn bring_forward(&mut self, child: NodeIndex) -> bool {
696 let parent = match self.parent(child) {
697 Some(p) => p,
698 None => return false,
699 };
700 let siblings = self.children(parent);
701 let pos = match siblings.iter().position(|&s| s == child) {
702 Some(p) => p,
703 None => return false,
704 };
705 if pos >= siblings.len() - 1 {
706 return false; }
708 self.rebuild_child_order(parent, &siblings, pos, pos + 1)
709 }
710
711 pub fn send_to_back(&mut self, child: NodeIndex) -> bool {
713 let parent = match self.parent(child) {
714 Some(p) => p,
715 None => return false,
716 };
717 let siblings = self.children(parent);
718 let pos = match siblings.iter().position(|&s| s == child) {
719 Some(p) => p,
720 None => return false,
721 };
722 if pos == 0 {
723 return false;
724 }
725 self.rebuild_child_order(parent, &siblings, pos, 0)
726 }
727
728 pub fn bring_to_front(&mut self, child: NodeIndex) -> bool {
730 let parent = match self.parent(child) {
731 Some(p) => p,
732 None => return false,
733 };
734 let siblings = self.children(parent);
735 let pos = match siblings.iter().position(|&s| s == child) {
736 Some(p) => p,
737 None => return false,
738 };
739 let last = siblings.len() - 1;
740 if pos == last {
741 return false;
742 }
743 self.rebuild_child_order(parent, &siblings, pos, last)
744 }
745
746 fn rebuild_child_order(
748 &mut self,
749 parent: NodeIndex,
750 siblings: &[NodeIndex],
751 from: usize,
752 to: usize,
753 ) -> bool {
754 for &sib in siblings {
756 if let Some(edge) = self.graph.find_edge(parent, sib) {
757 self.graph.remove_edge(edge);
758 }
759 }
760 let mut new_order: Vec<NodeIndex> = siblings.to_vec();
762 let child = new_order.remove(from);
763 new_order.insert(to, child);
764 for &sib in &new_order {
766 self.graph.add_edge(parent, sib, ());
767 }
768 true
769 }
770
771 pub fn define_style(&mut self, name: NodeId, style: Style) {
773 self.styles.insert(name, style);
774 }
775
776 pub fn resolve_style(&self, node: &SceneNode, active_triggers: &[AnimTrigger]) -> Style {
778 let mut resolved = Style::default();
779
780 for style_id in &node.use_styles {
782 if let Some(base) = self.styles.get(style_id) {
783 merge_style(&mut resolved, base);
784 }
785 }
786
787 merge_style(&mut resolved, &node.style);
789
790 for anim in &node.animations {
792 if active_triggers.contains(&anim.trigger) {
793 if anim.properties.fill.is_some() {
794 resolved.fill = anim.properties.fill.clone();
795 }
796 if anim.properties.opacity.is_some() {
797 resolved.opacity = anim.properties.opacity;
798 }
799 if anim.properties.scale.is_some() {
800 resolved.scale = anim.properties.scale;
801 }
802 }
803 }
804
805 resolved
806 }
807
808 pub fn rebuild_index(&mut self) {
810 self.id_index.clear();
811 for idx in self.graph.node_indices() {
812 let id = self.graph[idx].id;
813 self.id_index.insert(id, idx);
814 }
815 }
816
817 pub fn resolve_style_for_edge(&self, edge: &Edge, active_triggers: &[AnimTrigger]) -> Style {
819 let mut resolved = Style::default();
820 for style_id in &edge.use_styles {
821 if let Some(base) = self.styles.get(style_id) {
822 merge_style(&mut resolved, base);
823 }
824 }
825 merge_style(&mut resolved, &edge.style);
826
827 for anim in &edge.animations {
828 if active_triggers.contains(&anim.trigger) {
829 if anim.properties.fill.is_some() {
830 resolved.fill = anim.properties.fill.clone();
831 }
832 if anim.properties.opacity.is_some() {
833 resolved.opacity = anim.properties.opacity;
834 }
835 if anim.properties.scale.is_some() {
836 resolved.scale = anim.properties.scale;
837 }
838 }
839 }
840
841 resolved
842 }
843
844 pub fn effective_target(&self, leaf_id: NodeId, selected: &[NodeId]) -> NodeId {
851 let leaf_idx = match self.index_of(leaf_id) {
852 Some(idx) => idx,
853 None => return leaf_id,
854 };
855
856 let mut groups_bottom_up: Vec<NodeId> = Vec::new();
859 let mut cursor = self.parent(leaf_idx);
860 while let Some(parent_idx) = cursor {
861 if parent_idx == self.root {
862 break;
863 }
864 if matches!(self.graph[parent_idx].kind, NodeKind::Group) {
865 groups_bottom_up.push(self.graph[parent_idx].id);
866 }
867 cursor = self.parent(parent_idx);
868 }
869
870 groups_bottom_up.reverse();
872
873 let deepest_selected_pos = groups_bottom_up
876 .iter()
877 .rposition(|gid| selected.contains(gid));
878
879 match deepest_selected_pos {
880 None => {
881 if let Some(top) = groups_bottom_up.first() {
883 return *top;
884 }
885 }
886 Some(pos) if pos + 1 < groups_bottom_up.len() => {
887 return groups_bottom_up[pos + 1];
889 }
890 Some(_) => {
891 }
893 }
894
895 leaf_id
896 }
897
898 pub fn is_ancestor_of(&self, ancestor_id: NodeId, descendant_id: NodeId) -> bool {
900 if ancestor_id == descendant_id {
901 return false;
902 }
903 let mut current_idx = match self.index_of(descendant_id) {
904 Some(idx) => idx,
905 None => return false,
906 };
907 while let Some(parent_idx) = self.parent(current_idx) {
908 if self.graph[parent_idx].id == ancestor_id {
909 return true;
910 }
911 if matches!(self.graph[parent_idx].kind, NodeKind::Root) {
912 break;
913 }
914 current_idx = parent_idx;
915 }
916 false
917 }
918}
919
920impl Default for SceneGraph {
921 fn default() -> Self {
922 Self::new()
923 }
924}
925
926fn merge_style(dst: &mut Style, src: &Style) {
928 if src.fill.is_some() {
929 dst.fill = src.fill.clone();
930 }
931 if src.stroke.is_some() {
932 dst.stroke = src.stroke.clone();
933 }
934 if src.font.is_some() {
935 dst.font = src.font.clone();
936 }
937 if src.corner_radius.is_some() {
938 dst.corner_radius = src.corner_radius;
939 }
940 if src.opacity.is_some() {
941 dst.opacity = src.opacity;
942 }
943 if src.shadow.is_some() {
944 dst.shadow = src.shadow.clone();
945 }
946
947 if src.text_align.is_some() {
948 dst.text_align = src.text_align;
949 }
950 if src.text_valign.is_some() {
951 dst.text_valign = src.text_valign;
952 }
953 if src.scale.is_some() {
954 dst.scale = src.scale;
955 }
956}
957
958#[derive(Debug, Clone, Copy, Default, PartialEq)]
962pub struct ResolvedBounds {
963 pub x: f32,
964 pub y: f32,
965 pub width: f32,
966 pub height: f32,
967}
968
969impl ResolvedBounds {
970 pub fn contains(&self, px: f32, py: f32) -> bool {
971 px >= self.x && px <= self.x + self.width && py >= self.y && py <= self.y + self.height
972 }
973
974 pub fn center(&self) -> (f32, f32) {
975 (self.x + self.width / 2.0, self.y + self.height / 2.0)
976 }
977
978 pub fn intersects_rect(&self, rx: f32, ry: f32, rw: f32, rh: f32) -> bool {
980 self.x < rx + rw
981 && self.x + self.width > rx
982 && self.y < ry + rh
983 && self.y + self.height > ry
984 }
985}
986
987#[cfg(test)]
988mod tests {
989 use super::*;
990
991 #[test]
992 fn scene_graph_basics() {
993 let mut sg = SceneGraph::new();
994 let rect = SceneNode::new(
995 NodeId::intern("box1"),
996 NodeKind::Rect {
997 width: 100.0,
998 height: 50.0,
999 },
1000 );
1001 let idx = sg.add_node(sg.root, rect);
1002
1003 assert!(sg.get_by_id(NodeId::intern("box1")).is_some());
1004 assert_eq!(sg.children(sg.root).len(), 1);
1005 assert_eq!(sg.children(sg.root)[0], idx);
1006 }
1007
1008 #[test]
1009 fn color_hex_roundtrip() {
1010 let c = Color::from_hex("#6C5CE7").unwrap();
1011 assert_eq!(c.to_hex(), "#6C5CE7");
1012
1013 let c2 = Color::from_hex("#FF000080").unwrap();
1014 assert!((c2.a - 128.0 / 255.0).abs() < 0.01);
1015 assert!(c2.to_hex().len() == 9); }
1017
1018 #[test]
1019 fn style_merging() {
1020 let mut sg = SceneGraph::new();
1021 sg.define_style(
1022 NodeId::intern("base"),
1023 Style {
1024 fill: Some(Paint::Solid(Color::rgba(0.0, 0.0, 0.0, 1.0))),
1025 font: Some(FontSpec {
1026 family: "Inter".into(),
1027 weight: 400,
1028 size: 14.0,
1029 }),
1030 ..Default::default()
1031 },
1032 );
1033
1034 let mut node = SceneNode::new(
1035 NodeId::intern("txt"),
1036 NodeKind::Text {
1037 content: "hi".into(),
1038 max_width: None,
1039 },
1040 );
1041 node.use_styles.push(NodeId::intern("base"));
1042 node.style.font = Some(FontSpec {
1043 family: "Inter".into(),
1044 weight: 700,
1045 size: 24.0,
1046 });
1047
1048 let resolved = sg.resolve_style(&node, &[]);
1049 assert!(resolved.fill.is_some());
1051 let f = resolved.font.unwrap();
1053 assert_eq!(f.weight, 700);
1054 assert_eq!(f.size, 24.0);
1055 }
1056
1057 #[test]
1058 fn style_merging_align() {
1059 let mut sg = SceneGraph::new();
1060 sg.define_style(
1061 NodeId::intern("centered"),
1062 Style {
1063 text_align: Some(TextAlign::Center),
1064 text_valign: Some(TextVAlign::Middle),
1065 ..Default::default()
1066 },
1067 );
1068
1069 let mut node = SceneNode::new(
1071 NodeId::intern("overridden"),
1072 NodeKind::Text {
1073 content: "hello".into(),
1074 max_width: None,
1075 },
1076 );
1077 node.use_styles.push(NodeId::intern("centered"));
1078 node.style.text_align = Some(TextAlign::Right);
1079
1080 let resolved = sg.resolve_style(&node, &[]);
1081 assert_eq!(resolved.text_align, Some(TextAlign::Right));
1083 assert_eq!(resolved.text_valign, Some(TextVAlign::Middle));
1085 }
1086
1087 #[test]
1088 fn test_effective_target_group_selects_group_first() {
1089 let mut sg = SceneGraph::new();
1090
1091 let group_id = NodeId::intern("my_group");
1093 let rect_id = NodeId::intern("my_rect");
1094
1095 let group = SceneNode::new(group_id, NodeKind::Group);
1096 let rect = SceneNode::new(
1097 rect_id,
1098 NodeKind::Rect {
1099 width: 10.0,
1100 height: 10.0,
1101 },
1102 );
1103
1104 let group_idx = sg.add_node(sg.root, group);
1105 sg.add_node(group_idx, rect);
1106
1107 assert_eq!(sg.effective_target(rect_id, &[]), group_id);
1109 assert_eq!(sg.effective_target(rect_id, &[group_id]), rect_id);
1111 assert_eq!(sg.effective_target(group_id, &[]), group_id);
1113 }
1114
1115 #[test]
1116 fn test_effective_target_nested_groups_selects_topmost() {
1117 let mut sg = SceneGraph::new();
1118
1119 let outer_id = NodeId::intern("group_outer");
1121 let inner_id = NodeId::intern("group_inner");
1122 let leaf_id = NodeId::intern("rect_leaf");
1123
1124 let outer = SceneNode::new(outer_id, NodeKind::Group);
1125 let inner = SceneNode::new(inner_id, NodeKind::Group);
1126 let leaf = SceneNode::new(
1127 leaf_id,
1128 NodeKind::Rect {
1129 width: 50.0,
1130 height: 50.0,
1131 },
1132 );
1133
1134 let outer_idx = sg.add_node(sg.root, outer);
1135 let inner_idx = sg.add_node(outer_idx, inner);
1136 sg.add_node(inner_idx, leaf);
1137
1138 assert_eq!(sg.effective_target(leaf_id, &[]), outer_id);
1140 assert_eq!(sg.effective_target(leaf_id, &[outer_id]), inner_id);
1142 assert_eq!(sg.effective_target(leaf_id, &[outer_id, inner_id]), leaf_id);
1144 assert_eq!(sg.effective_target(leaf_id, &[inner_id]), leaf_id);
1147 }
1148
1149 #[test]
1150 fn test_effective_target_nested_drill_down_three_levels() {
1151 let mut sg = SceneGraph::new();
1152
1153 let a_id = NodeId::intern("group_a");
1155 let b_id = NodeId::intern("group_b");
1156 let c_id = NodeId::intern("group_c");
1157 let leaf_id = NodeId::intern("deep_leaf");
1158
1159 let a = SceneNode::new(a_id, NodeKind::Group);
1160 let b = SceneNode::new(b_id, NodeKind::Group);
1161 let c = SceneNode::new(c_id, NodeKind::Group);
1162 let leaf = SceneNode::new(
1163 leaf_id,
1164 NodeKind::Rect {
1165 width: 10.0,
1166 height: 10.0,
1167 },
1168 );
1169
1170 let a_idx = sg.add_node(sg.root, a);
1171 let b_idx = sg.add_node(a_idx, b);
1172 let c_idx = sg.add_node(b_idx, c);
1173 sg.add_node(c_idx, leaf);
1174
1175 assert_eq!(sg.effective_target(leaf_id, &[]), a_id);
1177 assert_eq!(sg.effective_target(leaf_id, &[a_id]), b_id);
1178 assert_eq!(sg.effective_target(leaf_id, &[b_id]), c_id);
1179 assert_eq!(sg.effective_target(leaf_id, &[c_id]), leaf_id);
1180 }
1181
1182 #[test]
1183 fn test_visual_highlight_differs_from_selected() {
1184 let mut sg = SceneGraph::new();
1187
1188 let group_id = NodeId::intern("card");
1189 let child_id = NodeId::intern("card_title");
1190
1191 let group = SceneNode::new(group_id, NodeKind::Group);
1192 let child = SceneNode::new(
1193 child_id,
1194 NodeKind::Text {
1195 content: "Title".into(),
1196 max_width: None,
1197 },
1198 );
1199
1200 let group_idx = sg.add_node(sg.root, group);
1201 sg.add_node(group_idx, child);
1202
1203 let logical_target = sg.effective_target(child_id, &[]);
1205 assert_eq!(logical_target, group_id);
1207 assert_ne!(child_id, logical_target);
1209 let drilled = sg.effective_target(child_id, &[group_id]);
1211 assert_eq!(drilled, child_id);
1212 }
1213
1214 #[test]
1215 fn test_effective_target_no_group() {
1216 let mut sg = SceneGraph::new();
1217
1218 let rect_id = NodeId::intern("standalone_rect");
1220 let rect = SceneNode::new(
1221 rect_id,
1222 NodeKind::Rect {
1223 width: 10.0,
1224 height: 10.0,
1225 },
1226 );
1227 sg.add_node(sg.root, rect);
1228
1229 assert_eq!(sg.effective_target(rect_id, &[]), rect_id);
1231 }
1232
1233 #[test]
1234 fn test_is_ancestor_of() {
1235 let mut sg = SceneGraph::new();
1236
1237 let group_id = NodeId::intern("grp");
1239 let rect_id = NodeId::intern("r1");
1240 let other_id = NodeId::intern("other");
1241
1242 let group = SceneNode::new(group_id, NodeKind::Group);
1243 let rect = SceneNode::new(
1244 rect_id,
1245 NodeKind::Rect {
1246 width: 10.0,
1247 height: 10.0,
1248 },
1249 );
1250 let other = SceneNode::new(
1251 other_id,
1252 NodeKind::Rect {
1253 width: 5.0,
1254 height: 5.0,
1255 },
1256 );
1257
1258 let group_idx = sg.add_node(sg.root, group);
1259 sg.add_node(group_idx, rect);
1260 sg.add_node(sg.root, other);
1261
1262 assert!(sg.is_ancestor_of(group_id, rect_id));
1264 assert!(sg.is_ancestor_of(NodeId::intern("root"), rect_id));
1266 assert!(!sg.is_ancestor_of(rect_id, group_id));
1268 assert!(!sg.is_ancestor_of(group_id, group_id));
1270 assert!(!sg.is_ancestor_of(other_id, rect_id));
1272 }
1273
1274 #[test]
1275 fn test_resolve_style_scale_animation() {
1276 let sg = SceneGraph::new();
1277
1278 let mut node = SceneNode::new(
1279 NodeId::intern("btn"),
1280 NodeKind::Rect {
1281 width: 100.0,
1282 height: 40.0,
1283 },
1284 );
1285 node.style.fill = Some(Paint::Solid(Color::rgba(1.0, 0.0, 0.0, 1.0)));
1286 node.animations.push(AnimKeyframe {
1287 trigger: AnimTrigger::Press,
1288 duration_ms: 100,
1289 easing: Easing::EaseOut,
1290 properties: AnimProperties {
1291 scale: Some(0.97),
1292 ..Default::default()
1293 },
1294 });
1295
1296 let resolved = sg.resolve_style(&node, &[]);
1298 assert!(resolved.scale.is_none());
1299
1300 let resolved = sg.resolve_style(&node, &[AnimTrigger::Press]);
1302 assert_eq!(resolved.scale, Some(0.97));
1303 assert!(resolved.fill.is_some());
1305 }
1306}