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
27const HEX_LUT: [u8; 256] = {
31 let mut lut = [255; 256];
32 let mut i = 0;
33 while i < 10 {
34 lut[(b'0' + i) as usize] = i;
35 i += 1;
36 }
37 let mut i = 0;
38 while i < 6 {
39 lut[(b'a' + i) as usize] = i + 10;
40 lut[(b'A' + i) as usize] = i + 10;
41 i += 1;
42 }
43 lut
44};
45
46#[inline(always)]
48pub fn hex_val(c: u8) -> Option<u8> {
49 let val = HEX_LUT[c as usize];
50 if val != 255 { Some(val) } else { None }
51}
52
53impl Color {
54 pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
56 Self { r, g, b, a }
57 }
58
59 pub fn from_hex(hex: &str) -> Option<Self> {
62 let hex = hex.strip_prefix('#').unwrap_or(hex);
63 let bytes = hex.as_bytes();
64
65 match bytes.len() {
66 3 => {
67 let r = hex_val(bytes[0])?;
68 let g = hex_val(bytes[1])?;
69 let b = hex_val(bytes[2])?;
70 Some(Self::rgba(
71 (r * 17) as f32 / 255.0,
72 (g * 17) as f32 / 255.0,
73 (b * 17) as f32 / 255.0,
74 1.0,
75 ))
76 }
77 4 => {
78 let r = hex_val(bytes[0])?;
79 let g = hex_val(bytes[1])?;
80 let b = hex_val(bytes[2])?;
81 let a = hex_val(bytes[3])?;
82 Some(Self::rgba(
83 (r * 17) as f32 / 255.0,
84 (g * 17) as f32 / 255.0,
85 (b * 17) as f32 / 255.0,
86 (a * 17) as f32 / 255.0,
87 ))
88 }
89 6 => {
90 let r = hex_val(bytes[0])? << 4 | hex_val(bytes[1])?;
91 let g = hex_val(bytes[2])? << 4 | hex_val(bytes[3])?;
92 let b = hex_val(bytes[4])? << 4 | hex_val(bytes[5])?;
93 Some(Self::rgba(
94 r as f32 / 255.0,
95 g as f32 / 255.0,
96 b as f32 / 255.0,
97 1.0,
98 ))
99 }
100 8 => {
101 let r = hex_val(bytes[0])? << 4 | hex_val(bytes[1])?;
102 let g = hex_val(bytes[2])? << 4 | hex_val(bytes[3])?;
103 let b = hex_val(bytes[4])? << 4 | hex_val(bytes[5])?;
104 let a = hex_val(bytes[6])? << 4 | hex_val(bytes[7])?;
105 Some(Self::rgba(
106 r as f32 / 255.0,
107 g as f32 / 255.0,
108 b as f32 / 255.0,
109 a as f32 / 255.0,
110 ))
111 }
112 _ => None,
113 }
114 }
115
116 pub fn to_hex(&self) -> String {
118 let r = (self.r * 255.0).round() as u8;
119 let g = (self.g * 255.0).round() as u8;
120 let b = (self.b * 255.0).round() as u8;
121 let a = (self.a * 255.0).round() as u8;
122 if a == 255 {
123 format!("#{r:02X}{g:02X}{b:02X}")
124 } else {
125 format!("#{r:02X}{g:02X}{b:02X}{a:02X}")
126 }
127 }
128}
129
130#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
132pub struct GradientStop {
133 pub offset: f32, pub color: Color,
135}
136
137#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
139pub enum Paint {
140 Solid(Color),
141 LinearGradient {
142 angle: f32, stops: Vec<GradientStop>,
144 },
145 RadialGradient {
146 stops: Vec<GradientStop>,
147 },
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct Stroke {
154 pub paint: Paint,
155 pub width: f32,
156 pub cap: StrokeCap,
157 pub join: StrokeJoin,
158}
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
161pub enum StrokeCap {
162 Butt,
163 Round,
164 Square,
165}
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
168pub enum StrokeJoin {
169 Miter,
170 Round,
171 Bevel,
172}
173
174impl Default for Stroke {
175 fn default() -> Self {
176 Self {
177 paint: Paint::Solid(Color::rgba(0.0, 0.0, 0.0, 1.0)),
178 width: 1.0,
179 cap: StrokeCap::Butt,
180 join: StrokeJoin::Miter,
181 }
182 }
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct FontSpec {
189 pub family: String,
190 pub weight: u16, pub size: f32,
192}
193
194impl Default for FontSpec {
195 fn default() -> Self {
196 Self {
197 family: "Inter".into(),
198 weight: 400,
199 size: 14.0,
200 }
201 }
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
208pub enum PathCmd {
209 MoveTo(f32, f32),
210 LineTo(f32, f32),
211 QuadTo(f32, f32, f32, f32), CubicTo(f32, f32, f32, f32, f32, f32), Close,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize)]
220pub enum ImageSource {
221 File(String),
223}
224
225#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
227pub enum ImageFit {
228 #[default]
230 Cover,
231 Contain,
233 Fill,
235 None,
237}
238
239#[derive(Debug, Clone, Serialize, Deserialize)]
242pub struct Shadow {
243 pub offset_x: f32,
244 pub offset_y: f32,
245 pub blur: f32,
246 pub color: Color,
247}
248
249#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
253pub enum TextAlign {
254 Left,
255 #[default]
256 Center,
257 Right,
258}
259
260#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
262pub enum TextVAlign {
263 Top,
264 #[default]
265 Middle,
266 Bottom,
267}
268
269#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
271pub enum HPlace {
272 Left,
273 #[default]
274 Center,
275 Right,
276}
277
278#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
280pub enum VPlace {
281 Top,
282 #[default]
283 Middle,
284 Bottom,
285}
286
287#[derive(Debug, Clone, Default, Serialize, Deserialize)]
289pub struct Properties {
290 pub fill: Option<Paint>,
291 pub stroke: Option<Stroke>,
292 pub font: Option<FontSpec>,
293 pub corner_radius: Option<f32>,
294 pub opacity: Option<f32>,
295 pub shadow: Option<Shadow>,
296
297 pub text_align: Option<TextAlign>,
299 pub text_valign: Option<TextVAlign>,
301
302 pub scale: Option<f32>,
304}
305
306#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
310pub enum AnimTrigger {
311 Hover,
312 Press,
313 Enter, Custom(String),
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize)]
319pub enum Easing {
320 Linear,
321 EaseIn,
322 EaseOut,
323 EaseInOut,
324 Spring,
325 CubicBezier(f32, f32, f32, f32),
326}
327
328#[derive(Debug, Clone, Serialize, Deserialize)]
330pub struct AnimKeyframe {
331 pub trigger: AnimTrigger,
332 pub duration_ms: u32,
333 pub easing: Easing,
334 pub properties: AnimProperties,
335 pub delay_ms: Option<u32>,
338}
339
340#[derive(Debug, Clone, Default, Serialize, Deserialize)]
342pub struct AnimProperties {
343 pub fill: Option<Paint>,
344 pub opacity: Option<f32>,
345 pub scale: Option<f32>,
346 pub rotate: Option<f32>, pub translate: Option<(f32, f32)>,
348}
349
350#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
354pub struct Import {
355 pub path: String,
357 pub namespace: String,
359}
360
361#[derive(Debug, Clone, Serialize, Deserialize)]
365pub enum Constraint {
366 CenterIn(NodeId),
368 Offset { from: NodeId, dx: f32, dy: f32 },
370 FillParent { pad: f32 },
372 Position { x: f32, y: f32 },
375}
376
377#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
381pub enum ArrowKind {
382 #[default]
383 None,
384 Start,
385 End,
386 Both,
387}
388
389#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
391pub enum CurveKind {
392 #[default]
393 Straight,
394 Smooth,
395 Step,
396}
397
398#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
400pub enum EdgeAnchor {
401 Node(NodeId),
403 Point(f32, f32),
405}
406
407impl EdgeAnchor {
408 pub fn node_id(&self) -> Option<NodeId> {
410 match self {
411 Self::Node(id) => Some(*id),
412 Self::Point(_, _) => None,
413 }
414 }
415}
416
417#[derive(Debug, Clone, Default, Serialize, Deserialize)]
423pub struct EdgeDefaults {
424 pub props: Properties,
425 pub arrow: Option<ArrowKind>,
426 pub curve: Option<CurveKind>,
427}
428
429#[derive(Debug, Clone, Serialize, Deserialize)]
431pub struct Edge {
432 pub id: NodeId,
433 pub from: EdgeAnchor,
434 pub to: EdgeAnchor,
435 pub text_child: Option<NodeId>,
437 pub props: Properties,
438 pub use_styles: SmallVec<[NodeId; 2]>,
439 pub arrow: ArrowKind,
440 pub curve: CurveKind,
441 pub note: Option<String>,
442 pub animations: SmallVec<[AnimKeyframe; 2]>,
443 pub flow: Option<FlowAnim>,
444 pub label_offset: Option<(f32, f32)>,
446}
447
448#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
450pub enum FlowKind {
451 Pulse,
453 Dash,
455}
456
457#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
459pub struct FlowAnim {
460 pub kind: FlowKind,
461 pub duration_ms: u32,
462}
463
464#[derive(Debug, Clone, Serialize, Deserialize)]
466pub enum LayoutMode {
467 Free { pad: f32 },
470 Column { gap: f32, pad: f32 },
472 Row { gap: f32, pad: f32 },
474 Grid { cols: u32, gap: f32, pad: f32 },
476}
477
478impl Default for LayoutMode {
479 fn default() -> Self {
480 LayoutMode::Free { pad: 0.0 }
481 }
482}
483
484#[derive(Debug, Clone, Serialize, Deserialize)]
488pub enum NodeKind {
489 Root,
491
492 Generic,
495
496 Group,
499
500 Frame {
503 width: f32,
504 height: f32,
505 clip: bool,
506 layout: LayoutMode,
507 },
508
509 Rect { width: f32, height: f32 },
511
512 Ellipse { rx: f32, ry: f32 },
514
515 Path { commands: Vec<PathCmd> },
517
518 Image {
520 source: ImageSource,
521 width: f32,
522 height: f32,
523 fit: ImageFit,
524 },
525
526 Text {
529 content: String,
530 max_width: Option<f32>,
531 },
532}
533
534impl NodeKind {
535 pub fn kind_name(&self) -> &'static str {
537 match self {
538 Self::Root => "root",
539 Self::Generic => "generic",
540 Self::Group => "group",
541 Self::Frame { .. } => "frame",
542 Self::Rect { .. } => "rect",
543 Self::Ellipse { .. } => "ellipse",
544 Self::Path { .. } => "path",
545 Self::Image { .. } => "image",
546 Self::Text { .. } => "text",
547 }
548 }
549}
550
551#[derive(Debug, Clone, Serialize, Deserialize)]
553pub struct SceneNode {
554 pub id: NodeId,
556
557 pub kind: NodeKind,
559
560 pub props: Properties,
562
563 pub use_styles: SmallVec<[NodeId; 2]>,
565
566 pub constraints: SmallVec<[Constraint; 2]>,
568
569 pub animations: SmallVec<[AnimKeyframe; 2]>,
571
572 pub note: Option<String>,
574
575 pub comments: Vec<String>,
578
579 pub place: Option<(HPlace, VPlace)>,
582
583 pub locked: bool,
586}
587
588impl SceneNode {
589 pub fn new(id: NodeId, kind: NodeKind) -> Self {
591 Self {
592 id,
593 kind,
594 props: Properties::default(),
595 use_styles: SmallVec::new(),
596 constraints: SmallVec::new(),
597 animations: SmallVec::new(),
598 note: None,
599 comments: Vec::new(),
600 place: None,
601 locked: false,
602 }
603 }
604}
605
606#[derive(Debug, Clone, Default)]
613pub struct GraphSnapshot {
614 pub node_hashes: HashMap<NodeId, u64>,
616 pub edge_hashes: HashMap<NodeId, u64>,
618}
619
620#[derive(Debug, Clone)]
627pub struct SceneGraph {
628 pub graph: StableDiGraph<SceneNode, ()>,
630
631 pub root: NodeIndex,
633
634 pub styles: HashMap<NodeId, Properties>,
636
637 pub id_index: HashMap<NodeId, NodeIndex>,
639
640 pub edges: Vec<Edge>,
642
643 pub imports: Vec<Import>,
645
646 pub sorted_child_order: HashMap<NodeIndex, Vec<NodeIndex>>,
650
651 pub edge_defaults: Option<EdgeDefaults>,
654}
655
656impl SceneGraph {
657 #[must_use]
659 pub fn new() -> Self {
660 let mut graph = StableDiGraph::new();
661 let root_node = SceneNode::new(NodeId::intern("root"), NodeKind::Root);
662 let root = graph.add_node(root_node);
663
664 let mut id_index = HashMap::new();
665 id_index.insert(NodeId::intern("root"), root);
666
667 Self {
668 graph,
669 root,
670 styles: HashMap::new(),
671 id_index,
672 edges: Vec::new(),
673 imports: Vec::new(),
674 sorted_child_order: HashMap::new(),
675 edge_defaults: None,
676 }
677 }
678
679 pub fn add_node(&mut self, parent: NodeIndex, node: SceneNode) -> NodeIndex {
681 let id = node.id;
682 let idx = self.graph.add_node(node);
683 self.graph.add_edge(parent, idx, ());
684 self.id_index.insert(id, idx);
685 idx
686 }
687
688 pub fn remove_node(&mut self, idx: NodeIndex) -> Option<SceneNode> {
690 let removed = self.graph.remove_node(idx);
691 if let Some(removed_node) = &removed {
692 self.id_index.remove(&removed_node.id);
693 }
694 removed
695 }
696
697 pub fn get_by_id(&self, id: NodeId) -> Option<&SceneNode> {
699 self.id_index.get(&id).map(|idx| &self.graph[*idx])
700 }
701
702 pub fn get_by_id_mut(&mut self, id: NodeId) -> Option<&mut SceneNode> {
704 self.id_index
705 .get(&id)
706 .copied()
707 .map(|idx| &mut self.graph[idx])
708 }
709
710 pub fn index_of(&self, id: NodeId) -> Option<NodeIndex> {
712 self.id_index.get(&id).copied()
713 }
714
715 pub fn parent(&self, idx: NodeIndex) -> Option<NodeIndex> {
717 self.graph
718 .neighbors_directed(idx, petgraph::Direction::Incoming)
719 .next()
720 }
721
722 pub fn reparent_node(&mut self, child: NodeIndex, new_parent: NodeIndex) {
724 if let Some(old_parent) = self.parent(child)
725 && let Some(edge) = self.graph.find_edge(old_parent, child)
726 {
727 self.graph.remove_edge(edge);
728 }
729 self.graph.add_edge(new_parent, child, ());
730 }
731
732 pub fn children(&self, idx: NodeIndex) -> Vec<NodeIndex> {
738 if let Some(order) = self.sorted_child_order.get(&idx) {
740 return order.clone();
741 }
742
743 let mut children: Vec<NodeIndex> = self
744 .graph
745 .neighbors_directed(idx, petgraph::Direction::Outgoing)
746 .collect();
747 children.sort();
748 children
749 }
750
751 pub fn send_backward(&mut self, child: NodeIndex) -> bool {
754 let parent = match self.parent(child) {
755 Some(p) => p,
756 None => return false,
757 };
758 let siblings = self.children(parent);
759 let pos = match siblings.iter().position(|&s| s == child) {
760 Some(p) => p,
761 None => return false,
762 };
763 if pos == 0 {
764 return false; }
766 self.rebuild_child_order(parent, &siblings, pos, pos - 1)
768 }
769
770 pub fn bring_forward(&mut self, child: NodeIndex) -> bool {
773 let parent = match self.parent(child) {
774 Some(p) => p,
775 None => return false,
776 };
777 let siblings = self.children(parent);
778 let pos = match siblings.iter().position(|&s| s == child) {
779 Some(p) => p,
780 None => return false,
781 };
782 if pos >= siblings.len() - 1 {
783 return false; }
785 self.rebuild_child_order(parent, &siblings, pos, pos + 1)
786 }
787
788 pub fn send_to_back(&mut self, child: NodeIndex) -> bool {
790 let parent = match self.parent(child) {
791 Some(p) => p,
792 None => return false,
793 };
794 let siblings = self.children(parent);
795 let pos = match siblings.iter().position(|&s| s == child) {
796 Some(p) => p,
797 None => return false,
798 };
799 if pos == 0 {
800 return false;
801 }
802 self.rebuild_child_order(parent, &siblings, pos, 0)
803 }
804
805 pub fn bring_to_front(&mut self, child: NodeIndex) -> bool {
807 let parent = match self.parent(child) {
808 Some(p) => p,
809 None => return false,
810 };
811 let siblings = self.children(parent);
812 let pos = match siblings.iter().position(|&s| s == child) {
813 Some(p) => p,
814 None => return false,
815 };
816 let last = siblings.len() - 1;
817 if pos == last {
818 return false;
819 }
820 self.rebuild_child_order(parent, &siblings, pos, last)
821 }
822
823 fn rebuild_child_order(
825 &mut self,
826 parent: NodeIndex,
827 siblings: &[NodeIndex],
828 from: usize,
829 to: usize,
830 ) -> bool {
831 for &sib in siblings {
833 if let Some(edge) = self.graph.find_edge(parent, sib) {
834 self.graph.remove_edge(edge);
835 }
836 }
837 let mut new_order: Vec<NodeIndex> = siblings.to_vec();
839 let child = new_order.remove(from);
840 new_order.insert(to, child);
841 for &sib in &new_order {
843 self.graph.add_edge(parent, sib, ());
844 }
845 self.sorted_child_order.insert(parent, new_order);
847 true
848 }
849
850 pub fn define_style(&mut self, name: NodeId, style: Properties) {
852 self.styles.insert(name, style);
853 }
854
855 pub fn resolve_style(&self, node: &SceneNode, active_triggers: &[AnimTrigger]) -> Properties {
857 let mut resolved = Properties::default();
858
859 for style_id in &node.use_styles {
861 if let Some(base) = self.styles.get(style_id) {
862 merge_style(&mut resolved, base);
863 }
864 }
865
866 merge_style(&mut resolved, &node.props);
868
869 for anim in &node.animations {
871 if active_triggers.contains(&anim.trigger) {
872 if anim.properties.fill.is_some() {
873 resolved.fill = anim.properties.fill.clone();
874 }
875 if anim.properties.opacity.is_some() {
876 resolved.opacity = anim.properties.opacity;
877 }
878 if anim.properties.scale.is_some() {
879 resolved.scale = anim.properties.scale;
880 }
881 }
882 }
883
884 resolved
885 }
886
887 pub fn rebuild_index(&mut self) {
889 self.id_index.clear();
890 for idx in self.graph.node_indices() {
891 let id = self.graph[idx].id;
892 self.id_index.insert(id, idx);
893 }
894 }
895
896 pub fn resolve_style_for_edge(
898 &self,
899 edge: &Edge,
900 active_triggers: &[AnimTrigger],
901 ) -> Properties {
902 let mut resolved = Properties::default();
903 for style_id in &edge.use_styles {
904 if let Some(base) = self.styles.get(style_id) {
905 merge_style(&mut resolved, base);
906 }
907 }
908 merge_style(&mut resolved, &edge.props);
909
910 for anim in &edge.animations {
911 if active_triggers.contains(&anim.trigger) {
912 if anim.properties.fill.is_some() {
913 resolved.fill = anim.properties.fill.clone();
914 }
915 if anim.properties.opacity.is_some() {
916 resolved.opacity = anim.properties.opacity;
917 }
918 if anim.properties.scale.is_some() {
919 resolved.scale = anim.properties.scale;
920 }
921 }
922 }
923
924 resolved
925 }
926
927 pub fn effective_target(&self, leaf_id: NodeId, selected: &[NodeId]) -> NodeId {
934 let leaf_idx = match self.index_of(leaf_id) {
935 Some(idx) => idx,
936 None => return leaf_id,
937 };
938
939 let mut groups_bottom_up: Vec<NodeId> = Vec::new();
942 let mut cursor = self.parent(leaf_idx);
943 while let Some(parent_idx) = cursor {
944 if parent_idx == self.root {
945 break;
946 }
947 if matches!(self.graph[parent_idx].kind, NodeKind::Group) {
948 groups_bottom_up.push(self.graph[parent_idx].id);
949 }
950 cursor = self.parent(parent_idx);
951 }
952
953 groups_bottom_up.reverse();
955
956 let deepest_selected_pos = groups_bottom_up
959 .iter()
960 .rposition(|gid| selected.contains(gid));
961
962 match deepest_selected_pos {
963 None => {
964 if let Some(top) = groups_bottom_up.first() {
966 return *top;
967 }
968 }
969 Some(pos) if pos + 1 < groups_bottom_up.len() => {
970 return groups_bottom_up[pos + 1];
972 }
973 Some(_) => {
974 }
976 }
977
978 leaf_id
979 }
980
981 pub fn is_ancestor_of(&self, ancestor_id: NodeId, descendant_id: NodeId) -> bool {
983 if ancestor_id == descendant_id {
984 return false;
985 }
986 let mut current_idx = match self.index_of(descendant_id) {
987 Some(idx) => idx,
988 None => return false,
989 };
990 while let Some(parent_idx) = self.parent(current_idx) {
991 if self.graph[parent_idx].id == ancestor_id {
992 return true;
993 }
994 if matches!(self.graph[parent_idx].kind, NodeKind::Root) {
995 break;
996 }
997 current_idx = parent_idx;
998 }
999 false
1000 }
1001}
1002
1003impl Default for SceneGraph {
1004 fn default() -> Self {
1005 Self::new()
1006 }
1007}
1008
1009fn merge_style(dst: &mut Properties, src: &Properties) {
1011 if src.fill.is_some() {
1012 dst.fill = src.fill.clone();
1013 }
1014 if src.stroke.is_some() {
1015 dst.stroke = src.stroke.clone();
1016 }
1017 if src.font.is_some() {
1018 dst.font = src.font.clone();
1019 }
1020 if src.corner_radius.is_some() {
1021 dst.corner_radius = src.corner_radius;
1022 }
1023 if src.opacity.is_some() {
1024 dst.opacity = src.opacity;
1025 }
1026 if src.shadow.is_some() {
1027 dst.shadow = src.shadow.clone();
1028 }
1029
1030 if src.text_align.is_some() {
1031 dst.text_align = src.text_align;
1032 }
1033 if src.text_valign.is_some() {
1034 dst.text_valign = src.text_valign;
1035 }
1036 if src.scale.is_some() {
1037 dst.scale = src.scale;
1038 }
1039}
1040
1041#[derive(Debug, Clone, Copy, Default, PartialEq)]
1045pub struct ResolvedBounds {
1046 pub x: f32,
1047 pub y: f32,
1048 pub width: f32,
1049 pub height: f32,
1050}
1051
1052impl ResolvedBounds {
1053 pub fn contains(&self, px: f32, py: f32) -> bool {
1055 px >= self.x && px <= self.x + self.width && py >= self.y && py <= self.y + self.height
1056 }
1057
1058 pub fn center(&self) -> (f32, f32) {
1060 (self.x + self.width / 2.0, self.y + self.height / 2.0)
1061 }
1062
1063 pub fn intersects_rect(&self, rx: f32, ry: f32, rw: f32, rh: f32) -> bool {
1065 self.x < rx + rw
1066 && self.x + self.width > rx
1067 && self.y < ry + rh
1068 && self.y + self.height > ry
1069 }
1070}
1071
1072#[cfg(test)]
1073mod tests {
1074 use super::*;
1075
1076 #[test]
1077 fn scene_graph_basics() {
1078 let mut sg = SceneGraph::new();
1079 let rect = SceneNode::new(
1080 NodeId::intern("box1"),
1081 NodeKind::Rect {
1082 width: 100.0,
1083 height: 50.0,
1084 },
1085 );
1086 let idx = sg.add_node(sg.root, rect);
1087
1088 assert!(sg.get_by_id(NodeId::intern("box1")).is_some());
1089 assert_eq!(sg.children(sg.root).len(), 1);
1090 assert_eq!(sg.children(sg.root)[0], idx);
1091 }
1092
1093 #[test]
1094 fn color_hex_roundtrip() {
1095 let c = Color::from_hex("#6C5CE7").unwrap();
1096 assert_eq!(c.to_hex(), "#6C5CE7");
1097
1098 let c2 = Color::from_hex("#FF000080").unwrap();
1099 assert!((c2.a - 128.0 / 255.0).abs() < 0.01);
1100 assert!(c2.to_hex().len() == 9); }
1102
1103 #[test]
1104 fn style_merging() {
1105 let mut sg = SceneGraph::new();
1106 sg.define_style(
1107 NodeId::intern("base"),
1108 Properties {
1109 fill: Some(Paint::Solid(Color::rgba(0.0, 0.0, 0.0, 1.0))),
1110 font: Some(FontSpec {
1111 family: "Inter".into(),
1112 weight: 400,
1113 size: 14.0,
1114 }),
1115 ..Default::default()
1116 },
1117 );
1118
1119 let mut node = SceneNode::new(
1120 NodeId::intern("txt"),
1121 NodeKind::Text {
1122 content: "hi".into(),
1123 max_width: None,
1124 },
1125 );
1126 node.use_styles.push(NodeId::intern("base"));
1127 node.props.font = Some(FontSpec {
1128 family: "Inter".into(),
1129 weight: 700,
1130 size: 24.0,
1131 });
1132
1133 let resolved = sg.resolve_style(&node, &[]);
1134 assert!(resolved.fill.is_some());
1136 let f = resolved.font.unwrap();
1138 assert_eq!(f.weight, 700);
1139 assert_eq!(f.size, 24.0);
1140 }
1141
1142 #[test]
1143 fn style_merging_align() {
1144 let mut sg = SceneGraph::new();
1145 sg.define_style(
1146 NodeId::intern("centered"),
1147 Properties {
1148 text_align: Some(TextAlign::Center),
1149 text_valign: Some(TextVAlign::Middle),
1150 ..Default::default()
1151 },
1152 );
1153
1154 let mut node = SceneNode::new(
1156 NodeId::intern("overridden"),
1157 NodeKind::Text {
1158 content: "hello".into(),
1159 max_width: None,
1160 },
1161 );
1162 node.use_styles.push(NodeId::intern("centered"));
1163 node.props.text_align = Some(TextAlign::Right);
1164
1165 let resolved = sg.resolve_style(&node, &[]);
1166 assert_eq!(resolved.text_align, Some(TextAlign::Right));
1168 assert_eq!(resolved.text_valign, Some(TextVAlign::Middle));
1170 }
1171
1172 #[test]
1173 fn test_effective_target_group_selects_group_first() {
1174 let mut sg = SceneGraph::new();
1175
1176 let group_id = NodeId::intern("my_group");
1178 let rect_id = NodeId::intern("my_rect");
1179
1180 let group = SceneNode::new(group_id, NodeKind::Group);
1181 let rect = SceneNode::new(
1182 rect_id,
1183 NodeKind::Rect {
1184 width: 10.0,
1185 height: 10.0,
1186 },
1187 );
1188
1189 let group_idx = sg.add_node(sg.root, group);
1190 sg.add_node(group_idx, rect);
1191
1192 assert_eq!(sg.effective_target(rect_id, &[]), group_id);
1194 assert_eq!(sg.effective_target(rect_id, &[group_id]), rect_id);
1196 assert_eq!(sg.effective_target(group_id, &[]), group_id);
1198 }
1199
1200 #[test]
1201 fn test_effective_target_nested_groups_selects_topmost() {
1202 let mut sg = SceneGraph::new();
1203
1204 let outer_id = NodeId::intern("group_outer");
1206 let inner_id = NodeId::intern("group_inner");
1207 let leaf_id = NodeId::intern("rect_leaf");
1208
1209 let outer = SceneNode::new(outer_id, NodeKind::Group);
1210 let inner = SceneNode::new(inner_id, NodeKind::Group);
1211 let leaf = SceneNode::new(
1212 leaf_id,
1213 NodeKind::Rect {
1214 width: 50.0,
1215 height: 50.0,
1216 },
1217 );
1218
1219 let outer_idx = sg.add_node(sg.root, outer);
1220 let inner_idx = sg.add_node(outer_idx, inner);
1221 sg.add_node(inner_idx, leaf);
1222
1223 assert_eq!(sg.effective_target(leaf_id, &[]), outer_id);
1225 assert_eq!(sg.effective_target(leaf_id, &[outer_id]), inner_id);
1227 assert_eq!(sg.effective_target(leaf_id, &[outer_id, inner_id]), leaf_id);
1229 assert_eq!(sg.effective_target(leaf_id, &[inner_id]), leaf_id);
1232 }
1233
1234 #[test]
1235 fn test_effective_target_nested_drill_down_three_levels() {
1236 let mut sg = SceneGraph::new();
1237
1238 let a_id = NodeId::intern("group_a");
1240 let b_id = NodeId::intern("group_b");
1241 let c_id = NodeId::intern("group_c");
1242 let leaf_id = NodeId::intern("deep_leaf");
1243
1244 let a = SceneNode::new(a_id, NodeKind::Group);
1245 let b = SceneNode::new(b_id, NodeKind::Group);
1246 let c = SceneNode::new(c_id, NodeKind::Group);
1247 let leaf = SceneNode::new(
1248 leaf_id,
1249 NodeKind::Rect {
1250 width: 10.0,
1251 height: 10.0,
1252 },
1253 );
1254
1255 let a_idx = sg.add_node(sg.root, a);
1256 let b_idx = sg.add_node(a_idx, b);
1257 let c_idx = sg.add_node(b_idx, c);
1258 sg.add_node(c_idx, leaf);
1259
1260 assert_eq!(sg.effective_target(leaf_id, &[]), a_id);
1262 assert_eq!(sg.effective_target(leaf_id, &[a_id]), b_id);
1263 assert_eq!(sg.effective_target(leaf_id, &[b_id]), c_id);
1264 assert_eq!(sg.effective_target(leaf_id, &[c_id]), leaf_id);
1265 }
1266
1267 #[test]
1268 fn test_visual_highlight_differs_from_selected() {
1269 let mut sg = SceneGraph::new();
1272
1273 let group_id = NodeId::intern("card");
1274 let child_id = NodeId::intern("card_title");
1275
1276 let group = SceneNode::new(group_id, NodeKind::Group);
1277 let child = SceneNode::new(
1278 child_id,
1279 NodeKind::Text {
1280 content: "Title".into(),
1281 max_width: None,
1282 },
1283 );
1284
1285 let group_idx = sg.add_node(sg.root, group);
1286 sg.add_node(group_idx, child);
1287
1288 let logical_target = sg.effective_target(child_id, &[]);
1290 assert_eq!(logical_target, group_id);
1292 assert_ne!(child_id, logical_target);
1294 let drilled = sg.effective_target(child_id, &[group_id]);
1296 assert_eq!(drilled, child_id);
1297 }
1298
1299 #[test]
1300 fn test_effective_target_no_group() {
1301 let mut sg = SceneGraph::new();
1302
1303 let rect_id = NodeId::intern("standalone_rect");
1305 let rect = SceneNode::new(
1306 rect_id,
1307 NodeKind::Rect {
1308 width: 10.0,
1309 height: 10.0,
1310 },
1311 );
1312 sg.add_node(sg.root, rect);
1313
1314 assert_eq!(sg.effective_target(rect_id, &[]), rect_id);
1316 }
1317
1318 #[test]
1319 fn test_is_ancestor_of() {
1320 let mut sg = SceneGraph::new();
1321
1322 let group_id = NodeId::intern("grp");
1324 let rect_id = NodeId::intern("r1");
1325 let other_id = NodeId::intern("other");
1326
1327 let group = SceneNode::new(group_id, NodeKind::Group);
1328 let rect = SceneNode::new(
1329 rect_id,
1330 NodeKind::Rect {
1331 width: 10.0,
1332 height: 10.0,
1333 },
1334 );
1335 let other = SceneNode::new(
1336 other_id,
1337 NodeKind::Rect {
1338 width: 5.0,
1339 height: 5.0,
1340 },
1341 );
1342
1343 let group_idx = sg.add_node(sg.root, group);
1344 sg.add_node(group_idx, rect);
1345 sg.add_node(sg.root, other);
1346
1347 assert!(sg.is_ancestor_of(group_id, rect_id));
1349 assert!(sg.is_ancestor_of(NodeId::intern("root"), rect_id));
1351 assert!(!sg.is_ancestor_of(rect_id, group_id));
1353 assert!(!sg.is_ancestor_of(group_id, group_id));
1355 assert!(!sg.is_ancestor_of(other_id, rect_id));
1357 }
1358
1359 #[test]
1360 fn test_resolve_style_scale_animation() {
1361 let sg = SceneGraph::new();
1362
1363 let mut node = SceneNode::new(
1364 NodeId::intern("btn"),
1365 NodeKind::Rect {
1366 width: 100.0,
1367 height: 40.0,
1368 },
1369 );
1370 node.props.fill = Some(Paint::Solid(Color::rgba(1.0, 0.0, 0.0, 1.0)));
1371 node.animations.push(AnimKeyframe {
1372 trigger: AnimTrigger::Press,
1373 duration_ms: 100,
1374 easing: Easing::EaseOut,
1375 properties: AnimProperties {
1376 scale: Some(0.97),
1377 ..Default::default()
1378 },
1379 delay_ms: None,
1380 });
1381
1382 let resolved = sg.resolve_style(&node, &[]);
1384 assert!(resolved.scale.is_none());
1385
1386 let resolved = sg.resolve_style(&node, &[AnimTrigger::Press]);
1388 assert_eq!(resolved.scale, Some(0.97));
1389 assert!(resolved.fill.is_some());
1391 }
1392
1393 #[test]
1394 fn z_order_bring_forward() {
1395 let mut sg = SceneGraph::new();
1396 let a = sg.add_node(
1397 sg.root,
1398 SceneNode::new(
1399 NodeId::intern("a"),
1400 NodeKind::Rect {
1401 width: 50.0,
1402 height: 50.0,
1403 },
1404 ),
1405 );
1406 let _b = sg.add_node(
1407 sg.root,
1408 SceneNode::new(
1409 NodeId::intern("b"),
1410 NodeKind::Rect {
1411 width: 50.0,
1412 height: 50.0,
1413 },
1414 ),
1415 );
1416 let _c = sg.add_node(
1417 sg.root,
1418 SceneNode::new(
1419 NodeId::intern("c"),
1420 NodeKind::Rect {
1421 width: 50.0,
1422 height: 50.0,
1423 },
1424 ),
1425 );
1426
1427 let ids: Vec<&str> = sg
1429 .children(sg.root)
1430 .iter()
1431 .map(|&i| sg.graph[i].id.as_str())
1432 .collect();
1433 assert_eq!(ids, vec!["a", "b", "c"]);
1434
1435 let changed = sg.bring_forward(a);
1437 assert!(changed);
1438 let ids: Vec<&str> = sg
1439 .children(sg.root)
1440 .iter()
1441 .map(|&i| sg.graph[i].id.as_str())
1442 .collect();
1443 assert_eq!(ids, vec!["b", "a", "c"]);
1444 }
1445
1446 #[test]
1447 fn z_order_send_backward() {
1448 let mut sg = SceneGraph::new();
1449 let _a = sg.add_node(
1450 sg.root,
1451 SceneNode::new(
1452 NodeId::intern("a"),
1453 NodeKind::Rect {
1454 width: 50.0,
1455 height: 50.0,
1456 },
1457 ),
1458 );
1459 let _b = sg.add_node(
1460 sg.root,
1461 SceneNode::new(
1462 NodeId::intern("b"),
1463 NodeKind::Rect {
1464 width: 50.0,
1465 height: 50.0,
1466 },
1467 ),
1468 );
1469 let c = sg.add_node(
1470 sg.root,
1471 SceneNode::new(
1472 NodeId::intern("c"),
1473 NodeKind::Rect {
1474 width: 50.0,
1475 height: 50.0,
1476 },
1477 ),
1478 );
1479
1480 let changed = sg.send_backward(c);
1482 assert!(changed);
1483 let ids: Vec<&str> = sg
1484 .children(sg.root)
1485 .iter()
1486 .map(|&i| sg.graph[i].id.as_str())
1487 .collect();
1488 assert_eq!(ids, vec!["a", "c", "b"]);
1489 }
1490
1491 #[test]
1492 fn z_order_bring_to_front() {
1493 let mut sg = SceneGraph::new();
1494 let a = sg.add_node(
1495 sg.root,
1496 SceneNode::new(
1497 NodeId::intern("a"),
1498 NodeKind::Rect {
1499 width: 50.0,
1500 height: 50.0,
1501 },
1502 ),
1503 );
1504 let _b = sg.add_node(
1505 sg.root,
1506 SceneNode::new(
1507 NodeId::intern("b"),
1508 NodeKind::Rect {
1509 width: 50.0,
1510 height: 50.0,
1511 },
1512 ),
1513 );
1514 let _c = sg.add_node(
1515 sg.root,
1516 SceneNode::new(
1517 NodeId::intern("c"),
1518 NodeKind::Rect {
1519 width: 50.0,
1520 height: 50.0,
1521 },
1522 ),
1523 );
1524
1525 let changed = sg.bring_to_front(a);
1527 assert!(changed);
1528 let ids: Vec<&str> = sg
1529 .children(sg.root)
1530 .iter()
1531 .map(|&i| sg.graph[i].id.as_str())
1532 .collect();
1533 assert_eq!(ids, vec!["b", "c", "a"]);
1534 }
1535
1536 #[test]
1537 fn z_order_send_to_back() {
1538 let mut sg = SceneGraph::new();
1539 let _a = sg.add_node(
1540 sg.root,
1541 SceneNode::new(
1542 NodeId::intern("a"),
1543 NodeKind::Rect {
1544 width: 50.0,
1545 height: 50.0,
1546 },
1547 ),
1548 );
1549 let _b = sg.add_node(
1550 sg.root,
1551 SceneNode::new(
1552 NodeId::intern("b"),
1553 NodeKind::Rect {
1554 width: 50.0,
1555 height: 50.0,
1556 },
1557 ),
1558 );
1559 let c = sg.add_node(
1560 sg.root,
1561 SceneNode::new(
1562 NodeId::intern("c"),
1563 NodeKind::Rect {
1564 width: 50.0,
1565 height: 50.0,
1566 },
1567 ),
1568 );
1569
1570 let changed = sg.send_to_back(c);
1572 assert!(changed);
1573 let ids: Vec<&str> = sg
1574 .children(sg.root)
1575 .iter()
1576 .map(|&i| sg.graph[i].id.as_str())
1577 .collect();
1578 assert_eq!(ids, vec!["c", "a", "b"]);
1579 }
1580
1581 #[test]
1582 fn z_order_emitter_roundtrip() {
1583 use crate::emitter::emit_document;
1584 use crate::parser::parse_document;
1585
1586 let mut sg = SceneGraph::new();
1587 let a = sg.add_node(
1588 sg.root,
1589 SceneNode::new(
1590 NodeId::intern("a"),
1591 NodeKind::Rect {
1592 width: 50.0,
1593 height: 50.0,
1594 },
1595 ),
1596 );
1597 let _b = sg.add_node(
1598 sg.root,
1599 SceneNode::new(
1600 NodeId::intern("b"),
1601 NodeKind::Rect {
1602 width: 50.0,
1603 height: 50.0,
1604 },
1605 ),
1606 );
1607 let _c = sg.add_node(
1608 sg.root,
1609 SceneNode::new(
1610 NodeId::intern("c"),
1611 NodeKind::Rect {
1612 width: 50.0,
1613 height: 50.0,
1614 },
1615 ),
1616 );
1617
1618 sg.bring_to_front(a);
1620
1621 let text = emit_document(&sg);
1623 let reparsed = parse_document(&text).unwrap();
1624 let ids: Vec<&str> = reparsed
1625 .children(reparsed.root)
1626 .iter()
1627 .map(|&i| reparsed.graph[i].id.as_str())
1628 .collect();
1629 assert_eq!(
1630 ids,
1631 vec!["b", "c", "a"],
1632 "Z-order should survive emit→parse roundtrip. Emitted:\n{}",
1633 text
1634 );
1635 }
1636}