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, PartialEq, Serialize, Deserialize)]
115pub struct GradientStop {
116 pub offset: f32, pub color: Color,
118}
119
120#[derive(Debug, Clone, PartialEq, 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)]
203pub enum ImageSource {
204 File(String),
206}
207
208#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
210pub enum ImageFit {
211 #[default]
213 Cover,
214 Contain,
216 Fill,
218 None,
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct Shadow {
226 pub offset_x: f32,
227 pub offset_y: f32,
228 pub blur: f32,
229 pub color: Color,
230}
231
232#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
236pub enum TextAlign {
237 Left,
238 #[default]
239 Center,
240 Right,
241}
242
243#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
245pub enum TextVAlign {
246 Top,
247 #[default]
248 Middle,
249 Bottom,
250}
251
252#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
254pub enum HPlace {
255 Left,
256 #[default]
257 Center,
258 Right,
259}
260
261#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
263pub enum VPlace {
264 Top,
265 #[default]
266 Middle,
267 Bottom,
268}
269
270#[derive(Debug, Clone, Default, Serialize, Deserialize)]
272pub struct Properties {
273 pub fill: Option<Paint>,
274 pub stroke: Option<Stroke>,
275 pub font: Option<FontSpec>,
276 pub corner_radius: Option<f32>,
277 pub opacity: Option<f32>,
278 pub shadow: Option<Shadow>,
279
280 pub text_align: Option<TextAlign>,
282 pub text_valign: Option<TextVAlign>,
284
285 pub scale: Option<f32>,
287}
288
289#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
293pub enum AnimTrigger {
294 Hover,
295 Press,
296 Enter, Custom(String),
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize)]
302pub enum Easing {
303 Linear,
304 EaseIn,
305 EaseOut,
306 EaseInOut,
307 Spring,
308 CubicBezier(f32, f32, f32, f32),
309}
310
311#[derive(Debug, Clone, Serialize, Deserialize)]
313pub struct AnimKeyframe {
314 pub trigger: AnimTrigger,
315 pub duration_ms: u32,
316 pub easing: Easing,
317 pub properties: AnimProperties,
318}
319
320#[derive(Debug, Clone, Default, Serialize, Deserialize)]
322pub struct AnimProperties {
323 pub fill: Option<Paint>,
324 pub opacity: Option<f32>,
325 pub scale: Option<f32>,
326 pub rotate: Option<f32>, pub translate: Option<(f32, f32)>,
328}
329
330#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
335pub enum Annotation {
336 Description(String),
338 Accept(String),
340 Status(String),
342 Priority(String),
344 Tag(String),
346}
347
348#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
352pub struct Import {
353 pub path: String,
355 pub namespace: String,
357}
358
359#[derive(Debug, Clone, Serialize, Deserialize)]
363pub enum Constraint {
364 CenterIn(NodeId),
366 Offset { from: NodeId, dx: f32, dy: f32 },
368 FillParent { pad: f32 },
370 Position { x: f32, y: f32 },
373}
374
375#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
379pub enum ArrowKind {
380 #[default]
381 None,
382 Start,
383 End,
384 Both,
385}
386
387#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
389pub enum CurveKind {
390 #[default]
391 Straight,
392 Smooth,
393 Step,
394}
395
396#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
398pub enum EdgeAnchor {
399 Node(NodeId),
401 Point(f32, f32),
403}
404
405impl EdgeAnchor {
406 pub fn node_id(&self) -> Option<NodeId> {
408 match self {
409 Self::Node(id) => Some(*id),
410 Self::Point(_, _) => None,
411 }
412 }
413}
414
415#[derive(Debug, Clone, Default, Serialize, Deserialize)]
421pub struct EdgeDefaults {
422 pub props: Properties,
423 pub arrow: Option<ArrowKind>,
424 pub curve: Option<CurveKind>,
425}
426
427#[derive(Debug, Clone, Serialize, Deserialize)]
429pub struct Edge {
430 pub id: NodeId,
431 pub from: EdgeAnchor,
432 pub to: EdgeAnchor,
433 pub text_child: Option<NodeId>,
435 pub props: Properties,
436 pub use_styles: SmallVec<[NodeId; 2]>,
437 pub arrow: ArrowKind,
438 pub curve: CurveKind,
439 pub annotations: Vec<Annotation>,
440 pub animations: SmallVec<[AnimKeyframe; 2]>,
441 pub flow: Option<FlowAnim>,
442 pub label_offset: Option<(f32, f32)>,
444}
445
446#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
448pub enum FlowKind {
449 Pulse,
451 Dash,
453}
454
455#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
457pub struct FlowAnim {
458 pub kind: FlowKind,
459 pub duration_ms: u32,
460}
461
462#[derive(Debug, Clone, Serialize, Deserialize)]
464pub enum LayoutMode {
465 Free { pad: f32 },
468 Column { gap: f32, pad: f32 },
470 Row { gap: f32, pad: f32 },
472 Grid { cols: u32, gap: f32, pad: f32 },
474}
475
476impl Default for LayoutMode {
477 fn default() -> Self {
478 LayoutMode::Free { pad: 0.0 }
479 }
480}
481
482#[derive(Debug, Clone, Serialize, Deserialize)]
486pub enum NodeKind {
487 Root,
489
490 Generic,
493
494 Group,
497
498 Frame {
501 width: f32,
502 height: f32,
503 clip: bool,
504 layout: LayoutMode,
505 },
506
507 Rect { width: f32, height: f32 },
509
510 Ellipse { rx: f32, ry: f32 },
512
513 Path { commands: Vec<PathCmd> },
515
516 Image {
518 source: ImageSource,
519 width: f32,
520 height: f32,
521 fit: ImageFit,
522 },
523
524 Text {
527 content: String,
528 max_width: Option<f32>,
529 },
530}
531
532impl NodeKind {
533 pub fn kind_name(&self) -> &'static str {
535 match self {
536 Self::Root => "root",
537 Self::Generic => "generic",
538 Self::Group => "group",
539 Self::Frame { .. } => "frame",
540 Self::Rect { .. } => "rect",
541 Self::Ellipse { .. } => "ellipse",
542 Self::Path { .. } => "path",
543 Self::Image { .. } => "image",
544 Self::Text { .. } => "text",
545 }
546 }
547}
548
549#[derive(Debug, Clone, Serialize, Deserialize)]
551pub struct SceneNode {
552 pub id: NodeId,
554
555 pub kind: NodeKind,
557
558 pub props: Properties,
560
561 pub use_styles: SmallVec<[NodeId; 2]>,
563
564 pub constraints: SmallVec<[Constraint; 2]>,
566
567 pub animations: SmallVec<[AnimKeyframe; 2]>,
569
570 pub annotations: Vec<Annotation>,
572
573 pub comments: Vec<String>,
576
577 pub place: Option<(HPlace, VPlace)>,
580}
581
582impl SceneNode {
583 pub fn new(id: NodeId, kind: NodeKind) -> Self {
584 Self {
585 id,
586 kind,
587 props: Properties::default(),
588 use_styles: SmallVec::new(),
589 constraints: SmallVec::new(),
590 animations: SmallVec::new(),
591 annotations: Vec::new(),
592 comments: Vec::new(),
593 place: None,
594 }
595 }
596}
597
598#[derive(Debug, Clone, Default)]
605pub struct GraphSnapshot {
606 pub node_hashes: HashMap<NodeId, u64>,
608 pub edge_hashes: HashMap<NodeId, u64>,
610}
611
612#[derive(Debug, Clone)]
619pub struct SceneGraph {
620 pub graph: StableDiGraph<SceneNode, ()>,
622
623 pub root: NodeIndex,
625
626 pub styles: HashMap<NodeId, Properties>,
628
629 pub id_index: HashMap<NodeId, NodeIndex>,
631
632 pub edges: Vec<Edge>,
634
635 pub imports: Vec<Import>,
637
638 pub sorted_child_order: HashMap<NodeIndex, Vec<NodeIndex>>,
642
643 pub edge_defaults: Option<EdgeDefaults>,
646}
647
648impl SceneGraph {
649 #[must_use]
651 pub fn new() -> Self {
652 let mut graph = StableDiGraph::new();
653 let root_node = SceneNode::new(NodeId::intern("root"), NodeKind::Root);
654 let root = graph.add_node(root_node);
655
656 let mut id_index = HashMap::new();
657 id_index.insert(NodeId::intern("root"), root);
658
659 Self {
660 graph,
661 root,
662 styles: HashMap::new(),
663 id_index,
664 edges: Vec::new(),
665 imports: Vec::new(),
666 sorted_child_order: HashMap::new(),
667 edge_defaults: None,
668 }
669 }
670
671 pub fn add_node(&mut self, parent: NodeIndex, node: SceneNode) -> NodeIndex {
673 let id = node.id;
674 let idx = self.graph.add_node(node);
675 self.graph.add_edge(parent, idx, ());
676 self.id_index.insert(id, idx);
677 idx
678 }
679
680 pub fn remove_node(&mut self, idx: NodeIndex) -> Option<SceneNode> {
682 let removed = self.graph.remove_node(idx);
683 if let Some(removed_node) = &removed {
684 self.id_index.remove(&removed_node.id);
685 }
686 removed
687 }
688
689 pub fn get_by_id(&self, id: NodeId) -> Option<&SceneNode> {
691 self.id_index.get(&id).map(|idx| &self.graph[*idx])
692 }
693
694 pub fn get_by_id_mut(&mut self, id: NodeId) -> Option<&mut SceneNode> {
696 self.id_index
697 .get(&id)
698 .copied()
699 .map(|idx| &mut self.graph[idx])
700 }
701
702 pub fn index_of(&self, id: NodeId) -> Option<NodeIndex> {
704 self.id_index.get(&id).copied()
705 }
706
707 pub fn parent(&self, idx: NodeIndex) -> Option<NodeIndex> {
709 self.graph
710 .neighbors_directed(idx, petgraph::Direction::Incoming)
711 .next()
712 }
713
714 pub fn reparent_node(&mut self, child: NodeIndex, new_parent: NodeIndex) {
716 if let Some(old_parent) = self.parent(child)
717 && let Some(edge) = self.graph.find_edge(old_parent, child)
718 {
719 self.graph.remove_edge(edge);
720 }
721 self.graph.add_edge(new_parent, child, ());
722 }
723
724 pub fn children(&self, idx: NodeIndex) -> Vec<NodeIndex> {
730 if let Some(order) = self.sorted_child_order.get(&idx) {
732 return order.clone();
733 }
734
735 let mut children: Vec<NodeIndex> = self
736 .graph
737 .neighbors_directed(idx, petgraph::Direction::Outgoing)
738 .collect();
739 children.sort();
740 children
741 }
742
743 pub fn send_backward(&mut self, child: NodeIndex) -> bool {
746 let parent = match self.parent(child) {
747 Some(p) => p,
748 None => return false,
749 };
750 let siblings = self.children(parent);
751 let pos = match siblings.iter().position(|&s| s == child) {
752 Some(p) => p,
753 None => return false,
754 };
755 if pos == 0 {
756 return false; }
758 self.rebuild_child_order(parent, &siblings, pos, pos - 1)
760 }
761
762 pub fn bring_forward(&mut self, child: NodeIndex) -> bool {
765 let parent = match self.parent(child) {
766 Some(p) => p,
767 None => return false,
768 };
769 let siblings = self.children(parent);
770 let pos = match siblings.iter().position(|&s| s == child) {
771 Some(p) => p,
772 None => return false,
773 };
774 if pos >= siblings.len() - 1 {
775 return false; }
777 self.rebuild_child_order(parent, &siblings, pos, pos + 1)
778 }
779
780 pub fn send_to_back(&mut self, child: NodeIndex) -> bool {
782 let parent = match self.parent(child) {
783 Some(p) => p,
784 None => return false,
785 };
786 let siblings = self.children(parent);
787 let pos = match siblings.iter().position(|&s| s == child) {
788 Some(p) => p,
789 None => return false,
790 };
791 if pos == 0 {
792 return false;
793 }
794 self.rebuild_child_order(parent, &siblings, pos, 0)
795 }
796
797 pub fn bring_to_front(&mut self, child: NodeIndex) -> bool {
799 let parent = match self.parent(child) {
800 Some(p) => p,
801 None => return false,
802 };
803 let siblings = self.children(parent);
804 let pos = match siblings.iter().position(|&s| s == child) {
805 Some(p) => p,
806 None => return false,
807 };
808 let last = siblings.len() - 1;
809 if pos == last {
810 return false;
811 }
812 self.rebuild_child_order(parent, &siblings, pos, last)
813 }
814
815 fn rebuild_child_order(
817 &mut self,
818 parent: NodeIndex,
819 siblings: &[NodeIndex],
820 from: usize,
821 to: usize,
822 ) -> bool {
823 for &sib in siblings {
825 if let Some(edge) = self.graph.find_edge(parent, sib) {
826 self.graph.remove_edge(edge);
827 }
828 }
829 let mut new_order: Vec<NodeIndex> = siblings.to_vec();
831 let child = new_order.remove(from);
832 new_order.insert(to, child);
833 for &sib in &new_order {
835 self.graph.add_edge(parent, sib, ());
836 }
837 true
838 }
839
840 pub fn define_style(&mut self, name: NodeId, style: Properties) {
842 self.styles.insert(name, style);
843 }
844
845 pub fn resolve_style(&self, node: &SceneNode, active_triggers: &[AnimTrigger]) -> Properties {
847 let mut resolved = Properties::default();
848
849 for style_id in &node.use_styles {
851 if let Some(base) = self.styles.get(style_id) {
852 merge_style(&mut resolved, base);
853 }
854 }
855
856 merge_style(&mut resolved, &node.props);
858
859 for anim in &node.animations {
861 if active_triggers.contains(&anim.trigger) {
862 if anim.properties.fill.is_some() {
863 resolved.fill = anim.properties.fill.clone();
864 }
865 if anim.properties.opacity.is_some() {
866 resolved.opacity = anim.properties.opacity;
867 }
868 if anim.properties.scale.is_some() {
869 resolved.scale = anim.properties.scale;
870 }
871 }
872 }
873
874 resolved
875 }
876
877 pub fn rebuild_index(&mut self) {
879 self.id_index.clear();
880 for idx in self.graph.node_indices() {
881 let id = self.graph[idx].id;
882 self.id_index.insert(id, idx);
883 }
884 }
885
886 pub fn resolve_style_for_edge(
888 &self,
889 edge: &Edge,
890 active_triggers: &[AnimTrigger],
891 ) -> Properties {
892 let mut resolved = Properties::default();
893 for style_id in &edge.use_styles {
894 if let Some(base) = self.styles.get(style_id) {
895 merge_style(&mut resolved, base);
896 }
897 }
898 merge_style(&mut resolved, &edge.props);
899
900 for anim in &edge.animations {
901 if active_triggers.contains(&anim.trigger) {
902 if anim.properties.fill.is_some() {
903 resolved.fill = anim.properties.fill.clone();
904 }
905 if anim.properties.opacity.is_some() {
906 resolved.opacity = anim.properties.opacity;
907 }
908 if anim.properties.scale.is_some() {
909 resolved.scale = anim.properties.scale;
910 }
911 }
912 }
913
914 resolved
915 }
916
917 pub fn effective_target(&self, leaf_id: NodeId, selected: &[NodeId]) -> NodeId {
924 let leaf_idx = match self.index_of(leaf_id) {
925 Some(idx) => idx,
926 None => return leaf_id,
927 };
928
929 let mut groups_bottom_up: Vec<NodeId> = Vec::new();
932 let mut cursor = self.parent(leaf_idx);
933 while let Some(parent_idx) = cursor {
934 if parent_idx == self.root {
935 break;
936 }
937 if matches!(self.graph[parent_idx].kind, NodeKind::Group) {
938 groups_bottom_up.push(self.graph[parent_idx].id);
939 }
940 cursor = self.parent(parent_idx);
941 }
942
943 groups_bottom_up.reverse();
945
946 let deepest_selected_pos = groups_bottom_up
949 .iter()
950 .rposition(|gid| selected.contains(gid));
951
952 match deepest_selected_pos {
953 None => {
954 if let Some(top) = groups_bottom_up.first() {
956 return *top;
957 }
958 }
959 Some(pos) if pos + 1 < groups_bottom_up.len() => {
960 return groups_bottom_up[pos + 1];
962 }
963 Some(_) => {
964 }
966 }
967
968 leaf_id
969 }
970
971 pub fn is_ancestor_of(&self, ancestor_id: NodeId, descendant_id: NodeId) -> bool {
973 if ancestor_id == descendant_id {
974 return false;
975 }
976 let mut current_idx = match self.index_of(descendant_id) {
977 Some(idx) => idx,
978 None => return false,
979 };
980 while let Some(parent_idx) = self.parent(current_idx) {
981 if self.graph[parent_idx].id == ancestor_id {
982 return true;
983 }
984 if matches!(self.graph[parent_idx].kind, NodeKind::Root) {
985 break;
986 }
987 current_idx = parent_idx;
988 }
989 false
990 }
991}
992
993impl Default for SceneGraph {
994 fn default() -> Self {
995 Self::new()
996 }
997}
998
999fn merge_style(dst: &mut Properties, src: &Properties) {
1001 if src.fill.is_some() {
1002 dst.fill = src.fill.clone();
1003 }
1004 if src.stroke.is_some() {
1005 dst.stroke = src.stroke.clone();
1006 }
1007 if src.font.is_some() {
1008 dst.font = src.font.clone();
1009 }
1010 if src.corner_radius.is_some() {
1011 dst.corner_radius = src.corner_radius;
1012 }
1013 if src.opacity.is_some() {
1014 dst.opacity = src.opacity;
1015 }
1016 if src.shadow.is_some() {
1017 dst.shadow = src.shadow.clone();
1018 }
1019
1020 if src.text_align.is_some() {
1021 dst.text_align = src.text_align;
1022 }
1023 if src.text_valign.is_some() {
1024 dst.text_valign = src.text_valign;
1025 }
1026 if src.scale.is_some() {
1027 dst.scale = src.scale;
1028 }
1029}
1030
1031#[derive(Debug, Clone, Copy, Default, PartialEq)]
1035pub struct ResolvedBounds {
1036 pub x: f32,
1037 pub y: f32,
1038 pub width: f32,
1039 pub height: f32,
1040}
1041
1042impl ResolvedBounds {
1043 pub fn contains(&self, px: f32, py: f32) -> bool {
1044 px >= self.x && px <= self.x + self.width && py >= self.y && py <= self.y + self.height
1045 }
1046
1047 pub fn center(&self) -> (f32, f32) {
1048 (self.x + self.width / 2.0, self.y + self.height / 2.0)
1049 }
1050
1051 pub fn intersects_rect(&self, rx: f32, ry: f32, rw: f32, rh: f32) -> bool {
1053 self.x < rx + rw
1054 && self.x + self.width > rx
1055 && self.y < ry + rh
1056 && self.y + self.height > ry
1057 }
1058}
1059
1060#[cfg(test)]
1061mod tests {
1062 use super::*;
1063
1064 #[test]
1065 fn scene_graph_basics() {
1066 let mut sg = SceneGraph::new();
1067 let rect = SceneNode::new(
1068 NodeId::intern("box1"),
1069 NodeKind::Rect {
1070 width: 100.0,
1071 height: 50.0,
1072 },
1073 );
1074 let idx = sg.add_node(sg.root, rect);
1075
1076 assert!(sg.get_by_id(NodeId::intern("box1")).is_some());
1077 assert_eq!(sg.children(sg.root).len(), 1);
1078 assert_eq!(sg.children(sg.root)[0], idx);
1079 }
1080
1081 #[test]
1082 fn color_hex_roundtrip() {
1083 let c = Color::from_hex("#6C5CE7").unwrap();
1084 assert_eq!(c.to_hex(), "#6C5CE7");
1085
1086 let c2 = Color::from_hex("#FF000080").unwrap();
1087 assert!((c2.a - 128.0 / 255.0).abs() < 0.01);
1088 assert!(c2.to_hex().len() == 9); }
1090
1091 #[test]
1092 fn style_merging() {
1093 let mut sg = SceneGraph::new();
1094 sg.define_style(
1095 NodeId::intern("base"),
1096 Properties {
1097 fill: Some(Paint::Solid(Color::rgba(0.0, 0.0, 0.0, 1.0))),
1098 font: Some(FontSpec {
1099 family: "Inter".into(),
1100 weight: 400,
1101 size: 14.0,
1102 }),
1103 ..Default::default()
1104 },
1105 );
1106
1107 let mut node = SceneNode::new(
1108 NodeId::intern("txt"),
1109 NodeKind::Text {
1110 content: "hi".into(),
1111 max_width: None,
1112 },
1113 );
1114 node.use_styles.push(NodeId::intern("base"));
1115 node.props.font = Some(FontSpec {
1116 family: "Inter".into(),
1117 weight: 700,
1118 size: 24.0,
1119 });
1120
1121 let resolved = sg.resolve_style(&node, &[]);
1122 assert!(resolved.fill.is_some());
1124 let f = resolved.font.unwrap();
1126 assert_eq!(f.weight, 700);
1127 assert_eq!(f.size, 24.0);
1128 }
1129
1130 #[test]
1131 fn style_merging_align() {
1132 let mut sg = SceneGraph::new();
1133 sg.define_style(
1134 NodeId::intern("centered"),
1135 Properties {
1136 text_align: Some(TextAlign::Center),
1137 text_valign: Some(TextVAlign::Middle),
1138 ..Default::default()
1139 },
1140 );
1141
1142 let mut node = SceneNode::new(
1144 NodeId::intern("overridden"),
1145 NodeKind::Text {
1146 content: "hello".into(),
1147 max_width: None,
1148 },
1149 );
1150 node.use_styles.push(NodeId::intern("centered"));
1151 node.props.text_align = Some(TextAlign::Right);
1152
1153 let resolved = sg.resolve_style(&node, &[]);
1154 assert_eq!(resolved.text_align, Some(TextAlign::Right));
1156 assert_eq!(resolved.text_valign, Some(TextVAlign::Middle));
1158 }
1159
1160 #[test]
1161 fn test_effective_target_group_selects_group_first() {
1162 let mut sg = SceneGraph::new();
1163
1164 let group_id = NodeId::intern("my_group");
1166 let rect_id = NodeId::intern("my_rect");
1167
1168 let group = SceneNode::new(group_id, NodeKind::Group);
1169 let rect = SceneNode::new(
1170 rect_id,
1171 NodeKind::Rect {
1172 width: 10.0,
1173 height: 10.0,
1174 },
1175 );
1176
1177 let group_idx = sg.add_node(sg.root, group);
1178 sg.add_node(group_idx, rect);
1179
1180 assert_eq!(sg.effective_target(rect_id, &[]), group_id);
1182 assert_eq!(sg.effective_target(rect_id, &[group_id]), rect_id);
1184 assert_eq!(sg.effective_target(group_id, &[]), group_id);
1186 }
1187
1188 #[test]
1189 fn test_effective_target_nested_groups_selects_topmost() {
1190 let mut sg = SceneGraph::new();
1191
1192 let outer_id = NodeId::intern("group_outer");
1194 let inner_id = NodeId::intern("group_inner");
1195 let leaf_id = NodeId::intern("rect_leaf");
1196
1197 let outer = SceneNode::new(outer_id, NodeKind::Group);
1198 let inner = SceneNode::new(inner_id, NodeKind::Group);
1199 let leaf = SceneNode::new(
1200 leaf_id,
1201 NodeKind::Rect {
1202 width: 50.0,
1203 height: 50.0,
1204 },
1205 );
1206
1207 let outer_idx = sg.add_node(sg.root, outer);
1208 let inner_idx = sg.add_node(outer_idx, inner);
1209 sg.add_node(inner_idx, leaf);
1210
1211 assert_eq!(sg.effective_target(leaf_id, &[]), outer_id);
1213 assert_eq!(sg.effective_target(leaf_id, &[outer_id]), inner_id);
1215 assert_eq!(sg.effective_target(leaf_id, &[outer_id, inner_id]), leaf_id);
1217 assert_eq!(sg.effective_target(leaf_id, &[inner_id]), leaf_id);
1220 }
1221
1222 #[test]
1223 fn test_effective_target_nested_drill_down_three_levels() {
1224 let mut sg = SceneGraph::new();
1225
1226 let a_id = NodeId::intern("group_a");
1228 let b_id = NodeId::intern("group_b");
1229 let c_id = NodeId::intern("group_c");
1230 let leaf_id = NodeId::intern("deep_leaf");
1231
1232 let a = SceneNode::new(a_id, NodeKind::Group);
1233 let b = SceneNode::new(b_id, NodeKind::Group);
1234 let c = SceneNode::new(c_id, NodeKind::Group);
1235 let leaf = SceneNode::new(
1236 leaf_id,
1237 NodeKind::Rect {
1238 width: 10.0,
1239 height: 10.0,
1240 },
1241 );
1242
1243 let a_idx = sg.add_node(sg.root, a);
1244 let b_idx = sg.add_node(a_idx, b);
1245 let c_idx = sg.add_node(b_idx, c);
1246 sg.add_node(c_idx, leaf);
1247
1248 assert_eq!(sg.effective_target(leaf_id, &[]), a_id);
1250 assert_eq!(sg.effective_target(leaf_id, &[a_id]), b_id);
1251 assert_eq!(sg.effective_target(leaf_id, &[b_id]), c_id);
1252 assert_eq!(sg.effective_target(leaf_id, &[c_id]), leaf_id);
1253 }
1254
1255 #[test]
1256 fn test_visual_highlight_differs_from_selected() {
1257 let mut sg = SceneGraph::new();
1260
1261 let group_id = NodeId::intern("card");
1262 let child_id = NodeId::intern("card_title");
1263
1264 let group = SceneNode::new(group_id, NodeKind::Group);
1265 let child = SceneNode::new(
1266 child_id,
1267 NodeKind::Text {
1268 content: "Title".into(),
1269 max_width: None,
1270 },
1271 );
1272
1273 let group_idx = sg.add_node(sg.root, group);
1274 sg.add_node(group_idx, child);
1275
1276 let logical_target = sg.effective_target(child_id, &[]);
1278 assert_eq!(logical_target, group_id);
1280 assert_ne!(child_id, logical_target);
1282 let drilled = sg.effective_target(child_id, &[group_id]);
1284 assert_eq!(drilled, child_id);
1285 }
1286
1287 #[test]
1288 fn test_effective_target_no_group() {
1289 let mut sg = SceneGraph::new();
1290
1291 let rect_id = NodeId::intern("standalone_rect");
1293 let rect = SceneNode::new(
1294 rect_id,
1295 NodeKind::Rect {
1296 width: 10.0,
1297 height: 10.0,
1298 },
1299 );
1300 sg.add_node(sg.root, rect);
1301
1302 assert_eq!(sg.effective_target(rect_id, &[]), rect_id);
1304 }
1305
1306 #[test]
1307 fn test_is_ancestor_of() {
1308 let mut sg = SceneGraph::new();
1309
1310 let group_id = NodeId::intern("grp");
1312 let rect_id = NodeId::intern("r1");
1313 let other_id = NodeId::intern("other");
1314
1315 let group = SceneNode::new(group_id, NodeKind::Group);
1316 let rect = SceneNode::new(
1317 rect_id,
1318 NodeKind::Rect {
1319 width: 10.0,
1320 height: 10.0,
1321 },
1322 );
1323 let other = SceneNode::new(
1324 other_id,
1325 NodeKind::Rect {
1326 width: 5.0,
1327 height: 5.0,
1328 },
1329 );
1330
1331 let group_idx = sg.add_node(sg.root, group);
1332 sg.add_node(group_idx, rect);
1333 sg.add_node(sg.root, other);
1334
1335 assert!(sg.is_ancestor_of(group_id, rect_id));
1337 assert!(sg.is_ancestor_of(NodeId::intern("root"), rect_id));
1339 assert!(!sg.is_ancestor_of(rect_id, group_id));
1341 assert!(!sg.is_ancestor_of(group_id, group_id));
1343 assert!(!sg.is_ancestor_of(other_id, rect_id));
1345 }
1346
1347 #[test]
1348 fn test_resolve_style_scale_animation() {
1349 let sg = SceneGraph::new();
1350
1351 let mut node = SceneNode::new(
1352 NodeId::intern("btn"),
1353 NodeKind::Rect {
1354 width: 100.0,
1355 height: 40.0,
1356 },
1357 );
1358 node.props.fill = Some(Paint::Solid(Color::rgba(1.0, 0.0, 0.0, 1.0)));
1359 node.animations.push(AnimKeyframe {
1360 trigger: AnimTrigger::Press,
1361 duration_ms: 100,
1362 easing: Easing::EaseOut,
1363 properties: AnimProperties {
1364 scale: Some(0.97),
1365 ..Default::default()
1366 },
1367 });
1368
1369 let resolved = sg.resolve_style(&node, &[]);
1371 assert!(resolved.scale.is_none());
1372
1373 let resolved = sg.resolve_style(&node, &[AnimTrigger::Press]);
1375 assert_eq!(resolved.scale, Some(0.97));
1376 assert!(resolved.fill.is_some());
1378 }
1379}