Skip to main content

rdom_core/
event.rs

1//! `Event` — display-agnostic event type, honoring the DOM
2//! `stop_propagation` / `stop_immediate_propagation` / `prevent_default`
3//! flags. Concrete payloads (KeyEvent, MouseEvent, render context) belong
4//! in `rdom-tui`; this core type carries just the routing state.
5//!
6//! Spec: <https://dom.spec.whatwg.org/#events>
7
8use crate::event_detail::EventDetail;
9use crate::node_id::NodeId;
10
11/// Which phase of dispatch is currently running.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub enum EventPhase {
14    /// No dispatch in progress.
15    None,
16    /// Descending from root toward `target`. Capture-mode listeners fire
17    /// on ancestors.
18    Capturing,
19    /// At the target node. Both capture and bubble listeners fire.
20    AtTarget,
21    /// Ascending from `target` back to root. Non-capture listeners fire
22    /// on ancestors.
23    Bubbling,
24}
25
26/// Minimal event — just routing state. Attach payload via a typed
27/// wrapper in `rdom-tui` or the caller's crate.
28///
29/// Users typically build one with `Event::new("click")`, optionally
30/// call `with_bubbles(false)` / `with_cancelable(true)`, then pass to
31/// `Dom::dispatch_event(target, &mut event)`.
32///
33/// # Cloning
34///
35/// `clone()` is the web's `new Event(e.type, e)`: a fresh,
36/// undispatched event. It copies `event_type`, the init flags
37/// (`bubbles`, `cancelable`) and `detail`; everything a dispatch
38/// writes starts over — `target` and `current_target` are `None`,
39/// `phase` is [`EventPhase::None`], the stop-propagation and canceled
40/// flags are clear, the dispatch flag is unset and
41/// [`redraw_requested`](Event::redraw_requested) is `false`. A copy
42/// is script-made, so it is not [synthetic](Event::is_synthetic)
43/// (a scripted copy has `isTrusted` false on the web). A clone taken
44/// inside a listener can therefore be dispatched or queued; the
45/// in-flight original still cannot (DOM §2.9 step 1).
46///
47/// `Event` has no `timeStamp`, so there is no creation time to copy
48/// or restart; should one be added, a copy takes its own creation
49/// time, as `new Event()` does (DOM §2.2).
50///
51/// Read what a listener needs from the dispatch state (`target`,
52/// `phase`, `default_prevented()`) inside the listener: a clone does
53/// not carry it.
54#[derive(Debug)]
55#[non_exhaustive]
56pub struct Event {
57    /// Event type string — "click", "input", etc. Case-sensitive.
58    pub event_type: String,
59    /// Whether the event bubbles after the target. Default: true.
60    pub bubbles: bool,
61    /// Whether `prevent_default` has meaning for this event. Default: true.
62    pub cancelable: bool,
63    /// The node where dispatch was initiated. Set by `dispatch_event`;
64    /// callers don't need to fill this in.
65    pub target: Option<NodeId>,
66    /// The node currently being visited in dispatch. Updated per node
67    /// so handlers see it.
68    pub current_target: Option<NodeId>,
69    /// Current phase of dispatch.
70    pub phase: EventPhase,
71    /// Typed payload for event types that carry semantic data.
72    /// [`EventDetail::None`] for events that don't carry detail
73    /// (default on `Event::new`); [`EventDetail::String`] for
74    /// `CustomEvent`-style ad-hoc author payloads; typed variants
75    /// for events with structured payloads (transitions, inputs,
76    /// submits, toggles, mouse, keyboard). Listeners read via the
77    /// `as_*` accessors on [`EventDetail`].
78    pub detail: EventDetail,
79
80    /// `true` when the event was synthesized by the runtime (as
81    /// opposed to originating from user input or an explicit
82    /// `dispatch_event` call from application code). Higher layers
83    /// use this to suppress default actions on events they
84    /// themselves created — preventing recursion (e.g., a runtime
85    /// that dispatches synthetic `click` after `mouseup`, then
86    /// would recursively try to dispatch another `click` as that
87    /// event's default action).
88    ///
89    /// Spec-faithful analog to the browser's
90    /// `Event.isTrusted` flag, inverted: browsers set `isTrusted =
91    /// true` for user-originated events and `false` for scripted
92    /// ones; we set `is_synthetic = true` for runtime-originated
93    /// events, which is the flag that's actually useful to
94    /// dispatch logic. The difference is semantic, not
95    /// behavioral.
96    pub(crate) is_synthetic: bool,
97
98    pub(crate) propagation_stopped: bool,
99    pub(crate) immediate_propagation_stopped: bool,
100    pub(crate) default_prevented: bool,
101    /// DOM "dispatch flag": set for the duration of `dispatch_event`.
102    /// Re-dispatching an in-flight event is `InvalidStateError` on the
103    /// web; here it returns `DomError::InvalidState`. Cleared by a drop
104    /// guard on every way out of `dispatch_event`, a listener panic
105    /// unwinding through included (`P7G-DISPATCH-FLAG-1`).
106    pub(crate) dispatching: bool,
107
108    /// Set by [`EventCtx::request_redraw`](crate::EventCtx::request_redraw)
109    /// when a listener mutated state that the host should repaint —
110    /// state the DOM mutation tracker can't see (e.g. a `<canvas>` whose
111    /// paint reads external app state). Accumulates across every listener
112    /// in the dispatch. **`rdom-core` never acts on this** — it's an
113    /// inert intent flag the rendering host reads after dispatch (the
114    /// `rdom-tui` runtime ORs it into its repaint decision). Read via
115    /// [`Event::redraw_requested`].
116    pub(crate) redraw_requested: bool,
117}
118
119/// DOM §2.2 `new Event(e.type, e)` — see [`Event`]'s "Cloning".
120impl Clone for Event {
121    fn clone(&self) -> Self {
122        let mut copy = Event::new(self.event_type.clone())
123            .with_bubbles(self.bubbles)
124            .with_cancelable(self.cancelable);
125        copy.detail = self.detail.clone();
126        copy
127    }
128}
129
130impl Event {
131    pub fn new(event_type: impl Into<String>) -> Self {
132        Self {
133            event_type: event_type.into(),
134            bubbles: true,
135            cancelable: true,
136            target: None,
137            current_target: None,
138            phase: EventPhase::None,
139            detail: EventDetail::None,
140            is_synthetic: false,
141            propagation_stopped: false,
142            immediate_propagation_stopped: false,
143            default_prevented: false,
144            dispatching: false,
145            redraw_requested: false,
146        }
147    }
148
149    pub fn with_bubbles(mut self, bubbles: bool) -> Self {
150        self.bubbles = bubbles;
151        self
152    }
153
154    pub fn with_cancelable(mut self, cancelable: bool) -> Self {
155        self.cancelable = cancelable;
156        self
157    }
158
159    /// Builder-style `detail` setter for string payloads —
160    /// `Event::new("custom").with_detail("hello")`. Produces an
161    /// [`EventDetail::String`]; for typed variants set
162    /// `event.detail` directly to the relevant variant.
163    pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
164        self.detail = EventDetail::String(detail.into());
165        self
166    }
167
168    /// Mark this event as synthesized by the runtime. Default is
169    /// `false` (not synthesized). Use when composing higher-level
170    /// events from lower-level ones — e.g., the runtime creates a
171    /// synthetic `click` after matching `mousedown`+`mouseup`, so
172    /// handlers firing during `click` can distinguish it from a
173    /// handler-scripted `dispatch_event("click", ...)`.
174    pub fn with_synthetic(mut self, synthetic: bool) -> Self {
175        self.is_synthetic = synthetic;
176        self
177    }
178
179    /// `true` iff this event was synthesized by the runtime. See
180    /// [`Event::with_synthetic`].
181    pub fn is_synthetic(&self) -> bool {
182        self.is_synthetic
183    }
184
185    /// Stop bubbling/capturing on subsequent nodes. Listeners still
186    /// registered at the current node *and phase* continue to fire (see
187    /// `stop_immediate_propagation` for the harder stop); at the target,
188    /// calling this from a capture listener suppresses the target's
189    /// bubble listeners, which belong to the next pass. The flag is
190    /// cleared when the dispatch ends (DOM §2.9 step 5.9).
191    pub fn stop_propagation(&mut self) {
192        self.propagation_stopped = true;
193    }
194
195    /// Stop this event immediately: no further listeners run on this
196    /// node, no further propagation.
197    pub fn stop_immediate_propagation(&mut self) {
198        self.propagation_stopped = true;
199        self.immediate_propagation_stopped = true;
200    }
201
202    /// Signal "please skip the default action". Only meaningful if
203    /// `cancelable` is true. The Dom itself has no notion of "default
204    /// action"; higher layers check `default_prevented()` to decide.
205    pub fn prevent_default(&mut self) {
206        if self.cancelable {
207            self.default_prevented = true;
208        }
209    }
210
211    /// `true` if any listener called
212    /// [`EventCtx::request_redraw`](crate::EventCtx::request_redraw)
213    /// during this dispatch. The rendering host reads this after
214    /// `dispatch_event` returns to decide whether to repaint.
215    pub fn redraw_requested(&self) -> bool {
216        self.redraw_requested
217    }
218
219    pub fn is_propagation_stopped(&self) -> bool {
220        self.propagation_stopped
221    }
222
223    pub fn is_immediate_propagation_stopped(&self) -> bool {
224        self.immediate_propagation_stopped
225    }
226
227    pub fn default_prevented(&self) -> bool {
228        self.default_prevented
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    #[test]
237    fn event_defaults() {
238        let e = Event::new("click");
239        assert_eq!(e.event_type, "click");
240        assert!(e.bubbles);
241        assert!(e.cancelable);
242        assert_eq!(e.phase, EventPhase::None);
243        assert!(!e.is_propagation_stopped());
244        assert!(!e.default_prevented());
245    }
246
247    #[test]
248    fn stop_propagation_sets_flag() {
249        let mut e = Event::new("click");
250        e.stop_propagation();
251        assert!(e.is_propagation_stopped());
252        assert!(!e.is_immediate_propagation_stopped());
253    }
254
255    #[test]
256    fn stop_immediate_sets_both_flags() {
257        let mut e = Event::new("click");
258        e.stop_immediate_propagation();
259        assert!(e.is_propagation_stopped());
260        assert!(e.is_immediate_propagation_stopped());
261    }
262
263    #[test]
264    fn prevent_default_only_when_cancelable() {
265        let mut e = Event::new("click");
266        e.prevent_default();
267        assert!(e.default_prevented());
268
269        let mut e2 = Event::new("click").with_cancelable(false);
270        e2.prevent_default();
271        assert!(!e2.default_prevented());
272    }
273
274    #[test]
275    fn synthetic_default_is_false() {
276        let e = Event::new("click");
277        assert!(!e.is_synthetic());
278    }
279
280    #[test]
281    fn with_synthetic_sets_flag() {
282        let e = Event::new("click").with_synthetic(true);
283        assert!(e.is_synthetic());
284
285        let e2 = Event::new("click").with_synthetic(false);
286        assert!(!e2.is_synthetic());
287    }
288
289    #[test]
290    fn synthetic_flag_independent_of_other_state() {
291        // Synthetic is orthogonal to bubbling/cancelable/propagation.
292        let mut e = Event::new("click")
293            .with_synthetic(true)
294            .with_bubbles(false)
295            .with_cancelable(false);
296        e.stop_propagation();
297        e.prevent_default();
298        assert!(e.is_synthetic());
299        assert!(!e.bubbles);
300        assert!(!e.cancelable);
301        assert!(e.is_propagation_stopped());
302        assert!(!e.default_prevented()); // cancelable=false blocks prevent_default
303    }
304
305    // ── Clone is `new Event(e.type, e)` (P7G-EVENT-CLONE-1) ──────────
306
307    use crate::dispatch::ListenerOptions;
308    use crate::{Dom, DomError, EventDetail};
309    use std::cell::RefCell;
310    use std::rc::Rc;
311
312    /// DOM §2.2 / §2.9: a copy is a fresh event — type, init flags and
313    /// detail carry over; the dispatch flag, target, current target,
314    /// phase and the stop / canceled flags do not.
315    #[test]
316    fn clone_taken_mid_dispatch_is_a_fresh_undispatched_event() {
317        let mut dom: Dom = Dom::new();
318        let root = dom.root();
319        let b = dom.create_element("b");
320        dom.append_child(root, b).unwrap();
321        let stash = Rc::new(RefCell::new(Vec::new()));
322        {
323            // Cancel and stop the event first, so the clone has every
324            // flag to (not) copy.
325            let stash = stash.clone();
326            dom.add_event_listener(b, "ping", ListenerOptions::default(), move |ctx| {
327                ctx.event.prevent_default();
328                ctx.event.stop_immediate_propagation();
329                ctx.request_redraw();
330                stash.borrow_mut().push(ctx.event.clone());
331            })
332            .unwrap();
333        }
334        let mut e = Event::new("ping")
335            .with_bubbles(false)
336            .with_cancelable(true)
337            .with_detail("payload");
338        dom.dispatch_event(b, &mut e).unwrap();
339        let copy = stash.borrow_mut().pop().expect("listener ran");
340        assert_eq!(copy.event_type, "ping");
341        assert!(!copy.bubbles);
342        assert!(copy.cancelable);
343        assert_eq!(copy.detail, EventDetail::String("payload".into()));
344        assert_eq!(copy.phase, EventPhase::None);
345        assert_eq!(copy.target, None);
346        assert_eq!(copy.current_target, None);
347        assert!(
348            !copy.default_prevented(),
349            "a copy of a canceled event is not canceled"
350        );
351        assert!(!copy.is_propagation_stopped());
352        assert!(!copy.is_immediate_propagation_stopped());
353        assert!(!copy.redraw_requested());
354    }
355
356    /// The copy dispatches normally, both after the original's dispatch
357    /// and nested inside it, while re-dispatching the original in
358    /// flight is still `InvalidStateError` (DOM §2.9 step 1).
359    #[test]
360    fn clone_dispatches_while_the_original_in_flight_still_cannot() {
361        let mut dom: Dom = Dom::new();
362        let root = dom.root();
363        let a = dom.create_element("a");
364        dom.append_child(root, a).unwrap();
365        let results = Rc::new(RefCell::new(Vec::new()));
366        let fired = Rc::new(RefCell::new(Vec::new()));
367        {
368            let fired = fired.clone();
369            dom.add_event_listener(a, "ping", ListenerOptions::default(), move |ctx| {
370                fired.borrow_mut().push(ctx.event.detail.clone());
371            })
372            .unwrap();
373        }
374        {
375            // A listener is skipped while it is running, so the nested
376            // dispatch reaches only the recorder above.
377            let results = results.clone();
378            dom.add_event_listener(a, "ping", ListenerOptions::default(), move |ctx| {
379                if ctx.event.detail == EventDetail::String("outer".into()) {
380                    let original = ctx.dom.dispatch_event(a, ctx.event);
381                    let mut copy = ctx.event.clone();
382                    copy.detail = EventDetail::String("copy".into());
383                    let nested = ctx.dom.dispatch_event(a, &mut copy);
384                    results.borrow_mut().push((original, nested, copy));
385                }
386            })
387            .unwrap();
388        }
389        let mut e = Event::new("ping").with_detail("outer");
390        dom.dispatch_event(a, &mut e).unwrap();
391        let (original, nested, mut copy) = results.borrow_mut().pop().unwrap();
392        assert!(matches!(original, Err(DomError::InvalidState(_))));
393        assert_eq!(nested, Ok(()));
394        assert_eq!(
395            *fired.borrow(),
396            vec![
397                EventDetail::String("outer".into()),
398                EventDetail::String("copy".into())
399            ]
400        );
401        dom.dispatch_event(a, &mut copy).unwrap();
402        assert_eq!(
403            fired.borrow().len(),
404            3,
405            "the copy dispatches again afterwards"
406        );
407    }
408
409    /// A copy of a runtime-synthesized event is script-made: untrusted
410    /// on the web (`isTrusted` false), not synthetic here.
411    #[test]
412    fn clone_is_not_synthetic() {
413        let e = Event::new("click").with_synthetic(true);
414        assert!(!e.clone().is_synthetic());
415    }
416}