Skip to main content

blitz_dom/events/
driver.rs

1use crate::Document;
2use blitz_traits::events::{
3    BlitzPointerEvent, BlitzPointerId, DomEvent, DomEventData, EventState, Point, PointerCoords,
4    UiEvent,
5};
6use blitz_traits::node_id::NodeId;
7use std::collections::VecDeque;
8
9pub trait EventHandler {
10    fn handle_event(
11        &mut self,
12        chain: &[NodeId],
13        event: &mut DomEvent,
14        doc: &mut dyn Document,
15        event_state: &mut EventState,
16    );
17}
18
19pub struct NoopEventHandler;
20impl EventHandler for NoopEventHandler {
21    fn handle_event(
22        &mut self,
23        _chain: &[NodeId],
24        _event: &mut DomEvent,
25        _doc: &mut dyn Document,
26        _event_state: &mut EventState,
27    ) {
28        // Do nothing
29    }
30}
31
32pub struct EventDriver<'doc, Handler: EventHandler> {
33    doc: &'doc mut dyn Document,
34    handler: Handler,
35    queue: VecDeque<DomEvent>,
36}
37
38impl<'doc, Handler: EventHandler> EventDriver<'doc, Handler> {
39    pub fn new(doc: &'doc mut dyn Document, handler: Handler) -> Self {
40        EventDriver {
41            doc,
42            handler,
43            queue: VecDeque::with_capacity(4),
44        }
45    }
46
47    pub fn handle_pointer_move(&mut self, event: &BlitzPointerEvent) -> Option<NodeId> {
48        let mut doc = self.doc.inner_mut();
49
50        let prev_hover_node_id = doc.hover_node_id;
51        let changed = doc.set_hover_to(event.page_x(), event.page_y());
52        let hover_node_id = doc.hover_node_id;
53
54        drop(doc);
55
56        self.dispatch_pointer_move(event, prev_hover_node_id, hover_node_id, changed)
57    }
58
59    /// Dispatch pointer movement to a semantic node that the caller already
60    /// resolved, without selecting a second target by coordinate hit testing.
61    pub fn handle_pointer_move_to_node(
62        &mut self,
63        event: &BlitzPointerEvent,
64        node_id: NodeId,
65    ) -> Option<NodeId> {
66        let mut doc = self.doc.inner_mut();
67
68        let prev_hover_node_id = doc.hover_node_id;
69        let changed = doc.set_hover_to_node(node_id, event.page_x(), event.page_y());
70        let hover_node_id = doc.hover_node_id;
71
72        drop(doc);
73
74        self.dispatch_pointer_move(event, prev_hover_node_id, hover_node_id, changed)
75    }
76
77    fn dispatch_pointer_move(
78        &mut self,
79        event: &BlitzPointerEvent,
80        prev_hover_node_id: Option<NodeId>,
81        hover_node_id: Option<NodeId>,
82        changed: bool,
83    ) -> Option<NodeId> {
84        if !changed {
85            return prev_hover_node_id;
86        }
87
88        let doc = self.doc.inner();
89        let mut old_chain = prev_hover_node_id
90            .map(|id| doc.node_chain(id))
91            .unwrap_or_default();
92        let mut new_chain = hover_node_id
93            .map(|id| doc.node_chain(id))
94            .unwrap_or_default();
95        old_chain.reverse();
96        new_chain.reverse();
97
98        // Find the difference in the node chain of the last hovered objected and the newest
99        let old_len = old_chain.len();
100        let new_len = new_chain.len();
101
102        let first_difference_index = old_chain
103            .iter()
104            .zip(&new_chain)
105            .position(|(old, new)| old != new)
106            .unwrap_or_else(|| old_len.min(new_len));
107
108        drop(doc);
109
110        let is_mouse = event.is_mouse();
111
112        if let Some(target) = prev_hover_node_id {
113            self.handle_dom_event(DomEvent::new(
114                target,
115                DomEventData::PointerOut(event.clone()),
116            ));
117            if is_mouse {
118                self.handle_dom_event(DomEvent::new(target, DomEventData::MouseOut(event.clone())));
119            }
120
121            // Send an mouseleave event to all old elements on the chain
122            for node_id in old_chain
123                .get(first_difference_index..)
124                .unwrap_or(&[])
125                .iter()
126            {
127                self.handle_dom_event(DomEvent::new(
128                    *node_id,
129                    DomEventData::PointerLeave(event.clone()),
130                ));
131                if is_mouse {
132                    self.handle_dom_event(DomEvent::new(
133                        *node_id,
134                        DomEventData::MouseLeave(event.clone()),
135                    ));
136                }
137            }
138        }
139
140        if let Some(target) = hover_node_id {
141            self.handle_dom_event(DomEvent::new(
142                target,
143                DomEventData::PointerOver(event.clone()),
144            ));
145
146            if is_mouse {
147                self.handle_dom_event(DomEvent::new(
148                    target,
149                    DomEventData::MouseOver(event.clone()),
150                ));
151            }
152
153            // Send an mouseenter event to all new elements on the chain
154            for node_id in new_chain
155                .get(first_difference_index..)
156                .unwrap_or(&[])
157                .iter()
158            {
159                self.handle_dom_event(DomEvent::new(
160                    *node_id,
161                    DomEventData::PointerEnter(event.clone()),
162                ));
163
164                if is_mouse {
165                    self.handle_dom_event(DomEvent::new(
166                        *node_id,
167                        DomEventData::MouseEnter(event.clone()),
168                    ));
169                }
170            }
171        }
172
173        hover_node_id
174    }
175
176    pub fn handle_ui_event(&mut self, event: UiEvent) {
177        self.handle_ui_event_inner(event, None);
178    }
179
180    /// Dispatch input to a DOM node that automation already resolved.
181    ///
182    /// Window input remains coordinate hit-tested through [`Self::handle_ui_event`].
183    /// A semantic driver has already selected its target by node id, and may be
184    /// operating without fonts or a compositor, so hit-testing its synthetic
185    /// coordinate would either choose a different node or reject a flat one.
186    pub fn handle_ui_event_to_node(&mut self, event: UiEvent, node_id: NodeId) {
187        self.handle_ui_event_inner(event, Some(node_id));
188    }
189
190    fn handle_ui_event_inner(&mut self, event: UiEvent, forced_target: Option<NodeId>) {
191        let doc = self.doc.inner();
192
193        let mut should_clear_hover = false;
194        let mut hover_node_id = doc.hover_node_id;
195        let focussed_node_id = doc.focus_node_id;
196        drop(doc);
197
198        // Update document input state (hover, focus, active, etc)
199        match &event {
200            UiEvent::PointerMove(event) => {
201                hover_node_id = forced_target
202                    .and_then(|node_id| self.handle_pointer_move_to_node(event, node_id))
203                    .or_else(|| self.handle_pointer_move(event));
204            }
205            UiEvent::PointerDown(event) => {
206                hover_node_id = forced_target
207                    .and_then(|node_id| self.handle_pointer_move_to_node(event, node_id))
208                    .or_else(|| self.handle_pointer_move(event));
209                let mut doc = self.doc.inner_mut();
210                doc.active_node();
211                doc.set_mousedown_node_id(hover_node_id);
212            }
213            UiEvent::PointerUp(event) => {
214                hover_node_id = forced_target
215                    .and_then(|node_id| self.handle_pointer_move_to_node(event, node_id))
216                    .or_else(|| self.handle_pointer_move(event));
217                let mut doc = self.doc.inner_mut();
218                doc.unactive_node();
219
220                if event.is_primary && matches!(event.id, BlitzPointerId::Finger(_)) {
221                    should_clear_hover = true;
222                }
223            }
224            UiEvent::PointerCancel(event) => {
225                hover_node_id = forced_target
226                    .and_then(|node_id| self.handle_pointer_move_to_node(event, node_id))
227                    .or_else(|| self.handle_pointer_move(event));
228                let mut doc = self.doc.inner_mut();
229                doc.unactive_node();
230                doc.set_mousedown_node_id(None);
231
232                if event.is_primary && matches!(event.id, BlitzPointerId::Finger(_)) {
233                    should_clear_hover = true;
234                }
235            }
236            _ => {}
237        };
238
239        let target = match event {
240            UiEvent::PointerMove(_) => hover_node_id,
241            UiEvent::PointerUp(_) => hover_node_id,
242            UiEvent::PointerDown(_) => hover_node_id,
243            UiEvent::PointerCancel(_) => hover_node_id,
244            UiEvent::Wheel(_) => hover_node_id,
245            UiEvent::KeyUp(_) => focussed_node_id,
246            UiEvent::KeyDown(_) => focussed_node_id,
247            UiEvent::Ime(_) => focussed_node_id,
248            UiEvent::AppleStandardKeybinding(_) => focussed_node_id,
249        };
250        // Fall back to the root element. A document without a root element (e.g. an
251        // empty iframe sub-document) has no event target, so there is nothing to do.
252        let Some(target) = target.or_else(|| self.doc.inner().try_root_element().map(|el| el.id))
253        else {
254            return;
255        };
256
257        match event {
258            UiEvent::PointerMove(data) => {
259                self.handle_pointer_event(
260                    target,
261                    data,
262                    DomEventData::PointerMove,
263                    Some(DomEventData::MouseMove),
264                    DomEventData::TouchMove,
265                );
266            }
267            UiEvent::PointerUp(data) => {
268                self.handle_pointer_event(
269                    target,
270                    data,
271                    DomEventData::PointerUp,
272                    Some(DomEventData::MouseUp),
273                    DomEventData::TouchEnd,
274                );
275            }
276            UiEvent::PointerDown(data) => {
277                self.handle_pointer_event(
278                    target,
279                    data,
280                    DomEventData::PointerDown,
281                    Some(DomEventData::MouseDown),
282                    DomEventData::TouchStart,
283                );
284            }
285            UiEvent::PointerCancel(data) => {
286                // `pointercancel` has no mouse-compatibility event, but does
287                // generate a `touchcancel` for touch-like inputs.
288                self.handle_pointer_event(
289                    target,
290                    data,
291                    DomEventData::PointerCancel,
292                    None::<fn(BlitzPointerEvent) -> DomEventData>,
293                    DomEventData::TouchCancel,
294                );
295            }
296            UiEvent::Wheel(data) => {
297                self.handle_dom_event(DomEvent::new(target, DomEventData::Wheel(data)))
298            }
299            UiEvent::KeyUp(data) => {
300                self.handle_dom_event(DomEvent::new(target, DomEventData::KeyUp(data)))
301            }
302            UiEvent::KeyDown(data) => {
303                self.handle_dom_event(DomEvent::new(target, DomEventData::KeyDown(data)))
304            }
305            UiEvent::Ime(data) => {
306                self.handle_dom_event(DomEvent::new(target, DomEventData::Ime(data)))
307            }
308            UiEvent::AppleStandardKeybinding(data) => {
309                let mut dom_event =
310                    DomEvent::new(target, DomEventData::AppleStandardKeybinding(data));
311                self.run_default_action(&mut dom_event);
312                self.process_queue();
313            }
314        };
315
316        // Update document input state (hover, focus, active, etc)
317        if should_clear_hover {
318            self.doc.inner_mut().clear_hover();
319        }
320    }
321
322    pub fn handle_dom_event(&mut self, event: DomEvent) {
323        self.queue.push_back(event);
324        self.process_queue();
325    }
326
327    fn handle_pointer_event(
328        &mut self,
329        target: NodeId,
330        data: BlitzPointerEvent,
331        make_ptr_data: impl FnOnce(BlitzPointerEvent) -> DomEventData,
332        make_mouse_data: Option<impl FnOnce(BlitzPointerEvent) -> DomEventData>,
333        make_touch_data: impl FnOnce(BlitzPointerEvent) -> DomEventData,
334    ) {
335        let mut ptr_event = DomEvent::new(target, make_ptr_data(data.clone()));
336        let mut event_state = EventState::default();
337        event_state = self.run_handler_event(&mut ptr_event, event_state);
338
339        // Generate the corresponding compatibility event (mouse events for the
340        // mouse, touch events for fingers and pen/stylus input) and expose it to
341        // script. The default action is always run on the pointer event so that
342        // the shell layer and default actions remain pointer-based.
343        //
344        // `pointercancel` has no mouse equivalent, so `make_mouse_data` is `None`
345        // in that case and no mouse event is generated.
346        if !event_state.is_cancelled() {
347            if data.is_mouse() {
348                if let Some(make_mouse_data) = make_mouse_data {
349                    let mut mouse_event = DomEvent::new(target, make_mouse_data(data));
350                    event_state = self.run_handler_event(&mut mouse_event, event_state);
351                }
352            } else if data.is_finger() || data.is_pen() {
353                let mut touch_event = DomEvent::new(target, make_touch_data(data));
354                event_state = self.run_handler_event(&mut touch_event, event_state);
355            }
356        }
357
358        if !event_state.is_cancelled() {
359            self.run_default_action(&mut ptr_event);
360        }
361        self.process_queue();
362    }
363
364    fn process_queue(&mut self) {
365        while let Some(mut event) = self.queue.pop_front() {
366            // HTML runs a click's *pre-click activation steps* before the event
367            // is dispatched, so a checkbox or radio has already taken its new
368            // value by the time any listener runs. Doing it afterwards -- as
369            // the default action, with everything else -- hands `click`
370            // listeners the value from before the press, which reads as a
371            // control that responds to every second press. Cancelling the click
372            // runs the canceled activation steps and puts it back.
373            let activation = match event.data {
374                DomEventData::Click(_) => {
375                    self.doc.inner_mut().run_pre_click_activation(event.target)
376                }
377                _ => None,
378            };
379
380            let event_state = self.run_handler_event(&mut event, EventState::default());
381            if event_state.is_cancelled() {
382                if let Some(activation) = activation {
383                    self.doc.inner_mut().undo_pre_click_activation(activation);
384                }
385            } else {
386                self.run_default_action(&mut event);
387            }
388        }
389    }
390
391    fn adjust_element_coords(
392        &self,
393        target: NodeId,
394        coords: &PointerCoords,
395        element: &mut Point<f32>,
396    ) {
397        if let Some(rect) = self.doc.inner().get_client_bounding_rect(target) {
398            element.x = coords.client_x - rect.x as f32;
399            element.y = coords.client_y - rect.y as f32;
400        }
401    }
402
403    fn run_handler_event(
404        &mut self,
405        event: &mut DomEvent,
406        initial_event_state: EventState,
407    ) -> EventState {
408        let chain = if event.bubbles {
409            let doc = self.doc.inner();
410            doc.node_chain(event.target)
411        } else {
412            vec![event.target]
413        };
414
415        match &mut event.data {
416            DomEventData::PointerMove(data)
417            | DomEventData::PointerDown(data)
418            | DomEventData::PointerUp(data)
419            | DomEventData::PointerCancel(data)
420            | DomEventData::PointerEnter(data)
421            | DomEventData::PointerLeave(data)
422            | DomEventData::PointerOver(data)
423            | DomEventData::PointerOut(data)
424            | DomEventData::MouseMove(data)
425            | DomEventData::MouseDown(data)
426            | DomEventData::MouseUp(data)
427            | DomEventData::MouseEnter(data)
428            | DomEventData::MouseLeave(data)
429            | DomEventData::MouseOver(data)
430            | DomEventData::MouseOut(data)
431            | DomEventData::TouchStart(data)
432            | DomEventData::TouchEnd(data)
433            | DomEventData::TouchMove(data)
434            | DomEventData::TouchCancel(data)
435            | DomEventData::Click(data)
436            | DomEventData::ContextMenu(data)
437            | DomEventData::DoubleClick(data) => {
438                self.adjust_element_coords(event.target, &data.coords, &mut data.element)
439            }
440            DomEventData::Wheel(data) => {
441                self.adjust_element_coords(event.target, &data.coords, &mut data.element)
442            }
443            _ => {}
444        }
445
446        let mut event_state = initial_event_state;
447        self.handler
448            .handle_event(&chain, event, self.doc, &mut event_state);
449
450        event_state
451    }
452
453    fn run_default_action(&mut self, event: &mut DomEvent) {
454        let mut doc = self.doc.inner_mut();
455        doc.handle_dom_event(event, |new_evt| self.queue.push_back(new_evt));
456    }
457}