Skip to main content

foundry_runtime/
scene.rs

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    // Box model (px)
67    pub margin: [f32; 4], // top, right, bottom, left
68    pub padding: [f32; 4],
69    pub border_width: [f32; 4],
70
71    // Sizing
72    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    // Flexbox
80    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    // Position
90    pub position: Position,
91    pub top: SizeValue,
92    pub left: SizeValue,
93    pub right: SizeValue,
94    pub bottom: SizeValue,
95
96    // Visual
97    pub background_color: Color,
98    pub color: Color,
99    pub border_color: Color,
100    pub border_radius: [f32; 4], // top-left, top-right, bottom-right, bottom-left
101    pub opacity: f32,
102
103    // Text
104    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    // Overflow
111    pub overflow: Overflow,
112
113    // Z-index
114    pub z_index: i32,
115
116    // Transitions
117    pub transition_duration: f32,
118    pub transition_property: TransitionProperty,
119
120    // Animation
121    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    /// Interpolate between self and target by t (0.0 = self, 1.0 = target)
178    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>, // event name -> JS code
398    pub hover_style: Option<ResolvedStyle>,      // :hover overrides
399    pub base_style: Option<ResolvedStyle>,       // original style (before hover)
400    pub is_hovered: bool,
401    pub image_src: Option<String>,
402    // Transition animation state
403    pub transition_start: Option<std::time::Instant>,
404    pub transition_from: Option<ResolvedStyle>,
405    pub transition_to: Option<ResolvedStyle>,
406    // CSS animation state
407    pub animation: Option<AnimationState>,
408}
409
410#[derive(Debug, Clone)]
411pub struct Keyframe {
412    pub percent: f32, // 0.0 = from, 1.0 = to
413    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,        // seconds
433    pub delay: f32,           // seconds
434    pub iteration_count: f32, // f32::INFINITY for infinite
435    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        // Set default display based on element kind
445        if kind.is_inline() {
446            style.display = Display::Inline;
447        }
448        // Heading font sizes
449        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    /// Tick all animations. Returns true if any animation is active (needs redraw).
572    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            // Tick CSS transitions
578            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            // Tick CSS keyframe animations
616            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; // Still in delay
628                }
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                    // Animation finished — apply final keyframe
635                    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                // Find the two keyframes to interpolate between
655                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}