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 spec: 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 spec: 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 spec: 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 if let Some(order) = self.sorted_child_order.get_mut(&old_parent) {
731 order.retain(|&idx| idx != child);
732 }
733 }
734 self.graph.add_edge(new_parent, child, ());
735 }
736
737 pub fn children(&self, idx: NodeIndex) -> Vec<NodeIndex> {
743 if let Some(order) = self.sorted_child_order.get(&idx) {
747 return order
748 .iter()
749 .copied()
750 .filter(|&c| self.graph.find_edge(idx, c).is_some())
751 .collect();
752 }
753
754 let mut children: Vec<NodeIndex> = self
755 .graph
756 .neighbors_directed(idx, petgraph::Direction::Outgoing)
757 .collect();
758 children.sort();
759 children
760 }
761
762 pub fn send_backward(&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 == 0 {
775 return false; }
777 self.rebuild_child_order(parent, &siblings, pos, pos - 1)
779 }
780
781 pub fn bring_forward(&mut self, child: NodeIndex) -> bool {
784 let parent = match self.parent(child) {
785 Some(p) => p,
786 None => return false,
787 };
788 let siblings = self.children(parent);
789 let pos = match siblings.iter().position(|&s| s == child) {
790 Some(p) => p,
791 None => return false,
792 };
793 if pos >= siblings.len() - 1 {
794 return false; }
796 self.rebuild_child_order(parent, &siblings, pos, pos + 1)
797 }
798
799 pub fn send_to_back(&mut self, child: NodeIndex) -> bool {
801 let parent = match self.parent(child) {
802 Some(p) => p,
803 None => return false,
804 };
805 let siblings = self.children(parent);
806 let pos = match siblings.iter().position(|&s| s == child) {
807 Some(p) => p,
808 None => return false,
809 };
810 if pos == 0 {
811 return false;
812 }
813 self.rebuild_child_order(parent, &siblings, pos, 0)
814 }
815
816 pub fn bring_to_front(&mut self, child: NodeIndex) -> bool {
818 let parent = match self.parent(child) {
819 Some(p) => p,
820 None => return false,
821 };
822 let siblings = self.children(parent);
823 let pos = match siblings.iter().position(|&s| s == child) {
824 Some(p) => p,
825 None => return false,
826 };
827 let last = siblings.len() - 1;
828 if pos == last {
829 return false;
830 }
831 self.rebuild_child_order(parent, &siblings, pos, last)
832 }
833
834 pub fn move_child_to_index(&mut self, child: NodeIndex, target_index: usize) -> bool {
838 let parent = match self.parent(child) {
839 Some(p) => p,
840 None => return false,
841 };
842 let siblings = self.children(parent);
843 let from = match siblings.iter().position(|&s| s == child) {
844 Some(p) => p,
845 None => return false,
846 };
847 let to = target_index.min(siblings.len().saturating_sub(1));
848 if from == to {
849 return false;
850 }
851 self.rebuild_child_order(parent, &siblings, from, to)
852 }
853
854 fn rebuild_child_order(
856 &mut self,
857 parent: NodeIndex,
858 siblings: &[NodeIndex],
859 from: usize,
860 to: usize,
861 ) -> bool {
862 for &sib in siblings {
864 if let Some(edge) = self.graph.find_edge(parent, sib) {
865 self.graph.remove_edge(edge);
866 }
867 }
868 let mut new_order: Vec<NodeIndex> = siblings.to_vec();
870 let child = new_order.remove(from);
871 new_order.insert(to, child);
872 for &sib in &new_order {
874 self.graph.add_edge(parent, sib, ());
875 }
876 self.sorted_child_order.insert(parent, new_order);
878 true
879 }
880
881 pub fn define_style(&mut self, name: NodeId, style: Properties) {
883 self.styles.insert(name, style);
884 }
885
886 pub fn resolve_style(&self, node: &SceneNode, active_triggers: &[AnimTrigger]) -> Properties {
888 let mut resolved = Properties::default();
889
890 for style_id in &node.use_styles {
892 if let Some(base) = self.styles.get(style_id) {
893 merge_style(&mut resolved, base);
894 }
895 }
896
897 merge_style(&mut resolved, &node.props);
899
900 for anim in &node.animations {
902 if active_triggers.contains(&anim.trigger) {
903 if anim.properties.fill.is_some() {
904 resolved.fill = anim.properties.fill.clone();
905 }
906 if anim.properties.opacity.is_some() {
907 resolved.opacity = anim.properties.opacity;
908 }
909 if anim.properties.scale.is_some() {
910 resolved.scale = anim.properties.scale;
911 }
912 }
913 }
914
915 resolved
916 }
917
918 pub fn rebuild_index(&mut self) {
920 self.id_index.clear();
921 for idx in self.graph.node_indices() {
922 let id = self.graph[idx].id;
923 self.id_index.insert(id, idx);
924 }
925 }
926
927 pub fn resolve_style_for_edge(
929 &self,
930 edge: &Edge,
931 active_triggers: &[AnimTrigger],
932 ) -> Properties {
933 let mut resolved = Properties::default();
934 for style_id in &edge.use_styles {
935 if let Some(base) = self.styles.get(style_id) {
936 merge_style(&mut resolved, base);
937 }
938 }
939 merge_style(&mut resolved, &edge.props);
940
941 for anim in &edge.animations {
942 if active_triggers.contains(&anim.trigger) {
943 if anim.properties.fill.is_some() {
944 resolved.fill = anim.properties.fill.clone();
945 }
946 if anim.properties.opacity.is_some() {
947 resolved.opacity = anim.properties.opacity;
948 }
949 if anim.properties.scale.is_some() {
950 resolved.scale = anim.properties.scale;
951 }
952 }
953 }
954
955 resolved
956 }
957
958 pub fn effective_target(&self, leaf_id: NodeId, selected: &[NodeId]) -> NodeId {
965 let leaf_idx = match self.index_of(leaf_id) {
966 Some(idx) => idx,
967 None => return leaf_id,
968 };
969
970 let mut groups_bottom_up: Vec<NodeId> = Vec::new();
973 let mut cursor = self.parent(leaf_idx);
974 while let Some(parent_idx) = cursor {
975 if parent_idx == self.root {
976 break;
977 }
978 if matches!(self.graph[parent_idx].kind, NodeKind::Group) {
979 groups_bottom_up.push(self.graph[parent_idx].id);
980 }
981 cursor = self.parent(parent_idx);
982 }
983
984 groups_bottom_up.reverse();
986
987 let deepest_selected_pos = groups_bottom_up
990 .iter()
991 .rposition(|gid| selected.contains(gid));
992
993 match deepest_selected_pos {
994 None => {
995 if let Some(top) = groups_bottom_up.first() {
997 return *top;
998 }
999 }
1000 Some(pos) if pos + 1 < groups_bottom_up.len() => {
1001 return groups_bottom_up[pos + 1];
1003 }
1004 Some(_) => {
1005 }
1007 }
1008
1009 leaf_id
1010 }
1011
1012 pub fn is_ancestor_of(&self, ancestor_id: NodeId, descendant_id: NodeId) -> bool {
1014 if ancestor_id == descendant_id {
1015 return false;
1016 }
1017 let mut current_idx = match self.index_of(descendant_id) {
1018 Some(idx) => idx,
1019 None => return false,
1020 };
1021 while let Some(parent_idx) = self.parent(current_idx) {
1022 if self.graph[parent_idx].id == ancestor_id {
1023 return true;
1024 }
1025 if matches!(self.graph[parent_idx].kind, NodeKind::Root) {
1026 break;
1027 }
1028 current_idx = parent_idx;
1029 }
1030 false
1031 }
1032}
1033
1034impl Default for SceneGraph {
1035 fn default() -> Self {
1036 Self::new()
1037 }
1038}
1039
1040fn merge_style(dst: &mut Properties, src: &Properties) {
1042 if src.fill.is_some() {
1043 dst.fill = src.fill.clone();
1044 }
1045 if src.stroke.is_some() {
1046 dst.stroke = src.stroke.clone();
1047 }
1048 if src.font.is_some() {
1049 dst.font = src.font.clone();
1050 }
1051 if src.corner_radius.is_some() {
1052 dst.corner_radius = src.corner_radius;
1053 }
1054 if src.opacity.is_some() {
1055 dst.opacity = src.opacity;
1056 }
1057 if src.shadow.is_some() {
1058 dst.shadow = src.shadow.clone();
1059 }
1060
1061 if src.text_align.is_some() {
1062 dst.text_align = src.text_align;
1063 }
1064 if src.text_valign.is_some() {
1065 dst.text_valign = src.text_valign;
1066 }
1067 if src.scale.is_some() {
1068 dst.scale = src.scale;
1069 }
1070}
1071
1072#[derive(Debug, Clone, Copy, Default, PartialEq)]
1076pub struct ResolvedBounds {
1077 pub x: f32,
1078 pub y: f32,
1079 pub width: f32,
1080 pub height: f32,
1081}
1082
1083impl ResolvedBounds {
1084 pub fn contains(&self, px: f32, py: f32) -> bool {
1086 px >= self.x && px <= self.x + self.width && py >= self.y && py <= self.y + self.height
1087 }
1088
1089 pub fn center(&self) -> (f32, f32) {
1091 (self.x + self.width / 2.0, self.y + self.height / 2.0)
1092 }
1093
1094 pub fn intersects_rect(&self, rx: f32, ry: f32, rw: f32, rh: f32) -> bool {
1096 self.x < rx + rw
1097 && self.x + self.width > rx
1098 && self.y < ry + rh
1099 && self.y + self.height > ry
1100 }
1101}
1102
1103#[cfg(test)]
1104mod tests {
1105 use super::*;
1106
1107 #[test]
1108 fn scene_graph_basics() {
1109 let mut sg = SceneGraph::new();
1110 let rect = SceneNode::new(
1111 NodeId::intern("box1"),
1112 NodeKind::Rect {
1113 width: 100.0,
1114 height: 50.0,
1115 },
1116 );
1117 let idx = sg.add_node(sg.root, rect);
1118
1119 assert!(sg.get_by_id(NodeId::intern("box1")).is_some());
1120 assert_eq!(sg.children(sg.root).len(), 1);
1121 assert_eq!(sg.children(sg.root)[0], idx);
1122 }
1123
1124 #[test]
1125 fn color_hex_roundtrip() {
1126 let c = Color::from_hex("#6C5CE7").unwrap();
1127 assert_eq!(c.to_hex(), "#6C5CE7");
1128
1129 let c2 = Color::from_hex("#FF000080").unwrap();
1130 assert!((c2.a - 128.0 / 255.0).abs() < 0.01);
1131 assert!(c2.to_hex().len() == 9); }
1133
1134 #[test]
1135 fn style_merging() {
1136 let mut sg = SceneGraph::new();
1137 sg.define_style(
1138 NodeId::intern("base"),
1139 Properties {
1140 fill: Some(Paint::Solid(Color::rgba(0.0, 0.0, 0.0, 1.0))),
1141 font: Some(FontSpec {
1142 family: "Inter".into(),
1143 weight: 400,
1144 size: 14.0,
1145 }),
1146 ..Default::default()
1147 },
1148 );
1149
1150 let mut node = SceneNode::new(
1151 NodeId::intern("txt"),
1152 NodeKind::Text {
1153 content: "hi".into(),
1154 max_width: None,
1155 },
1156 );
1157 node.use_styles.push(NodeId::intern("base"));
1158 node.props.font = Some(FontSpec {
1159 family: "Inter".into(),
1160 weight: 700,
1161 size: 24.0,
1162 });
1163
1164 let resolved = sg.resolve_style(&node, &[]);
1165 assert!(resolved.fill.is_some());
1167 let f = resolved.font.unwrap();
1169 assert_eq!(f.weight, 700);
1170 assert_eq!(f.size, 24.0);
1171 }
1172
1173 #[test]
1174 fn style_merging_align() {
1175 let mut sg = SceneGraph::new();
1176 sg.define_style(
1177 NodeId::intern("centered"),
1178 Properties {
1179 text_align: Some(TextAlign::Center),
1180 text_valign: Some(TextVAlign::Middle),
1181 ..Default::default()
1182 },
1183 );
1184
1185 let mut node = SceneNode::new(
1187 NodeId::intern("overridden"),
1188 NodeKind::Text {
1189 content: "hello".into(),
1190 max_width: None,
1191 },
1192 );
1193 node.use_styles.push(NodeId::intern("centered"));
1194 node.props.text_align = Some(TextAlign::Right);
1195
1196 let resolved = sg.resolve_style(&node, &[]);
1197 assert_eq!(resolved.text_align, Some(TextAlign::Right));
1199 assert_eq!(resolved.text_valign, Some(TextVAlign::Middle));
1201 }
1202
1203 #[test]
1204 fn test_effective_target_group_selects_group_first() {
1205 let mut sg = SceneGraph::new();
1206
1207 let group_id = NodeId::intern("my_group");
1209 let rect_id = NodeId::intern("my_rect");
1210
1211 let group = SceneNode::new(group_id, NodeKind::Group);
1212 let rect = SceneNode::new(
1213 rect_id,
1214 NodeKind::Rect {
1215 width: 10.0,
1216 height: 10.0,
1217 },
1218 );
1219
1220 let group_idx = sg.add_node(sg.root, group);
1221 sg.add_node(group_idx, rect);
1222
1223 assert_eq!(sg.effective_target(rect_id, &[]), group_id);
1225 assert_eq!(sg.effective_target(rect_id, &[group_id]), rect_id);
1227 assert_eq!(sg.effective_target(group_id, &[]), group_id);
1229 }
1230
1231 #[test]
1232 fn test_effective_target_nested_groups_selects_topmost() {
1233 let mut sg = SceneGraph::new();
1234
1235 let outer_id = NodeId::intern("group_outer");
1237 let inner_id = NodeId::intern("group_inner");
1238 let leaf_id = NodeId::intern("rect_leaf");
1239
1240 let outer = SceneNode::new(outer_id, NodeKind::Group);
1241 let inner = SceneNode::new(inner_id, NodeKind::Group);
1242 let leaf = SceneNode::new(
1243 leaf_id,
1244 NodeKind::Rect {
1245 width: 50.0,
1246 height: 50.0,
1247 },
1248 );
1249
1250 let outer_idx = sg.add_node(sg.root, outer);
1251 let inner_idx = sg.add_node(outer_idx, inner);
1252 sg.add_node(inner_idx, leaf);
1253
1254 assert_eq!(sg.effective_target(leaf_id, &[]), outer_id);
1256 assert_eq!(sg.effective_target(leaf_id, &[outer_id]), inner_id);
1258 assert_eq!(sg.effective_target(leaf_id, &[outer_id, inner_id]), leaf_id);
1260 assert_eq!(sg.effective_target(leaf_id, &[inner_id]), leaf_id);
1263 }
1264
1265 #[test]
1266 fn test_effective_target_nested_drill_down_three_levels() {
1267 let mut sg = SceneGraph::new();
1268
1269 let a_id = NodeId::intern("group_a");
1271 let b_id = NodeId::intern("group_b");
1272 let c_id = NodeId::intern("group_c");
1273 let leaf_id = NodeId::intern("deep_leaf");
1274
1275 let a = SceneNode::new(a_id, NodeKind::Group);
1276 let b = SceneNode::new(b_id, NodeKind::Group);
1277 let c = SceneNode::new(c_id, NodeKind::Group);
1278 let leaf = SceneNode::new(
1279 leaf_id,
1280 NodeKind::Rect {
1281 width: 10.0,
1282 height: 10.0,
1283 },
1284 );
1285
1286 let a_idx = sg.add_node(sg.root, a);
1287 let b_idx = sg.add_node(a_idx, b);
1288 let c_idx = sg.add_node(b_idx, c);
1289 sg.add_node(c_idx, leaf);
1290
1291 assert_eq!(sg.effective_target(leaf_id, &[]), a_id);
1293 assert_eq!(sg.effective_target(leaf_id, &[a_id]), b_id);
1294 assert_eq!(sg.effective_target(leaf_id, &[b_id]), c_id);
1295 assert_eq!(sg.effective_target(leaf_id, &[c_id]), leaf_id);
1296 }
1297
1298 #[test]
1299 fn test_visual_highlight_differs_from_selected() {
1300 let mut sg = SceneGraph::new();
1303
1304 let group_id = NodeId::intern("card");
1305 let child_id = NodeId::intern("card_title");
1306
1307 let group = SceneNode::new(group_id, NodeKind::Group);
1308 let child = SceneNode::new(
1309 child_id,
1310 NodeKind::Text {
1311 content: "Title".into(),
1312 max_width: None,
1313 },
1314 );
1315
1316 let group_idx = sg.add_node(sg.root, group);
1317 sg.add_node(group_idx, child);
1318
1319 let logical_target = sg.effective_target(child_id, &[]);
1321 assert_eq!(logical_target, group_id);
1323 assert_ne!(child_id, logical_target);
1325 let drilled = sg.effective_target(child_id, &[group_id]);
1327 assert_eq!(drilled, child_id);
1328 }
1329
1330 #[test]
1331 fn test_effective_target_no_group() {
1332 let mut sg = SceneGraph::new();
1333
1334 let rect_id = NodeId::intern("standalone_rect");
1336 let rect = SceneNode::new(
1337 rect_id,
1338 NodeKind::Rect {
1339 width: 10.0,
1340 height: 10.0,
1341 },
1342 );
1343 sg.add_node(sg.root, rect);
1344
1345 assert_eq!(sg.effective_target(rect_id, &[]), rect_id);
1347 }
1348
1349 #[test]
1350 fn test_is_ancestor_of() {
1351 let mut sg = SceneGraph::new();
1352
1353 let group_id = NodeId::intern("grp");
1355 let rect_id = NodeId::intern("r1");
1356 let other_id = NodeId::intern("other");
1357
1358 let group = SceneNode::new(group_id, NodeKind::Group);
1359 let rect = SceneNode::new(
1360 rect_id,
1361 NodeKind::Rect {
1362 width: 10.0,
1363 height: 10.0,
1364 },
1365 );
1366 let other = SceneNode::new(
1367 other_id,
1368 NodeKind::Rect {
1369 width: 5.0,
1370 height: 5.0,
1371 },
1372 );
1373
1374 let group_idx = sg.add_node(sg.root, group);
1375 sg.add_node(group_idx, rect);
1376 sg.add_node(sg.root, other);
1377
1378 assert!(sg.is_ancestor_of(group_id, rect_id));
1380 assert!(sg.is_ancestor_of(NodeId::intern("root"), rect_id));
1382 assert!(!sg.is_ancestor_of(rect_id, group_id));
1384 assert!(!sg.is_ancestor_of(group_id, group_id));
1386 assert!(!sg.is_ancestor_of(other_id, rect_id));
1388 }
1389
1390 #[test]
1391 fn test_resolve_style_scale_animation() {
1392 let sg = SceneGraph::new();
1393
1394 let mut node = SceneNode::new(
1395 NodeId::intern("btn"),
1396 NodeKind::Rect {
1397 width: 100.0,
1398 height: 40.0,
1399 },
1400 );
1401 node.props.fill = Some(Paint::Solid(Color::rgba(1.0, 0.0, 0.0, 1.0)));
1402 node.animations.push(AnimKeyframe {
1403 trigger: AnimTrigger::Press,
1404 duration_ms: 100,
1405 easing: Easing::EaseOut,
1406 properties: AnimProperties {
1407 scale: Some(0.97),
1408 ..Default::default()
1409 },
1410 delay_ms: None,
1411 });
1412
1413 let resolved = sg.resolve_style(&node, &[]);
1415 assert!(resolved.scale.is_none());
1416
1417 let resolved = sg.resolve_style(&node, &[AnimTrigger::Press]);
1419 assert_eq!(resolved.scale, Some(0.97));
1420 assert!(resolved.fill.is_some());
1422 }
1423
1424 #[test]
1425 fn z_order_bring_forward() {
1426 let mut sg = SceneGraph::new();
1427 let a = sg.add_node(
1428 sg.root,
1429 SceneNode::new(
1430 NodeId::intern("a"),
1431 NodeKind::Rect {
1432 width: 50.0,
1433 height: 50.0,
1434 },
1435 ),
1436 );
1437 let _b = sg.add_node(
1438 sg.root,
1439 SceneNode::new(
1440 NodeId::intern("b"),
1441 NodeKind::Rect {
1442 width: 50.0,
1443 height: 50.0,
1444 },
1445 ),
1446 );
1447 let _c = sg.add_node(
1448 sg.root,
1449 SceneNode::new(
1450 NodeId::intern("c"),
1451 NodeKind::Rect {
1452 width: 50.0,
1453 height: 50.0,
1454 },
1455 ),
1456 );
1457
1458 let ids: Vec<&str> = sg
1460 .children(sg.root)
1461 .iter()
1462 .map(|&i| sg.graph[i].id.as_str())
1463 .collect();
1464 assert_eq!(ids, vec!["a", "b", "c"]);
1465
1466 let changed = sg.bring_forward(a);
1468 assert!(changed);
1469 let ids: Vec<&str> = sg
1470 .children(sg.root)
1471 .iter()
1472 .map(|&i| sg.graph[i].id.as_str())
1473 .collect();
1474 assert_eq!(ids, vec!["b", "a", "c"]);
1475 }
1476
1477 #[test]
1478 fn z_order_send_backward() {
1479 let mut sg = SceneGraph::new();
1480 let _a = sg.add_node(
1481 sg.root,
1482 SceneNode::new(
1483 NodeId::intern("a"),
1484 NodeKind::Rect {
1485 width: 50.0,
1486 height: 50.0,
1487 },
1488 ),
1489 );
1490 let _b = sg.add_node(
1491 sg.root,
1492 SceneNode::new(
1493 NodeId::intern("b"),
1494 NodeKind::Rect {
1495 width: 50.0,
1496 height: 50.0,
1497 },
1498 ),
1499 );
1500 let c = sg.add_node(
1501 sg.root,
1502 SceneNode::new(
1503 NodeId::intern("c"),
1504 NodeKind::Rect {
1505 width: 50.0,
1506 height: 50.0,
1507 },
1508 ),
1509 );
1510
1511 let changed = sg.send_backward(c);
1513 assert!(changed);
1514 let ids: Vec<&str> = sg
1515 .children(sg.root)
1516 .iter()
1517 .map(|&i| sg.graph[i].id.as_str())
1518 .collect();
1519 assert_eq!(ids, vec!["a", "c", "b"]);
1520 }
1521
1522 #[test]
1523 fn z_order_bring_to_front() {
1524 let mut sg = SceneGraph::new();
1525 let a = sg.add_node(
1526 sg.root,
1527 SceneNode::new(
1528 NodeId::intern("a"),
1529 NodeKind::Rect {
1530 width: 50.0,
1531 height: 50.0,
1532 },
1533 ),
1534 );
1535 let _b = sg.add_node(
1536 sg.root,
1537 SceneNode::new(
1538 NodeId::intern("b"),
1539 NodeKind::Rect {
1540 width: 50.0,
1541 height: 50.0,
1542 },
1543 ),
1544 );
1545 let _c = sg.add_node(
1546 sg.root,
1547 SceneNode::new(
1548 NodeId::intern("c"),
1549 NodeKind::Rect {
1550 width: 50.0,
1551 height: 50.0,
1552 },
1553 ),
1554 );
1555
1556 let changed = sg.bring_to_front(a);
1558 assert!(changed);
1559 let ids: Vec<&str> = sg
1560 .children(sg.root)
1561 .iter()
1562 .map(|&i| sg.graph[i].id.as_str())
1563 .collect();
1564 assert_eq!(ids, vec!["b", "c", "a"]);
1565 }
1566
1567 #[test]
1568 fn z_order_send_to_back() {
1569 let mut sg = SceneGraph::new();
1570 let _a = sg.add_node(
1571 sg.root,
1572 SceneNode::new(
1573 NodeId::intern("a"),
1574 NodeKind::Rect {
1575 width: 50.0,
1576 height: 50.0,
1577 },
1578 ),
1579 );
1580 let _b = sg.add_node(
1581 sg.root,
1582 SceneNode::new(
1583 NodeId::intern("b"),
1584 NodeKind::Rect {
1585 width: 50.0,
1586 height: 50.0,
1587 },
1588 ),
1589 );
1590 let c = sg.add_node(
1591 sg.root,
1592 SceneNode::new(
1593 NodeId::intern("c"),
1594 NodeKind::Rect {
1595 width: 50.0,
1596 height: 50.0,
1597 },
1598 ),
1599 );
1600
1601 let changed = sg.send_to_back(c);
1603 assert!(changed);
1604 let ids: Vec<&str> = sg
1605 .children(sg.root)
1606 .iter()
1607 .map(|&i| sg.graph[i].id.as_str())
1608 .collect();
1609 assert_eq!(ids, vec!["c", "a", "b"]);
1610 }
1611
1612 #[test]
1613 fn z_order_emitter_roundtrip() {
1614 use crate::emitter::emit_document;
1615 use crate::parser::parse_document;
1616
1617 let mut sg = SceneGraph::new();
1618 let a = sg.add_node(
1619 sg.root,
1620 SceneNode::new(
1621 NodeId::intern("a"),
1622 NodeKind::Rect {
1623 width: 50.0,
1624 height: 50.0,
1625 },
1626 ),
1627 );
1628 let _b = sg.add_node(
1629 sg.root,
1630 SceneNode::new(
1631 NodeId::intern("b"),
1632 NodeKind::Rect {
1633 width: 50.0,
1634 height: 50.0,
1635 },
1636 ),
1637 );
1638 let _c = sg.add_node(
1639 sg.root,
1640 SceneNode::new(
1641 NodeId::intern("c"),
1642 NodeKind::Rect {
1643 width: 50.0,
1644 height: 50.0,
1645 },
1646 ),
1647 );
1648
1649 sg.bring_to_front(a);
1651
1652 let text = emit_document(&sg);
1654 let reparsed = parse_document(&text).unwrap();
1655 let ids: Vec<&str> = reparsed
1656 .children(reparsed.root)
1657 .iter()
1658 .map(|&i| reparsed.graph[i].id.as_str())
1659 .collect();
1660 assert_eq!(
1661 ids,
1662 vec!["b", "c", "a"],
1663 "Z-order should survive emit→parse roundtrip. Emitted:\n{}",
1664 text
1665 );
1666 }
1667
1668 #[test]
1669 fn move_child_to_index_basic() {
1670 let mut sg = SceneGraph::new();
1671 let a = sg.add_node(
1672 sg.root,
1673 SceneNode::new(
1674 NodeId::intern("a"),
1675 NodeKind::Rect {
1676 width: 10.0,
1677 height: 10.0,
1678 },
1679 ),
1680 );
1681 let _b = sg.add_node(
1682 sg.root,
1683 SceneNode::new(
1684 NodeId::intern("b"),
1685 NodeKind::Rect {
1686 width: 10.0,
1687 height: 10.0,
1688 },
1689 ),
1690 );
1691 let _c = sg.add_node(
1692 sg.root,
1693 SceneNode::new(
1694 NodeId::intern("c"),
1695 NodeKind::Rect {
1696 width: 10.0,
1697 height: 10.0,
1698 },
1699 ),
1700 );
1701 assert!(sg.move_child_to_index(a, 2));
1703 let ids: Vec<&str> = sg
1704 .children(sg.root)
1705 .iter()
1706 .map(|&i| sg.graph[i].id.as_str())
1707 .collect();
1708 assert_eq!(ids, vec!["b", "c", "a"]);
1709 }
1710
1711 #[test]
1712 fn move_child_to_index_out_of_bounds() {
1713 let mut sg = SceneGraph::new();
1714 let a = sg.add_node(
1715 sg.root,
1716 SceneNode::new(
1717 NodeId::intern("a"),
1718 NodeKind::Rect {
1719 width: 10.0,
1720 height: 10.0,
1721 },
1722 ),
1723 );
1724 let _b = sg.add_node(
1725 sg.root,
1726 SceneNode::new(
1727 NodeId::intern("b"),
1728 NodeKind::Rect {
1729 width: 10.0,
1730 height: 10.0,
1731 },
1732 ),
1733 );
1734 assert!(sg.move_child_to_index(a, 999));
1736 let ids: Vec<&str> = sg
1737 .children(sg.root)
1738 .iter()
1739 .map(|&i| sg.graph[i].id.as_str())
1740 .collect();
1741 assert_eq!(ids, vec!["b", "a"]);
1742 }
1743
1744 #[test]
1745 fn move_child_to_index_noop() {
1746 let mut sg = SceneGraph::new();
1747 let a = sg.add_node(
1748 sg.root,
1749 SceneNode::new(
1750 NodeId::intern("a"),
1751 NodeKind::Rect {
1752 width: 10.0,
1753 height: 10.0,
1754 },
1755 ),
1756 );
1757 assert!(!sg.move_child_to_index(a, 0));
1759 }
1760
1761 #[test]
1762 fn reparent_then_children_correct() {
1763 let mut sg = SceneGraph::new();
1764 let group = sg.add_node(
1765 sg.root,
1766 SceneNode::new(NodeId::intern("g"), NodeKind::Group),
1767 );
1768 let rect = sg.add_node(
1769 sg.root,
1770 SceneNode::new(
1771 NodeId::intern("r"),
1772 NodeKind::Rect {
1773 width: 10.0,
1774 height: 10.0,
1775 },
1776 ),
1777 );
1778 assert_eq!(sg.children(sg.root).len(), 2);
1780 sg.reparent_node(rect, group);
1782 assert_eq!(sg.children(sg.root).len(), 1);
1783 assert_eq!(sg.children(group).len(), 1);
1784 assert_eq!(sg.children(group)[0], rect);
1785 sg.reparent_node(rect, sg.root);
1787 assert_eq!(sg.children(sg.root).len(), 2);
1788 assert_eq!(sg.children(group).len(), 0);
1789 }
1790
1791 #[test]
1794 fn reparent_after_reorder_no_ghost_children() {
1795 let mut sg = SceneGraph::new();
1796 let parent = sg.add_node(
1797 sg.root,
1798 SceneNode::new(
1799 NodeId::intern("parent"),
1800 NodeKind::Rect {
1801 width: 200.0,
1802 height: 200.0,
1803 },
1804 ),
1805 );
1806 let child_a = sg.add_node(
1807 parent,
1808 SceneNode::new(
1809 NodeId::intern("a"),
1810 NodeKind::Rect {
1811 width: 50.0,
1812 height: 50.0,
1813 },
1814 ),
1815 );
1816 let child_b = sg.add_node(
1817 parent,
1818 SceneNode::new(
1819 NodeId::intern("b"),
1820 NodeKind::Rect {
1821 width: 50.0,
1822 height: 50.0,
1823 },
1824 ),
1825 );
1826
1827 assert!(sg.move_child_to_index(child_b, 0));
1829 assert!(sg.sorted_child_order.contains_key(&parent));
1830 assert_eq!(sg.children(parent), vec![child_b, child_a]);
1831
1832 sg.reparent_node(child_b, sg.root);
1834
1835 let parent_children = sg.children(parent);
1837 assert_eq!(
1838 parent_children.len(),
1839 1,
1840 "parent should have exactly 1 child after reparent, got {:?}",
1841 parent_children
1842 );
1843 assert_eq!(parent_children[0], child_a);
1844
1845 let root_children = sg.children(sg.root);
1847 assert_eq!(root_children.len(), 2);
1848 assert!(root_children.contains(&parent));
1849 assert!(root_children.contains(&child_b));
1850
1851 let emitted = crate::emitter::emit_document(&sg);
1853 let count = emitted.matches("@b").count();
1854 assert_eq!(
1855 count, 1,
1856 "@b should appear exactly once in emitted text, found {count} times:\n{emitted}"
1857 );
1858 }
1859}