Skip to main content

lumen_script/
event.rs

1//! Host-neutral event object, binding registry, and propagation driver for
2//! the dynamic DOM API (phase 4).
3//!
4//! `n.on("click", handler)` binds a handler to a node for an event type and
5//! returns an off token; `off()` unbinds. Bindings live in a process-global
6//! registry keyed by token so the generic dispatcher (and the C-ABI, which
7//! has no script host) share one source of truth. The handler itself is
8//! either a closure held by the active [`crate::ScriptHost`] (keyed by the
9//! same token) or a native Rust callback (the C-ABI / SDK path).
10//!
11//! During dispatch the driver publishes the event into a process-global
12//! current-event cell; a handler reads target / position / key / modifiers /
13//! button / delta / value from it and mutates its `prevent_default` /
14//! `stop_propagation` / `stop_immediate_propagation` flags through the free
15//! functions here. A host may wrap those in a registered `Event` handle or
16//! call them procedurally, as the C-ABI does.
17//!
18//! Propagation follows the DOM contract: capture from the root down to the
19//! target, dispatch at the target, then bubble back up (for events that
20//! bubble). `stop_propagation` halts movement to the next node after the
21//! current node's handlers finish; `stop_immediate_propagation` also halts
22//! the remaining handlers on the current node.
23
24use std::collections::HashSet;
25use std::sync::atomic::{AtomicU64, Ordering};
26use std::sync::{Arc, OnceLock, RwLock};
27
28/// The full event payload delivered to a handler. Positional / key / button /
29/// delta / value fields carry meaning only for the event types that produce
30/// them (a `keydown` has `key` + modifiers, a `wheel` has `delta`, an
31/// `input` / `change` has `value`, and so on); unused fields stay at their
32/// defaults.
33#[derive(Clone, Debug, Default)]
34pub struct EventData {
35    /// Event type name (`"click"`, `"keydown"`, `"wheel"`, ...).
36    pub event_type: String,
37    /// The node the event originally targeted (packed handle).
38    pub target: u64,
39    /// The node whose handler is currently running (packed handle). The
40    /// driver updates this as it walks the propagation path.
41    pub current_target: u64,
42    /// Pointer position relative to the target's top-left, logical pixels.
43    pub local: (f64, f64),
44    /// Pointer position in window (client) coordinates, logical pixels.
45    pub client: (f64, f64),
46    /// Logical key for keyboard events (`"a"`, `"Enter"`, `"ArrowLeft"`).
47    pub key: String,
48    /// Shift held.
49    pub shift: bool,
50    /// Control held.
51    pub ctrl: bool,
52    /// Alt / Option held.
53    pub alt: bool,
54    /// Super / Cmd / Windows held.
55    pub super_: bool,
56    /// Pointer button: `0` primary, `1` middle, `2` secondary, `-1` none.
57    pub button: i64,
58    /// Wheel scroll delta, logical pixels.
59    pub delta: (f64, f64),
60    /// Text value for `input` / `change` events.
61    pub value: String,
62    /// Set by `prevent_default`: the event's default action is cancelled.
63    pub default_prevented: bool,
64    /// Set by `stop_propagation`: no further nodes receive the event.
65    pub propagation_stopped: bool,
66    /// Set by `stop_immediate_propagation`: no further handlers run at all.
67    pub immediate_stopped: bool,
68}
69
70// ---------------------------------------------------------------------------
71// Current-event cell + accessors (read + mutated by handlers).
72// ---------------------------------------------------------------------------
73
74static CURRENT_EVENT: OnceLock<RwLock<EventData>> = OnceLock::new();
75
76fn current_cell() -> &'static RwLock<EventData> {
77    CURRENT_EVENT.get_or_init(|| RwLock::new(EventData::default()))
78}
79
80/// Publish `data` as the current event before invoking handlers.
81pub fn set_current_event(data: EventData) {
82    if let Ok(mut g) = current_cell().write() {
83        *g = data;
84    }
85}
86
87/// Read the current event (cheap clone).
88pub fn current_event() -> EventData {
89    current_cell().read().map(|g| g.clone()).unwrap_or_default()
90}
91
92/// Update the current event's `current_target` as the driver moves along
93/// the propagation path.
94pub fn set_current_target(node: u64) {
95    if let Ok(mut g) = current_cell().write() {
96        g.current_target = node;
97    }
98}
99
100/// `ev.target()`.
101pub fn event_target() -> u64 {
102    current_cell().read().map(|g| g.target).unwrap_or(0)
103}
104
105/// `ev.current_target()`.
106pub fn event_current_target() -> u64 {
107    current_cell().read().map(|g| g.current_target).unwrap_or(0)
108}
109
110/// `ev.type()`.
111pub fn event_type() -> String {
112    current_cell()
113        .read()
114        .map(|g| g.event_type.clone())
115        .unwrap_or_default()
116}
117
118/// Local position `(x, y)` (relative to the target).
119pub fn event_position_local() -> (f64, f64) {
120    current_cell().read().map(|g| g.local).unwrap_or((0.0, 0.0))
121}
122
123/// Client position `(x, y)` (window coordinates).
124pub fn event_position_client() -> (f64, f64) {
125    current_cell()
126        .read()
127        .map(|g| g.client)
128        .unwrap_or((0.0, 0.0))
129}
130
131/// `ev.key()`.
132pub fn event_key() -> String {
133    current_cell()
134        .read()
135        .map(|g| g.key.clone())
136        .unwrap_or_default()
137}
138
139/// `ev.modifiers()` as `(shift, ctrl, alt, super)`.
140pub fn event_modifiers() -> (bool, bool, bool, bool) {
141    current_cell()
142        .read()
143        .map(|g| (g.shift, g.ctrl, g.alt, g.super_))
144        .unwrap_or((false, false, false, false))
145}
146
147/// `ev.button()`.
148pub fn event_button() -> i64 {
149    current_cell().read().map(|g| g.button).unwrap_or(-1)
150}
151
152/// `ev.delta()` as `(x, y)`.
153pub fn event_delta() -> (f64, f64) {
154    current_cell().read().map(|g| g.delta).unwrap_or((0.0, 0.0))
155}
156
157/// `ev.value()`.
158pub fn event_value() -> String {
159    current_cell()
160        .read()
161        .map(|g| g.value.clone())
162        .unwrap_or_default()
163}
164
165/// `ev.prevent_default()`.
166pub fn event_prevent_default() {
167    if let Ok(mut g) = current_cell().write() {
168        g.default_prevented = true;
169    }
170}
171
172/// `ev.stop_propagation()`.
173pub fn event_stop_propagation() {
174    if let Ok(mut g) = current_cell().write() {
175        g.propagation_stopped = true;
176    }
177}
178
179/// `ev.stop_immediate_propagation()`. Implies `stop_propagation`.
180pub fn event_stop_immediate_propagation() {
181    if let Ok(mut g) = current_cell().write() {
182        g.propagation_stopped = true;
183        g.immediate_stopped = true;
184    }
185}
186
187fn is_propagation_stopped() -> bool {
188    current_cell()
189        .read()
190        .map(|g| g.propagation_stopped)
191        .unwrap_or(false)
192}
193
194fn is_immediate_stopped() -> bool {
195    current_cell()
196        .read()
197        .map(|g| g.immediate_stopped)
198        .unwrap_or(false)
199}
200
201// ---------------------------------------------------------------------------
202// Binding registry.
203// ---------------------------------------------------------------------------
204
205/// How a bound handler is invoked at dispatch time.
206#[derive(Clone)]
207pub enum EventHandler {
208    /// A closure held by the active [`crate::ScriptHost`], keyed by the
209    /// binding's token. The generic dispatcher calls back into the host.
210    Host,
211    /// A native Rust callback (C-ABI / Rust SDK). Invoked directly; it reads
212    /// the current event via the accessor free functions above.
213    Native(Arc<dyn Fn() + Send + Sync>),
214}
215
216impl std::fmt::Debug for EventHandler {
217    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
218        match self {
219            EventHandler::Host => f.write_str("Host"),
220            EventHandler::Native(_) => f.write_str("Native"),
221        }
222    }
223}
224
225/// One `n.on(type, handler)` binding.
226#[derive(Clone, Debug)]
227pub struct EventBinding {
228    /// Unbind token (also the host's closure key).
229    pub token: u64,
230    /// Bound node (packed handle).
231    pub node: u64,
232    /// Event type this binding listens for.
233    pub event_type: String,
234    /// `true` = capture-phase listener; `false` = bubble / target listener.
235    pub capture: bool,
236    /// How to invoke the handler.
237    pub handler: EventHandler,
238}
239
240static EVENT_TOKEN: AtomicU64 = AtomicU64::new(1);
241static BINDINGS: OnceLock<RwLock<Vec<EventBinding>>> = OnceLock::new();
242
243fn bindings() -> &'static RwLock<Vec<EventBinding>> {
244    BINDINGS.get_or_init(|| RwLock::new(Vec::new()))
245}
246
247/// Mint a fresh, process-unique bind / off token (always `>= 1`).
248pub fn mint_event_token() -> u64 {
249    EVENT_TOKEN.fetch_add(1, Ordering::Relaxed)
250}
251
252/// Register a host-closure binding (token, node, type, capture). The host
253/// holds the closure itself keyed by `token`.
254pub fn register_host_binding(token: u64, node: u64, event_type: String, capture: bool) {
255    register(EventBinding {
256        token,
257        node,
258        event_type,
259        capture,
260        handler: EventHandler::Host,
261    });
262}
263
264/// Register a native-callback binding (the C-ABI / SDK path). Returns the
265/// token for the caller to hand back as the off token.
266pub fn register_native_binding(
267    node: u64,
268    event_type: String,
269    capture: bool,
270    callback: Arc<dyn Fn() + Send + Sync>,
271) -> u64 {
272    let token = mint_event_token();
273    register(EventBinding {
274        token,
275        node,
276        event_type,
277        capture,
278        handler: EventHandler::Native(callback),
279    });
280    token
281}
282
283fn register(binding: EventBinding) {
284    if let Ok(mut b) = bindings().write() {
285        // A re-registered token replaces its prior binding (idempotent).
286        b.retain(|e| e.token != binding.token);
287        b.push(binding);
288    }
289}
290
291/// Remove the binding for `token`, returning it when present.
292pub fn unregister_binding(token: u64) -> Option<EventBinding> {
293    let mut b = bindings().write().ok()?;
294    let idx = b.iter().position(|e| e.token == token)?;
295    Some(b.remove(idx))
296}
297
298/// Drop every host-closure binding. Called on host reset, and on hot reload
299/// before the script re-runs and re-binds.
300pub fn clear_host_bindings() {
301    if let Ok(mut b) = bindings().write() {
302        b.retain(|e| !matches!(e.handler, EventHandler::Host));
303    }
304}
305
306/// Remove every host-closure binding and return it, in registration order.
307///
308/// A hot reload takes the bindings out before it re-runs the script, then
309/// hands the snapshot to [`restore_host_bindings`] so the ones the new script
310/// did not re-bind keep firing. Native bindings are left in place; they belong
311/// to the C-ABI / SDK caller, not to the script.
312pub fn take_host_bindings() -> Vec<EventBinding> {
313    let Ok(mut b) = bindings().write() else {
314        return Vec::new();
315    };
316    let mut taken = Vec::new();
317    b.retain(|e| {
318        if matches!(e.handler, EventHandler::Host) {
319            taken.push(e.clone());
320            false
321        } else {
322            true
323        }
324    });
325    taken
326}
327
328/// Carry `prior` bindings forward past a hot reload, and report the tokens
329/// that were dropped instead.
330///
331/// A prior binding is dropped when the reloaded script already bound the same
332/// node, event type, and phase; the new binding is the one that matches the
333/// new source. Every other prior binding is re-registered, so a handler bound
334/// from `on_start` (which the runtime fires once, at app construction) still
335/// fires after a reload. The returned tokens are the dropped ones, for the
336/// caller to purge from its own handler map.
337///
338/// Carried bindings go back in front of the new ones, preserving the order
339/// handlers ran in before the reload.
340pub fn restore_host_bindings(prior: Vec<EventBinding>) -> Vec<u64> {
341    let Ok(mut b) = bindings().write() else {
342        return Vec::new();
343    };
344    let mut dropped = Vec::new();
345    let mut carried = Vec::new();
346    for e in prior {
347        let superseded = b
348            .iter()
349            .any(|n| n.node == e.node && n.event_type == e.event_type && n.capture == e.capture);
350        if superseded {
351            dropped.push(e.token);
352        } else {
353            carried.push(e);
354        }
355    }
356    carried.append(&mut b);
357    *b = carried;
358    dropped
359}
360
361/// Drop all bindings (mainly for test isolation).
362pub fn clear_all_bindings() {
363    if let Ok(mut b) = bindings().write() {
364        b.clear();
365    }
366}
367
368/// Snapshot of the bindings for `event_type`, in registration order.
369fn bindings_for(event_type: &str) -> Vec<EventBinding> {
370    bindings()
371        .read()
372        .map(|b| {
373            b.iter()
374                .filter(|e| e.event_type == event_type)
375                .cloned()
376                .collect()
377        })
378        .unwrap_or_default()
379}
380
381/// Whether any binding exists for `event_type` (fast dispatch gate).
382pub fn has_bindings_for(event_type: &str) -> bool {
383    bindings()
384        .read()
385        .map(|b| b.iter().any(|e| e.event_type == event_type))
386        .unwrap_or(false)
387}
388
389// ---------------------------------------------------------------------------
390// Default-action coordination.
391//
392// `click`'s default action is link navigation (a `<a href>` click navigates
393// the active page). The nav executor lives in the runtime's
394// `navigate_on_anchor_click`; the phase-4 dispatcher records here which click
395// targets had their default cancelled so that executor can skip them. The set
396// is cleared at the start of every dispatch pass.
397// ---------------------------------------------------------------------------
398
399static PREVENTED_CLICKS: OnceLock<RwLock<HashSet<u64>>> = OnceLock::new();
400
401fn prevented_clicks() -> &'static RwLock<HashSet<u64>> {
402    PREVENTED_CLICKS.get_or_init(|| RwLock::new(HashSet::new()))
403}
404
405/// Reset the per-tick prevented-click set. The dispatcher calls this before
406/// processing a tick's events.
407pub fn clear_prevented_clicks() {
408    if let Ok(mut s) = prevented_clicks().write() {
409        s.clear();
410    }
411}
412
413/// Record that a click on `target` (packed handle) had its default action
414/// cancelled via `prevent_default`.
415pub fn mark_prevented_click(target: u64) {
416    if let Ok(mut s) = prevented_clicks().write() {
417        s.insert(target);
418    }
419}
420
421/// Whether a click on `target` (packed handle) was default-prevented this
422/// tick. The anchor-navigation executor consults this to honor
423/// `prevent_default`.
424pub fn is_click_default_prevented(target: u64) -> bool {
425    prevented_clicks()
426        .read()
427        .map(|s| s.contains(&target))
428        .unwrap_or(false)
429}
430
431// ---------------------------------------------------------------------------
432// Event-type metadata.
433// ---------------------------------------------------------------------------
434
435/// Whether `event_type` bubbles (fires bubble-phase handlers on ancestors
436/// after the target). Mirrors the DOM: `focus` / `blur`, pointer
437/// `enter` / `leave`, and `scroll` do not bubble; everything else does.
438pub fn event_bubbles(event_type: &str) -> bool {
439    !matches!(
440        event_type,
441        "focus" | "blur" | "pointerenter" | "pointerleave" | "scroll"
442    )
443}
444
445// ---------------------------------------------------------------------------
446// Propagation driver.
447// ---------------------------------------------------------------------------
448
449#[derive(Clone, Copy, PartialEq)]
450enum Phase {
451    Capture,
452    Target,
453    Bubble,
454}
455
456/// Drive a full capture -> target -> bubble dispatch for `data`.
457///
458/// `ancestors_root_first` is the target's ancestor chain packed-handle list,
459/// ordered root-first and EXCLUDING the target itself. Capture handlers on
460/// those ancestors fire top-down, then all handlers on the target, then (for
461/// a bubbling event) bubble handlers on the ancestors bottom-up. `invoke_host`
462/// runs a host-closure binding by token; native bindings are invoked
463/// directly. Returns the final [`EventData`] so the caller can read the
464/// `default_prevented` flag and apply (or skip) the default action.
465pub fn dispatch(
466    data: EventData,
467    ancestors_root_first: &[u64],
468    bubbles: bool,
469    mut invoke_host: impl FnMut(u64),
470) -> EventData {
471    let etype = data.event_type.clone();
472    let target = data.target;
473    set_current_event(data);
474
475    let all = bindings_for(&etype);
476    if all.is_empty() {
477        return current_event();
478    }
479
480    let mut plan: Vec<(u64, Phase)> = Vec::new();
481    for &n in ancestors_root_first {
482        plan.push((n, Phase::Capture));
483    }
484    plan.push((target, Phase::Target));
485    if bubbles {
486        for &n in ancestors_root_first.iter().rev() {
487            plan.push((n, Phase::Bubble));
488        }
489    }
490
491    'outer: for (node, phase) in plan {
492        set_current_target(node);
493        for b in all.iter().filter(|b| b.node == node) {
494            let want = match phase {
495                Phase::Capture => b.capture,
496                Phase::Bubble => !b.capture,
497                Phase::Target => true,
498            };
499            if !want {
500                continue;
501            }
502            match &b.handler {
503                EventHandler::Native(f) => f(),
504                EventHandler::Host => invoke_host(b.token),
505            }
506            if is_immediate_stopped() {
507                break 'outer;
508            }
509        }
510        if is_propagation_stopped() {
511            break;
512        }
513    }
514
515    current_event()
516}
517
518/// Serialises the process-global binding registry and current-event cell
519/// across every test that touches them: the ones in this module and the
520/// dispatch tests in [`crate::dom_events`]. One lock, or a
521/// `clear_all_bindings` in one module wipes what another just registered.
522#[cfg(test)]
523pub(crate) static TEST_GUARD: std::sync::Mutex<()> = std::sync::Mutex::new(());
524
525#[cfg(test)]
526mod tests {
527    use super::TEST_GUARD as GUARD;
528    use super::*;
529    use std::sync::Mutex;
530
531    fn record(log: &Arc<Mutex<Vec<String>>>, tag: &'static str) -> Arc<dyn Fn() + Send + Sync> {
532        let log = log.clone();
533        Arc::new(move || log.lock().unwrap().push(tag.to_string()))
534    }
535
536    #[test]
537    fn native_handler_fires_with_event_fields() {
538        let _g = GUARD.lock().unwrap();
539        clear_all_bindings();
540        let seen = Arc::new(Mutex::new((0u64, String::new())));
541        let s = seen.clone();
542        let cb: Arc<dyn Fn() + Send + Sync> = Arc::new(move || {
543            *s.lock().unwrap() = (event_target(), event_key());
544        });
545        register_native_binding(10, "keydown".into(), false, cb);
546        let data = EventData {
547            event_type: "keydown".into(),
548            target: 10,
549            key: "Enter".into(),
550            ..Default::default()
551        };
552        dispatch(data, &[], event_bubbles("keydown"), |_| {});
553        assert_eq!(*seen.lock().unwrap(), (10, "Enter".to_string()));
554        clear_all_bindings();
555    }
556
557    #[test]
558    fn capture_then_target_then_bubble_order() {
559        let _g = GUARD.lock().unwrap();
560        clear_all_bindings();
561        let log = Arc::new(Mutex::new(Vec::new()));
562        // Tree: root(1) -> mid(2) -> leaf(3). Bind capture + bubble at each.
563        register_native_binding(1, "click".into(), true, record(&log, "root-cap"));
564        register_native_binding(1, "click".into(), false, record(&log, "root-bub"));
565        register_native_binding(2, "click".into(), true, record(&log, "mid-cap"));
566        register_native_binding(2, "click".into(), false, record(&log, "mid-bub"));
567        register_native_binding(3, "click".into(), true, record(&log, "leaf-cap"));
568        register_native_binding(3, "click".into(), false, record(&log, "leaf-bub"));
569        let data = EventData {
570            event_type: "click".into(),
571            target: 3,
572            ..Default::default()
573        };
574        dispatch(data, &[1, 2], true, |_| {});
575        assert_eq!(
576            *log.lock().unwrap(),
577            vec![
578                "root-cap", "mid-cap", // capture down
579                "leaf-cap", "leaf-bub", // target (registration order)
580                "mid-bub", "root-bub", // bubble up
581            ]
582        );
583        clear_all_bindings();
584    }
585
586    #[test]
587    fn stop_propagation_halts_after_current_node() {
588        let _g = GUARD.lock().unwrap();
589        clear_all_bindings();
590        let log = Arc::new(Mutex::new(Vec::new()));
591        let l = log.clone();
592        register_native_binding(
593            2,
594            "click".into(),
595            false,
596            Arc::new(move || {
597                l.lock().unwrap().push("mid".to_string());
598                event_stop_propagation();
599            }),
600        );
601        register_native_binding(1, "click".into(), false, record(&log, "root"));
602        let data = EventData {
603            event_type: "click".into(),
604            target: 2,
605            ..Default::default()
606        };
607        dispatch(data, &[1], true, |_| {});
608        // mid runs, stops propagation; root (bubble) never runs.
609        assert_eq!(*log.lock().unwrap(), vec!["mid"]);
610        clear_all_bindings();
611    }
612
613    #[test]
614    fn stop_immediate_halts_remaining_on_same_node() {
615        let _g = GUARD.lock().unwrap();
616        clear_all_bindings();
617        let log = Arc::new(Mutex::new(Vec::new()));
618        let l = log.clone();
619        register_native_binding(
620            5,
621            "click".into(),
622            false,
623            Arc::new(move || {
624                l.lock().unwrap().push("first".to_string());
625                event_stop_immediate_propagation();
626            }),
627        );
628        register_native_binding(5, "click".into(), false, record(&log, "second"));
629        let data = EventData {
630            event_type: "click".into(),
631            target: 5,
632            ..Default::default()
633        };
634        dispatch(data, &[], true, |_| {});
635        assert_eq!(*log.lock().unwrap(), vec!["first"]);
636        clear_all_bindings();
637    }
638
639    #[test]
640    fn prevent_default_records_on_returned_event() {
641        let _g = GUARD.lock().unwrap();
642        clear_all_bindings();
643        register_native_binding(7, "click".into(), false, Arc::new(event_prevent_default));
644        let data = EventData {
645            event_type: "click".into(),
646            target: 7,
647            ..Default::default()
648        };
649        let out = dispatch(data, &[], true, |_| {});
650        assert!(out.default_prevented);
651        clear_all_bindings();
652    }
653
654    #[test]
655    fn unbind_stops_delivery() {
656        let _g = GUARD.lock().unwrap();
657        clear_all_bindings();
658        let log = Arc::new(Mutex::new(Vec::new()));
659        let tok = register_native_binding(9, "click".into(), false, record(&log, "hit"));
660        assert!(unregister_binding(tok).is_some());
661        let data = EventData {
662            event_type: "click".into(),
663            target: 9,
664            ..Default::default()
665        };
666        dispatch(data, &[], true, |_| {});
667        assert!(log.lock().unwrap().is_empty());
668        clear_all_bindings();
669    }
670
671    #[test]
672    fn non_bubbling_event_skips_ancestors() {
673        let _g = GUARD.lock().unwrap();
674        clear_all_bindings();
675        let log = Arc::new(Mutex::new(Vec::new()));
676        register_native_binding(1, "focus".into(), false, record(&log, "root-bub"));
677        register_native_binding(2, "focus".into(), false, record(&log, "target"));
678        let data = EventData {
679            event_type: "focus".into(),
680            target: 2,
681            ..Default::default()
682        };
683        dispatch(data, &[1], event_bubbles("focus"), |_| {});
684        // focus does not bubble: only the target handler runs.
685        assert_eq!(*log.lock().unwrap(), vec!["target"]);
686        clear_all_bindings();
687    }
688}