1use std::collections::HashMap;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4pub struct NodeId(pub usize);
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum ElementKind {
8 Div,
9 Span,
10 P,
11 H1,
12 H2,
13 H3,
14 H4,
15 H5,
16 H6,
17 Button,
18 Input,
19 Img,
20 A,
21 Ul,
22 Ol,
23 Li,
24 Body,
25 Html,
26 Text,
27 Unknown,
28}
29
30impl ElementKind {
31 pub fn from_tag(tag: &str) -> Self {
32 match tag {
33 "div" => Self::Div,
34 "span" => Self::Span,
35 "p" => Self::P,
36 "h1" => Self::H1,
37 "h2" => Self::H2,
38 "h3" => Self::H3,
39 "h4" => Self::H4,
40 "h5" => Self::H5,
41 "h6" => Self::H6,
42 "button" => Self::Button,
43 "input" => Self::Input,
44 "img" => Self::Img,
45 "a" => Self::A,
46 "ul" => Self::Ul,
47 "ol" => Self::Ol,
48 "li" => Self::Li,
49 "body" => Self::Body,
50 "html" => Self::Html,
51 _ => Self::Unknown,
52 }
53 }
54
55 pub fn is_inline(&self) -> bool {
56 matches!(self, Self::Span | Self::A | Self::Text)
57 }
58
59 pub fn is_block(&self) -> bool {
60 !self.is_inline() && !matches!(self, Self::Unknown)
61 }
62}
63
64#[derive(Debug, Clone)]
65pub struct ResolvedStyle {
66 pub margin: [f32; 4], pub padding: [f32; 4],
69 pub border_width: [f32; 4],
70
71 pub width: SizeValue,
73 pub height: SizeValue,
74 pub min_width: SizeValue,
75 pub min_height: SizeValue,
76 pub max_width: SizeValue,
77 pub max_height: SizeValue,
78
79 pub display: Display,
81 pub flex_direction: FlexDirection,
82 pub justify_content: JustifyContent,
83 pub align_items: AlignItems,
84 pub flex_wrap: FlexWrap,
85 pub flex_grow: f32,
86 pub flex_shrink: f32,
87 pub gap: f32,
88
89 pub position: Position,
91 pub top: SizeValue,
92 pub left: SizeValue,
93 pub right: SizeValue,
94 pub bottom: SizeValue,
95
96 pub background_color: Color,
98 pub color: Color,
99 pub border_color: Color,
100 pub border_radius: [f32; 4], pub opacity: f32,
102
103 pub font_size: f32,
105 pub font_weight: u16,
106 pub font_family: String,
107 pub text_align: TextAlign,
108 pub line_height: f32,
109
110 pub overflow: Overflow,
112
113 pub z_index: i32,
115
116 pub transition_duration: f32,
118 pub transition_property: TransitionProperty,
119
120 pub animation_name: String,
122 pub animation_duration: f32,
123 pub animation_delay: f32,
124 pub animation_iteration_count: f32,
125 pub animation_direction: AnimationDirection,
126}
127
128impl Default for ResolvedStyle {
129 fn default() -> Self {
130 Self {
131 margin: [0.0; 4],
132 padding: [0.0; 4],
133 border_width: [0.0; 4],
134 width: SizeValue::Auto,
135 height: SizeValue::Auto,
136 min_width: SizeValue::Auto,
137 min_height: SizeValue::Auto,
138 max_width: SizeValue::Auto,
139 max_height: SizeValue::Auto,
140 display: Display::Block,
141 flex_direction: FlexDirection::Row,
142 justify_content: JustifyContent::Start,
143 align_items: AlignItems::Stretch,
144 flex_wrap: FlexWrap::NoWrap,
145 flex_grow: 0.0,
146 flex_shrink: 1.0,
147 gap: 0.0,
148 position: Position::Relative,
149 top: SizeValue::Auto,
150 left: SizeValue::Auto,
151 right: SizeValue::Auto,
152 bottom: SizeValue::Auto,
153 background_color: Color::TRANSPARENT,
154 color: Color::BLACK,
155 border_color: Color::BLACK,
156 border_radius: [0.0; 4],
157 opacity: 1.0,
158 font_size: 16.0,
159 font_weight: 400,
160 font_family: String::new(),
161 text_align: TextAlign::Left,
162 line_height: 1.2,
163 overflow: Overflow::Visible,
164 z_index: 0,
165 transition_duration: 0.0,
166 transition_property: TransitionProperty::None,
167 animation_name: String::new(),
168 animation_duration: 0.0,
169 animation_delay: 0.0,
170 animation_iteration_count: 1.0,
171 animation_direction: AnimationDirection::Normal,
172 }
173 }
174}
175
176impl ResolvedStyle {
177 pub fn lerp(&self, target: &ResolvedStyle, t: f32) -> ResolvedStyle {
179 let mut result = target.clone();
180 let t = t.clamp(0.0, 1.0);
181 let inv = 1.0 - t;
182
183 result.background_color = Color {
184 r: self.background_color.r * inv + target.background_color.r * t,
185 g: self.background_color.g * inv + target.background_color.g * t,
186 b: self.background_color.b * inv + target.background_color.b * t,
187 a: self.background_color.a * inv + target.background_color.a * t,
188 };
189 result.color = Color {
190 r: self.color.r * inv + target.color.r * t,
191 g: self.color.g * inv + target.color.g * t,
192 b: self.color.b * inv + target.color.b * t,
193 a: self.color.a * inv + target.color.a * t,
194 };
195 result.border_color = Color {
196 r: self.border_color.r * inv + target.border_color.r * t,
197 g: self.border_color.g * inv + target.border_color.g * t,
198 b: self.border_color.b * inv + target.border_color.b * t,
199 a: self.border_color.a * inv + target.border_color.a * t,
200 };
201
202 result.opacity = self.opacity * inv + target.opacity * t;
203 result.font_size = self.font_size * inv + target.font_size * t;
204 result.border_radius = [
205 self.border_radius[0] * inv + target.border_radius[0] * t,
206 self.border_radius[1] * inv + target.border_radius[1] * t,
207 self.border_radius[2] * inv + target.border_radius[2] * t,
208 self.border_radius[3] * inv + target.border_radius[3] * t,
209 ];
210 result.border_width = [
211 self.border_width[0] * inv + target.border_width[0] * t,
212 self.border_width[1] * inv + target.border_width[1] * t,
213 self.border_width[2] * inv + target.border_width[2] * t,
214 self.border_width[3] * inv + target.border_width[3] * t,
215 ];
216 result.padding = [
217 self.padding[0] * inv + target.padding[0] * t,
218 self.padding[1] * inv + target.padding[1] * t,
219 self.padding[2] * inv + target.padding[2] * t,
220 self.padding[3] * inv + target.padding[3] * t,
221 ];
222 result.margin = [
223 self.margin[0] * inv + target.margin[0] * t,
224 self.margin[1] * inv + target.margin[1] * t,
225 self.margin[2] * inv + target.margin[2] * t,
226 self.margin[3] * inv + target.margin[3] * t,
227 ];
228
229 result
230 }
231}
232
233#[derive(Debug, Clone, Copy, PartialEq)]
234pub enum SizeValue {
235 Px(f32),
236 Percent(f32),
237 Em(f32),
238 Rem(f32),
239 Vh(f32),
240 Vw(f32),
241 Auto,
242}
243
244#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
245pub enum Display {
246 #[default]
247 Block,
248 Flex,
249 Inline,
250 None,
251}
252
253#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
254pub enum FlexDirection {
255 #[default]
256 Row,
257 Column,
258 RowReverse,
259 ColumnReverse,
260}
261
262#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
263pub enum JustifyContent {
264 #[default]
265 Start,
266 Center,
267 End,
268 SpaceBetween,
269 SpaceAround,
270 SpaceEvenly,
271}
272
273#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
274pub enum AlignItems {
275 #[default]
276 Stretch,
277 Start,
278 Center,
279 End,
280}
281
282#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
283pub enum FlexWrap {
284 #[default]
285 NoWrap,
286 Wrap,
287}
288
289#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
290pub enum Position {
291 #[default]
292 Relative,
293 Absolute,
294 Fixed,
295}
296
297#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
298pub enum TextAlign {
299 #[default]
300 Left,
301 Center,
302 Right,
303}
304
305#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
306pub enum Overflow {
307 #[default]
308 Visible,
309 Hidden,
310 Scroll,
311}
312
313#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
314pub enum TransitionProperty {
315 #[default]
316 None,
317 All,
318}
319
320#[derive(Debug, Clone, Copy, PartialEq)]
321pub struct Color {
322 pub r: f32,
323 pub g: f32,
324 pub b: f32,
325 pub a: f32,
326}
327
328impl Color {
329 pub const TRANSPARENT: Self = Self {
330 r: 0.0,
331 g: 0.0,
332 b: 0.0,
333 a: 0.0,
334 };
335 pub const BLACK: Self = Self {
336 r: 0.0,
337 g: 0.0,
338 b: 0.0,
339 a: 1.0,
340 };
341 pub const WHITE: Self = Self {
342 r: 1.0,
343 g: 1.0,
344 b: 1.0,
345 a: 1.0,
346 };
347
348 pub fn from_rgba(r: u8, g: u8, b: u8, a: f32) -> Self {
349 Self {
350 r: r as f32 / 255.0,
351 g: g as f32 / 255.0,
352 b: b as f32 / 255.0,
353 a,
354 }
355 }
356
357 pub fn to_array(&self) -> [f32; 4] {
358 [self.r, self.g, self.b, self.a]
359 }
360}
361
362#[derive(Debug, Clone)]
363pub struct LayoutRect {
364 pub x: f32,
365 pub y: f32,
366 pub width: f32,
367 pub height: f32,
368}
369
370impl Default for LayoutRect {
371 fn default() -> Self {
372 Self {
373 x: 0.0,
374 y: 0.0,
375 width: 0.0,
376 height: 0.0,
377 }
378 }
379}
380
381#[derive(Debug, Clone)]
382pub struct SceneNode {
383 pub id: NodeId,
384 pub kind: ElementKind,
385 pub tag: String,
386 pub text_content: Option<String>,
387 pub attributes: HashMap<String, String>,
388 pub classes: Vec<String>,
389 pub element_id: Option<String>,
390 pub style: ResolvedStyle,
391 pub layout: LayoutRect,
392 pub children: Vec<NodeId>,
393 pub parent: Option<NodeId>,
394 pub dirty: bool,
395 pub scroll_offset: (f32, f32),
396 pub content_height: f32,
397 pub event_handlers: HashMap<String, String>, pub hover_style: Option<ResolvedStyle>, pub base_style: Option<ResolvedStyle>, pub is_hovered: bool,
401 pub image_src: Option<String>,
402 pub transition_start: Option<std::time::Instant>,
404 pub transition_from: Option<ResolvedStyle>,
405 pub transition_to: Option<ResolvedStyle>,
406 pub animation: Option<AnimationState>,
408}
409
410#[derive(Debug, Clone)]
411pub struct Keyframe {
412 pub percent: f32, pub style: ResolvedStyle,
414}
415
416#[derive(Debug, Clone)]
417pub struct KeyframeAnimation {
418 pub name: String,
419 pub keyframes: Vec<Keyframe>,
420}
421
422#[derive(Debug, Clone, Copy, PartialEq, Eq)]
423pub enum AnimationDirection {
424 Normal,
425 Reverse,
426 Alternate,
427}
428
429#[derive(Debug, Clone)]
430pub struct AnimationState {
431 pub animation_name: String,
432 pub duration: f32, pub delay: f32, pub iteration_count: f32, pub direction: AnimationDirection,
436 pub start_time: Option<std::time::Instant>,
437 pub current_iteration: f32,
438 pub keyframes: Vec<Keyframe>,
439}
440
441impl SceneNode {
442 pub fn new(id: NodeId, kind: ElementKind, tag: String) -> Self {
443 let mut style = ResolvedStyle::default();
444 if kind.is_inline() {
446 style.display = Display::Inline;
447 }
448 match kind {
450 ElementKind::H1 => {
451 style.font_size = 32.0;
452 style.font_weight = 700;
453 }
454 ElementKind::H2 => {
455 style.font_size = 24.0;
456 style.font_weight = 700;
457 }
458 ElementKind::H3 => {
459 style.font_size = 20.0;
460 style.font_weight = 700;
461 }
462 ElementKind::H4 => {
463 style.font_size = 16.0;
464 style.font_weight = 700;
465 }
466 ElementKind::H5 => {
467 style.font_size = 14.0;
468 style.font_weight = 700;
469 }
470 ElementKind::H6 => {
471 style.font_size = 12.0;
472 style.font_weight = 700;
473 }
474 _ => {}
475 }
476
477 Self {
478 id,
479 kind,
480 tag,
481 text_content: None,
482 attributes: HashMap::new(),
483 classes: Vec::new(),
484 element_id: None,
485 style,
486 layout: LayoutRect::default(),
487 children: Vec::new(),
488 parent: None,
489 dirty: true,
490 scroll_offset: (0.0, 0.0),
491 content_height: 0.0,
492 event_handlers: HashMap::new(),
493 hover_style: None,
494 base_style: None,
495 is_hovered: false,
496 image_src: None,
497 transition_start: None,
498 transition_from: None,
499 transition_to: None,
500 animation: None,
501 }
502 }
503
504 pub fn mark_dirty(&mut self) {
505 self.dirty = true;
506 }
507}
508
509pub struct SceneGraph {
510 pub nodes: Vec<SceneNode>,
511 pub root: Option<NodeId>,
512 pub keyframes: Vec<KeyframeAnimation>,
513}
514
515impl Default for SceneGraph {
516 fn default() -> Self {
517 Self::new()
518 }
519}
520
521impl SceneGraph {
522 pub fn new() -> Self {
523 Self {
524 nodes: Vec::new(),
525 root: None,
526 keyframes: Vec::new(),
527 }
528 }
529
530 pub fn add_node(&mut self, kind: ElementKind, tag: String) -> NodeId {
531 let id = NodeId(self.nodes.len());
532 self.nodes.push(SceneNode::new(id, kind, tag));
533 if self.root.is_none() {
534 self.root = Some(id);
535 }
536 id
537 }
538
539 pub fn add_child(&mut self, parent: NodeId, child: NodeId) {
540 self.nodes[parent.0].children.push(child);
541 self.nodes[child.0].parent = Some(parent);
542 }
543
544 pub fn get(&self, id: NodeId) -> &SceneNode {
545 &self.nodes[id.0]
546 }
547
548 pub fn get_mut(&mut self, id: NodeId) -> &mut SceneNode {
549 &mut self.nodes[id.0]
550 }
551
552 pub fn mark_dirty_recursive(&mut self, id: NodeId) {
553 self.nodes[id.0].dirty = true;
554 let children: Vec<NodeId> = self.nodes[id.0].children.clone();
555 for child in children {
556 self.mark_dirty_recursive(child);
557 }
558 }
559
560 pub fn find_by_element_id(&self, element_id: &str) -> Option<NodeId> {
561 self.nodes
562 .iter()
563 .find(|n| n.element_id.as_deref() == Some(element_id))
564 .map(|n| n.id)
565 }
566
567 pub fn node_count(&self) -> usize {
568 self.nodes.len()
569 }
570
571 pub fn tick_animations(&mut self) -> bool {
573 let now = std::time::Instant::now();
574 let mut any_active = false;
575
576 for node in &mut self.nodes {
577 if let (Some(start), Some(from), Some(to)) = (
579 node.transition_start,
580 node.transition_from.as_ref(),
581 node.transition_to.as_ref(),
582 ) {
583 let duration = node
584 .style
585 .transition_duration
586 .max(from.transition_duration.max(to.transition_duration));
587 if duration <= 0.0 {
588 node.style = to.clone();
589 node.transition_start = None;
590 node.transition_from = None;
591 node.transition_to = None;
592 node.dirty = true;
593 continue;
594 }
595 let elapsed = now.duration_since(start).as_secs_f32();
596 let t = (elapsed / duration).clamp(0.0, 1.0);
597 node.style = from.lerp(to, t);
598 node.style.transition_duration = to.transition_duration;
599 node.style.transition_property = to.transition_property;
600 node.dirty = true;
601 any_active = true;
602 if t >= 1.0 {
603 node.style = to.clone();
604 node.transition_start = None;
605 node.transition_from = None;
606 node.transition_to = None;
607 if !node.is_hovered {
608 if let Some(base) = &node.base_style {
609 node.style = base.clone();
610 }
611 }
612 }
613 }
614
615 if let Some(anim) = &mut node.animation {
617 if anim.keyframes.len() < 2 || anim.duration <= 0.0 {
618 continue;
619 }
620 if anim.start_time.is_none() {
621 anim.start_time = Some(now);
622 }
623 let start = anim.start_time.unwrap();
624 let elapsed = now.duration_since(start).as_secs_f32() - anim.delay;
625 if elapsed < 0.0 {
626 any_active = true;
627 continue; }
629
630 let cycle_time = elapsed % anim.duration;
631 let iteration = (elapsed / anim.duration).floor();
632
633 if iteration >= anim.iteration_count && !anim.iteration_count.is_infinite() {
634 if let Some(last) = anim.keyframes.last() {
636 apply_keyframe_style(&mut node.style, &last.style);
637 }
638 node.animation = None;
639 node.dirty = true;
640 continue;
641 }
642
643 let mut progress = cycle_time / anim.duration;
644 match anim.direction {
645 AnimationDirection::Reverse => progress = 1.0 - progress,
646 AnimationDirection::Alternate => {
647 if iteration as u32 % 2 == 1 {
648 progress = 1.0 - progress;
649 }
650 }
651 AnimationDirection::Normal => {}
652 }
653
654 let kfs = &anim.keyframes;
656 let mut from_idx = 0;
657 let mut to_idx = kfs.len() - 1;
658 for j in 0..kfs.len() - 1 {
659 if progress >= kfs[j].percent && progress <= kfs[j + 1].percent {
660 from_idx = j;
661 to_idx = j + 1;
662 break;
663 }
664 }
665
666 let from_pct = kfs[from_idx].percent;
667 let to_pct = kfs[to_idx].percent;
668 let local_t = if (to_pct - from_pct).abs() > 0.001 {
669 (progress - from_pct) / (to_pct - from_pct)
670 } else {
671 1.0
672 };
673
674 let interpolated = kfs[from_idx].style.lerp(&kfs[to_idx].style, local_t);
675 apply_keyframe_style(&mut node.style, &interpolated);
676 node.dirty = true;
677 any_active = true;
678 }
679 }
680
681 any_active
682 }
683}
684
685fn apply_keyframe_style(target: &mut ResolvedStyle, kf: &ResolvedStyle) {
686 target.background_color = kf.background_color;
687 target.color = kf.color;
688 target.border_color = kf.border_color;
689 target.opacity = kf.opacity;
690 target.border_radius = kf.border_radius;
691 target.padding = kf.padding;
692 target.margin = kf.margin;
693}