Skip to main content

foundry_runtime/
events.rs

1use crate::scene::*;
2
3#[derive(Debug, Clone)]
4pub enum AppEvent {
5    Click(f32, f32),
6    MouseMove(f32, f32),
7    KeyPress(String),
8    Scroll(f32, f32, f32, f32), // x, y, dx, dy
9}
10
11#[derive(Debug, Clone)]
12pub struct EventResult {
13    pub target: NodeId,
14    pub event_type: String,
15    pub handler_code: String,
16}
17
18pub struct EventSystem {
19    pub hover_node: Option<NodeId>,
20    pub focus_node: Option<NodeId>,
21}
22
23impl Default for EventSystem {
24    fn default() -> Self {
25        Self::new()
26    }
27}
28
29impl EventSystem {
30    pub fn new() -> Self {
31        Self {
32            hover_node: None,
33            focus_node: None,
34        }
35    }
36
37    pub fn handle(&mut self, event: &AppEvent, scene: &mut SceneGraph) -> Vec<EventResult> {
38        match event {
39            AppEvent::Click(x, y) => self.handle_click(*x, *y, scene),
40            AppEvent::MouseMove(x, y) => self.handle_mouse_move(*x, *y, scene),
41            AppEvent::Scroll(x, y, dx, dy) => {
42                self.handle_scroll(*x, *y, *dx, *dy, scene);
43                Vec::new()
44            }
45            AppEvent::KeyPress(key) => self.handle_key(key, scene),
46        }
47    }
48
49    fn handle_click(&mut self, x: f32, y: f32, scene: &SceneGraph) -> Vec<EventResult> {
50        let mut results = Vec::new();
51
52        if let Some(hit) = self.hit_test(x, y, scene) {
53            // Walk up from hit node, collecting click handlers (bubble)
54            let mut current = Some(hit);
55            while let Some(node_id) = current {
56                let node = scene.get(node_id);
57                if let Some(handler) = node.event_handlers.get("click") {
58                    results.push(EventResult {
59                        target: hit,
60                        event_type: "click".to_string(),
61                        handler_code: handler.clone(),
62                    });
63                }
64                current = node.parent;
65            }
66        }
67
68        results
69    }
70
71    fn handle_mouse_move(&mut self, x: f32, y: f32, scene: &mut SceneGraph) -> Vec<EventResult> {
72        let mut results = Vec::new();
73        let new_hover = self.hit_test(x, y, scene);
74
75        if new_hover != self.hover_node {
76            // Remove hover style from old node
77            if let Some(old) = self.hover_node {
78                let node = scene.get(old);
79                let handler = node.event_handlers.get("mouseleave").cloned();
80                let has_base = node.base_style.is_some();
81                let has_transition = node.style.transition_duration > 0.0;
82                if let Some(handler) = handler {
83                    results.push(EventResult {
84                        target: old,
85                        event_type: "mouseleave".to_string(),
86                        handler_code: handler,
87                    });
88                }
89                if has_base {
90                    let base = scene.get(old).base_style.clone().unwrap();
91                    if has_transition {
92                        // Start transition back to base
93                        let current = scene.get(old).style.clone();
94                        let node = scene.get_mut(old);
95                        node.transition_from = Some(current);
96                        node.transition_to = Some(base);
97                        node.transition_start = Some(std::time::Instant::now());
98                    } else {
99                        scene.get_mut(old).style = base;
100                    }
101                    scene.get_mut(old).is_hovered = false;
102                    scene.get_mut(old).dirty = true;
103                }
104            }
105            // Apply hover style to new node
106            if let Some(new) = new_hover {
107                let node = scene.get(new);
108                let handler = node.event_handlers.get("mouseenter").cloned();
109                let has_hover = node.hover_style.is_some();
110                let has_transition = node.style.transition_duration > 0.0
111                    || node
112                        .hover_style
113                        .as_ref()
114                        .map(|h| h.transition_duration > 0.0)
115                        .unwrap_or(false);
116                if let Some(handler) = handler {
117                    results.push(EventResult {
118                        target: new,
119                        event_type: "mouseenter".to_string(),
120                        handler_code: handler,
121                    });
122                }
123                if has_hover {
124                    let hover = scene.get(new).hover_style.clone().unwrap();
125                    if has_transition {
126                        // Start transition to hover style
127                        let current = scene.get(new).style.clone();
128                        let node = scene.get_mut(new);
129                        node.transition_from = Some(current);
130                        node.transition_to = Some(hover);
131                        node.transition_start = Some(std::time::Instant::now());
132                    } else {
133                        scene.get_mut(new).style = hover;
134                    }
135                    scene.get_mut(new).is_hovered = true;
136                    scene.get_mut(new).dirty = true;
137                }
138            }
139            self.hover_node = new_hover;
140        }
141
142        results
143    }
144
145    fn handle_scroll(&mut self, x: f32, y: f32, _dx: f32, dy: f32, scene: &mut SceneGraph) {
146        // Find the deepest scrollable container at (x, y)
147        if let Some(root) = scene.root {
148            if let Some(scroll_target) = self.find_scrollable(x, y, scene, root) {
149                let node = scene.get_mut(scroll_target);
150                let max_scroll = (node.content_height - node.layout.height).max(0.0);
151                node.scroll_offset.1 = (node.scroll_offset.1 + dy * 30.0).clamp(0.0, max_scroll);
152            }
153        }
154    }
155
156    fn handle_key(&self, _key: &str, scene: &SceneGraph) -> Vec<EventResult> {
157        let mut results = Vec::new();
158
159        if let Some(focus) = self.focus_node {
160            let node = scene.get(focus);
161            if let Some(handler) = node.event_handlers.get("keypress") {
162                results.push(EventResult {
163                    target: focus,
164                    event_type: "keypress".to_string(),
165                    handler_code: handler.clone(),
166                });
167            }
168        }
169
170        results
171    }
172
173    fn hit_test(&self, x: f32, y: f32, scene: &SceneGraph) -> Option<NodeId> {
174        if let Some(root) = scene.root {
175            self.hit_test_recursive(x, y, scene, root)
176        } else {
177            None
178        }
179    }
180
181    fn hit_test_recursive(
182        &self,
183        x: f32,
184        y: f32,
185        scene: &SceneGraph,
186        node_id: NodeId,
187    ) -> Option<NodeId> {
188        let node = scene.get(node_id);
189
190        if node.style.display == Display::None {
191            return None;
192        }
193
194        let layout = &node.layout;
195        // Account for scroll offset
196        let scroll_y = node.scroll_offset.1;
197        let in_bounds = x >= layout.x
198            && x <= layout.x + layout.width
199            && y >= layout.y
200            && y <= layout.y + layout.height;
201
202        if !in_bounds && node.style.overflow != Overflow::Visible {
203            return None;
204        }
205
206        // Adjust child hit coords for this node's scroll offset
207        let child_y = y + scroll_y;
208
209        // Check children in reverse order (last drawn = on top)
210        let children: Vec<NodeId> = node.children.clone();
211        for &child_id in children.iter().rev() {
212            if let Some(hit) = self.hit_test_recursive(x, child_y, scene, child_id) {
213                return Some(hit);
214            }
215        }
216
217        if in_bounds && !node.event_handlers.is_empty() {
218            return Some(node_id);
219        }
220
221        if in_bounds {
222            return Some(node_id);
223        }
224
225        None
226    }
227
228    fn find_scrollable(
229        &self,
230        x: f32,
231        y: f32,
232        scene: &SceneGraph,
233        node_id: NodeId,
234    ) -> Option<NodeId> {
235        let node = scene.get(node_id);
236        let layout = &node.layout;
237
238        let in_bounds = x >= layout.x
239            && x <= layout.x + layout.width
240            && y >= layout.y
241            && y <= layout.y + layout.height;
242
243        if !in_bounds {
244            return None;
245        }
246
247        // Check children first (deepest scrollable wins)
248        let children: Vec<NodeId> = node.children.clone();
249        for &child_id in children.iter().rev() {
250            if let Some(found) = self.find_scrollable(x, y, scene, child_id) {
251                return Some(found);
252            }
253        }
254
255        if node.style.overflow == Overflow::Scroll {
256            return Some(node_id);
257        }
258
259        None
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266
267    fn node(scene: &mut SceneGraph, tag: &str, rect: (f32, f32, f32, f32)) -> NodeId {
268        let id = scene.add_node(ElementKind::from_tag(tag), tag.to_string());
269        scene.get_mut(id).layout = LayoutRect {
270            x: rect.0,
271            y: rect.1,
272            width: rect.2,
273            height: rect.3,
274        };
275        id
276    }
277
278    fn on(scene: &mut SceneGraph, id: NodeId, event: &str, code: &str) {
279        scene
280            .get_mut(id)
281            .event_handlers
282            .insert(event.to_string(), code.to_string());
283    }
284
285    /// root 0,0 200x200 containing a 100x100 box at 50,50
286    fn simple_scene() -> (SceneGraph, NodeId, NodeId) {
287        let mut scene = SceneGraph::new();
288        let root = node(&mut scene, "div", (0.0, 0.0, 200.0, 200.0));
289        let inner = node(&mut scene, "div", (50.0, 50.0, 100.0, 100.0));
290        scene.add_child(root, inner);
291        (scene, root, inner)
292    }
293
294    #[test]
295    fn click_inside_a_child_hits_the_child() {
296        let (mut scene, _root, inner) = simple_scene();
297        on(&mut scene, inner, "click", "inner()");
298
299        let results = EventSystem::new().handle(&AppEvent::Click(100.0, 100.0), &mut scene);
300
301        assert_eq!(results.len(), 1);
302        assert_eq!(results[0].target, inner);
303        assert_eq!(results[0].event_type, "click");
304        assert_eq!(results[0].handler_code, "inner()");
305    }
306
307    #[test]
308    fn click_outside_the_child_falls_through_to_the_parent() {
309        let (mut scene, root, inner) = simple_scene();
310        on(&mut scene, inner, "click", "inner()");
311        on(&mut scene, root, "click", "root()");
312
313        let results = EventSystem::new().handle(&AppEvent::Click(10.0, 10.0), &mut scene);
314
315        assert_eq!(results.len(), 1);
316        assert_eq!(results[0].target, root);
317        assert_eq!(results[0].handler_code, "root()");
318    }
319
320    #[test]
321    fn click_bubbles_from_the_target_up_through_its_ancestors() {
322        let mut scene = SceneGraph::new();
323        let root = node(&mut scene, "div", (0.0, 0.0, 300.0, 300.0));
324        let mid = node(&mut scene, "div", (0.0, 0.0, 200.0, 200.0));
325        let leaf = node(&mut scene, "button", (0.0, 0.0, 100.0, 100.0));
326        scene.add_child(root, mid);
327        scene.add_child(mid, leaf);
328        on(&mut scene, root, "click", "root()");
329        on(&mut scene, mid, "click", "mid()");
330        on(&mut scene, leaf, "click", "leaf()");
331
332        let results = EventSystem::new().handle(&AppEvent::Click(10.0, 10.0), &mut scene);
333
334        // innermost handler first, then each ancestor
335        let codes: Vec<&str> = results.iter().map(|r| r.handler_code.as_str()).collect();
336        assert_eq!(codes, vec!["leaf()", "mid()", "root()"]);
337        // every result reports the hit node as the event target
338        assert!(results.iter().all(|r| r.target == leaf));
339    }
340
341    #[test]
342    fn a_click_that_misses_everything_produces_no_results() {
343        let (mut scene, root, inner) = simple_scene();
344        on(&mut scene, inner, "click", "inner()");
345        on(&mut scene, root, "click", "root()");
346
347        let results = EventSystem::new().handle(&AppEvent::Click(999.0, 999.0), &mut scene);
348
349        assert!(results.is_empty());
350    }
351
352    #[test]
353    fn the_last_of_two_overlapping_siblings_wins() {
354        let mut scene = SceneGraph::new();
355        let root = node(&mut scene, "div", (0.0, 0.0, 200.0, 200.0));
356        let under = node(&mut scene, "div", (0.0, 0.0, 100.0, 100.0));
357        let over = node(&mut scene, "div", (0.0, 0.0, 100.0, 100.0));
358        scene.add_child(root, under);
359        scene.add_child(root, over);
360        on(&mut scene, under, "click", "under()");
361        on(&mut scene, over, "click", "over()");
362
363        let results = EventSystem::new().handle(&AppEvent::Click(50.0, 50.0), &mut scene);
364
365        assert_eq!(results[0].target, over, "later siblings paint on top");
366        assert_eq!(results[0].handler_code, "over()");
367    }
368
369    #[test]
370    fn display_none_subtrees_are_not_hit_testable() {
371        let (mut scene, root, inner) = simple_scene();
372        scene.get_mut(inner).style.display = Display::None;
373        on(&mut scene, inner, "click", "inner()");
374        on(&mut scene, root, "click", "root()");
375
376        let results = EventSystem::new().handle(&AppEvent::Click(100.0, 100.0), &mut scene);
377
378        assert_eq!(results.len(), 1);
379        assert_eq!(results[0].target, root);
380    }
381
382    #[test]
383    fn hit_testing_is_inclusive_of_the_box_edges() {
384        let (mut scene, _root, inner) = simple_scene();
385        on(&mut scene, inner, "click", "inner()");
386
387        let mut sys = EventSystem::new();
388        // top-left and bottom-right corners of the 50,50 100x100 box
389        assert_eq!(
390            sys.handle(&AppEvent::Click(50.0, 50.0), &mut scene).len(),
391            1
392        );
393        assert_eq!(
394            sys.handle(&AppEvent::Click(150.0, 150.0), &mut scene).len(),
395            1
396        );
397        // one pixel past the right edge is the parent, which has no handler
398        assert!(sys
399            .handle(&AppEvent::Click(151.0, 150.0), &mut scene)
400            .is_empty());
401    }
402
403    #[test]
404    fn mouse_move_tracks_the_hovered_node() {
405        let (mut scene, root, inner) = simple_scene();
406        let mut sys = EventSystem::new();
407
408        sys.handle(&AppEvent::MouseMove(100.0, 100.0), &mut scene);
409        assert_eq!(sys.hover_node, Some(inner));
410
411        sys.handle(&AppEvent::MouseMove(5.0, 5.0), &mut scene);
412        assert_eq!(sys.hover_node, Some(root));
413
414        sys.handle(&AppEvent::MouseMove(-10.0, -10.0), &mut scene);
415        assert_eq!(sys.hover_node, None);
416    }
417
418    #[test]
419    fn hover_style_is_applied_on_enter_and_restored_on_leave() {
420        let (mut scene, _root, inner) = simple_scene();
421        let base = scene.get(inner).style.clone();
422        let mut hover = base.clone();
423        hover.background_color = Color::from_rgba(255, 0, 0, 1.0);
424        scene.get_mut(inner).base_style = Some(base.clone());
425        scene.get_mut(inner).hover_style = Some(hover);
426
427        let mut sys = EventSystem::new();
428        sys.handle(&AppEvent::MouseMove(100.0, 100.0), &mut scene);
429        assert!(scene.get(inner).is_hovered);
430        assert_eq!(scene.get(inner).style.background_color.r, 1.0);
431
432        sys.handle(&AppEvent::MouseMove(5.0, 5.0), &mut scene);
433        assert!(!scene.get(inner).is_hovered);
434        assert_eq!(
435            scene.get(inner).style.background_color,
436            base.background_color
437        );
438    }
439
440    #[test]
441    fn a_transition_defers_the_hover_style_instead_of_snapping() {
442        let (mut scene, _root, inner) = simple_scene();
443        let base = scene.get(inner).style.clone();
444        let mut hover = base.clone();
445        hover.background_color = Color::from_rgba(255, 0, 0, 1.0);
446        hover.transition_duration = 0.3;
447        scene.get_mut(inner).base_style = Some(base.clone());
448        scene.get_mut(inner).hover_style = Some(hover);
449
450        EventSystem::new().handle(&AppEvent::MouseMove(100.0, 100.0), &mut scene);
451
452        let node = scene.get(inner);
453        assert!(node.transition_start.is_some());
454        assert!(node.transition_to.is_some());
455        // the visible style is still the base one; tick_animations interpolates it
456        assert_eq!(node.style.background_color, base.background_color);
457    }
458
459    #[test]
460    fn mouseenter_and_mouseleave_handlers_fire_once_per_crossing() {
461        let (mut scene, _root, inner) = simple_scene();
462        on(&mut scene, inner, "mouseenter", "enter()");
463        on(&mut scene, inner, "mouseleave", "leave()");
464        let mut sys = EventSystem::new();
465
466        let enter = sys.handle(&AppEvent::MouseMove(100.0, 100.0), &mut scene);
467        assert_eq!(enter.len(), 1);
468        assert_eq!(enter[0].event_type, "mouseenter");
469
470        // moving within the same node fires nothing
471        assert!(sys
472            .handle(&AppEvent::MouseMove(110.0, 110.0), &mut scene)
473            .is_empty());
474
475        let leave = sys.handle(&AppEvent::MouseMove(5.0, 5.0), &mut scene);
476        assert_eq!(leave.len(), 1);
477        assert_eq!(leave[0].event_type, "mouseleave");
478        assert_eq!(leave[0].target, inner);
479    }
480
481    #[test]
482    fn scrolling_moves_a_scroll_container_and_clamps_at_both_ends() {
483        let mut scene = SceneGraph::new();
484        let root = node(&mut scene, "div", (0.0, 0.0, 200.0, 100.0));
485        scene.get_mut(root).style.overflow = Overflow::Scroll;
486        scene.get_mut(root).content_height = 400.0;
487
488        let mut sys = EventSystem::new();
489        sys.handle(&AppEvent::Scroll(50.0, 50.0, 0.0, 2.0), &mut scene);
490        assert_eq!(scene.get(root).scroll_offset.1, 60.0); // 2 lines * 30px
491
492        // cannot scroll above the top
493        sys.handle(&AppEvent::Scroll(50.0, 50.0, 0.0, -10.0), &mut scene);
494        assert_eq!(scene.get(root).scroll_offset.1, 0.0);
495
496        // cannot scroll past content_height - height
497        sys.handle(&AppEvent::Scroll(50.0, 50.0, 0.0, 100.0), &mut scene);
498        assert_eq!(scene.get(root).scroll_offset.1, 300.0);
499    }
500
501    #[test]
502    fn scrolling_targets_the_innermost_scrollable_ancestor() {
503        let mut scene = SceneGraph::new();
504        let outer = node(&mut scene, "div", (0.0, 0.0, 200.0, 200.0));
505        scene.get_mut(outer).style.overflow = Overflow::Scroll;
506        scene.get_mut(outer).content_height = 1000.0;
507        let inner = node(&mut scene, "div", (0.0, 0.0, 100.0, 100.0));
508        scene.get_mut(inner).style.overflow = Overflow::Scroll;
509        scene.get_mut(inner).content_height = 1000.0;
510        scene.add_child(outer, inner);
511
512        let mut sys = EventSystem::new();
513        sys.handle(&AppEvent::Scroll(50.0, 50.0, 0.0, 1.0), &mut scene);
514
515        assert_eq!(scene.get(inner).scroll_offset.1, 30.0);
516        assert_eq!(scene.get(outer).scroll_offset.1, 0.0);
517
518        // outside the inner box, the outer container scrolls instead
519        sys.handle(&AppEvent::Scroll(150.0, 150.0, 0.0, 1.0), &mut scene);
520        assert_eq!(scene.get(outer).scroll_offset.1, 30.0);
521    }
522
523    #[test]
524    fn a_non_scrollable_container_ignores_the_wheel() {
525        let mut scene = SceneGraph::new();
526        let root = node(&mut scene, "div", (0.0, 0.0, 200.0, 100.0));
527        scene.get_mut(root).content_height = 400.0;
528
529        EventSystem::new().handle(&AppEvent::Scroll(50.0, 50.0, 0.0, 3.0), &mut scene);
530
531        assert_eq!(scene.get(root).scroll_offset.1, 0.0);
532    }
533
534    #[test]
535    fn keypress_only_dispatches_to_the_focused_node() {
536        let (mut scene, _root, inner) = simple_scene();
537        on(&mut scene, inner, "keypress", "key()");
538        let mut sys = EventSystem::new();
539
540        assert!(sys
541            .handle(&AppEvent::KeyPress("a".into()), &mut scene)
542            .is_empty());
543
544        sys.focus_node = Some(inner);
545        let results = sys.handle(&AppEvent::KeyPress("a".into()), &mut scene);
546        assert_eq!(results.len(), 1);
547        assert_eq!(results[0].event_type, "keypress");
548        assert_eq!(results[0].target, inner);
549    }
550
551    #[test]
552    fn hit_testing_compensates_for_the_scroll_offset_of_a_container() {
553        let mut scene = SceneGraph::new();
554        let root = node(&mut scene, "div", (0.0, 0.0, 200.0, 100.0));
555        scene.get_mut(root).style.overflow = Overflow::Scroll;
556        // a child that has been scrolled up out of the visible box
557        let item = node(&mut scene, "div", (0.0, 120.0, 200.0, 40.0));
558        scene.add_child(root, item);
559        on(&mut scene, item, "click", "item()");
560
561        let mut sys = EventSystem::new();
562        // unscrolled: y=30 is inside the container but above the item
563        assert!(sys
564            .handle(&AppEvent::Click(10.0, 30.0), &mut scene)
565            .is_empty());
566
567        // scroll the container down so the item moves into view
568        scene.get_mut(root).scroll_offset.1 = 100.0;
569        let results = sys.handle(&AppEvent::Click(10.0, 30.0), &mut scene);
570        assert_eq!(results.len(), 1);
571        assert_eq!(results[0].target, item);
572    }
573}