Skip to main content

dioxus_native_dom/
events.rs

1use blitz_dom::{BaseDocument, Node};
2use blitz_traits::events::{
3    BlitzKeyEvent, BlitzPointerEvent, BlitzPointerId, BlitzScrollEvent, BlitzWheelDelta,
4    BlitzWheelEvent, MouseEventButton,
5};
6use dioxus_html::{
7    AnimationData, BeforeInputData, CancelData, ClipboardData, CompositionData, DragData,
8    FocusData, FormData, FormValue, HasFileData, HasFocusData, HasFormData, HasKeyboardData,
9    HasMouseData, HasPointerData, HasScrollData, HasTouchData, HasTouchPointData, HasWheelData,
10    HtmlEventConverter, ImageData, KeyboardData, MediaData, MountedData, MountedError,
11    MountedResult, MouseData, PlatformEventData, PointerData, RenderedElementBacking, ResizeData,
12    ScrollBehavior, ScrollData, ScrollToOptions, SelectionData, ToggleData, TouchData, TouchPoint,
13    TransitionData, VisibleData, WheelData,
14    geometry::{
15        ClientPoint, ElementPoint, PagePoint, PixelsRect, PixelsSize, PixelsVector2D, ScreenPoint,
16        WheelDelta,
17        euclid::{Point2D, Size2D, Vector3D},
18    },
19    input_data::{MouseButton, MouseButtonSet},
20    point_interaction::{
21        InteractionElementOffset, InteractionLocation, ModifiersInteraction, PointerInteraction,
22    },
23};
24use keyboard_types::{Code, Key, Location, Modifiers};
25use std::{
26    any::Any,
27    cell::{Ref, RefCell, RefMut},
28    fmt::Display,
29    future::Future,
30    pin::Pin,
31    rc::Rc,
32};
33
34use crate::NodeId;
35
36pub struct NativeConverter {}
37
38impl HtmlEventConverter for NativeConverter {
39    fn convert_cancel_data(&self, _event: &PlatformEventData) -> CancelData {
40        unimplemented!("todo: convert_cancel_data in dioxus-native. requires support in blitz")
41    }
42
43    fn convert_form_data(&self, event: &PlatformEventData) -> FormData {
44        event.downcast::<NativeFormData>().unwrap().clone().into()
45    }
46
47    fn convert_mouse_data(&self, event: &PlatformEventData) -> MouseData {
48        event
49            .downcast::<NativePointerData>()
50            .unwrap()
51            .clone()
52            .into()
53    }
54
55    fn convert_keyboard_data(&self, event: &PlatformEventData) -> KeyboardData {
56        event
57            .downcast::<BlitzKeyboardData>()
58            .unwrap()
59            .clone()
60            .into()
61    }
62
63    fn convert_focus_data(&self, _event: &PlatformEventData) -> FocusData {
64        NativeFocusData {}.into()
65    }
66
67    fn convert_animation_data(&self, _event: &PlatformEventData) -> AnimationData {
68        unimplemented!("todo: convert_animation_data in dioxus-native. requires support in blitz")
69    }
70
71    fn convert_before_input_data(&self, _event: &PlatformEventData) -> BeforeInputData {
72        unimplemented!(
73            "todo: convert_before_input_data in dioxus-native. requires support in blitz"
74        )
75    }
76
77    fn convert_clipboard_data(&self, _event: &PlatformEventData) -> ClipboardData {
78        unimplemented!("todo: convert_clipboard_data in dioxus-native. requires support in blitz")
79    }
80
81    fn convert_composition_data(&self, _event: &PlatformEventData) -> CompositionData {
82        unimplemented!("todo: convert_composition_data in dioxus-native. requires support in blitz")
83    }
84
85    fn convert_drag_data(&self, _event: &PlatformEventData) -> DragData {
86        unimplemented!("todo: convert_drag_data in dioxus-native. requires support in blitz")
87    }
88
89    fn convert_image_data(&self, _event: &PlatformEventData) -> ImageData {
90        unimplemented!("todo: convert_image_data in dioxus-native. requires support in blitz")
91    }
92
93    fn convert_media_data(&self, _event: &PlatformEventData) -> MediaData {
94        unimplemented!("todo: convert_media_data in dioxus-native. requires support in blitz")
95    }
96
97    fn convert_mounted_data(&self, event: &PlatformEventData) -> MountedData {
98        event.downcast::<NodeHandle>().unwrap().clone().into()
99    }
100
101    fn convert_pointer_data(&self, event: &PlatformEventData) -> PointerData {
102        event
103            .downcast::<NativePointerData>()
104            .unwrap()
105            .clone()
106            .into()
107    }
108
109    fn convert_scroll_data(&self, event: &PlatformEventData) -> ScrollData {
110        event.downcast::<NativeScrollData>().unwrap().clone().into()
111    }
112
113    fn convert_selection_data(&self, _event: &PlatformEventData) -> SelectionData {
114        unimplemented!("todo: convert_selection_data in dioxus-native. requires support in blitz")
115    }
116
117    fn convert_toggle_data(&self, _event: &PlatformEventData) -> ToggleData {
118        unimplemented!("todo: convert_toggle_data in dioxus-native. requires support in blitz")
119    }
120
121    fn convert_touch_data(&self, event: &PlatformEventData) -> TouchData {
122        event.downcast::<NativeTouchData>().unwrap().clone().into()
123    }
124
125    fn convert_transition_data(&self, _event: &PlatformEventData) -> TransitionData {
126        unimplemented!("todo: convert_transition_data in dioxus-native. requires support in blitz")
127    }
128
129    fn convert_wheel_data(&self, event: &PlatformEventData) -> WheelData {
130        event.downcast::<NativeWheelData>().unwrap().clone().into()
131    }
132
133    fn convert_resize_data(&self, _event: &PlatformEventData) -> ResizeData {
134        unimplemented!("todo: convert_resize_data in dioxus-native. requires support in blitz")
135    }
136
137    fn convert_visible_data(&self, _event: &PlatformEventData) -> VisibleData {
138        unimplemented!("todo: convert_visible_data in dioxus-native. requires support in blitz")
139    }
140}
141
142#[derive(Clone)]
143pub struct NodeHandle {
144    pub(crate) doc: Rc<RefCell<BaseDocument>>,
145    pub(crate) node_id: NodeId,
146}
147
148impl NodeHandle {
149    pub fn node_id(&self) -> NodeId {
150        self.node_id
151    }
152
153    pub fn doc(&self) -> Ref<'_, BaseDocument> {
154        self.doc.borrow()
155    }
156
157    /// Returns `None` if the document is currently mutably borrowed.
158    /// Use this from background tasks to avoid panicking during event handling.
159    pub fn try_doc(&self) -> Option<Ref<'_, BaseDocument>> {
160        self.doc.try_borrow().ok()
161    }
162
163    pub fn doc_mut(&self) -> RefMut<'_, BaseDocument> {
164        self.doc.borrow_mut()
165    }
166
167    pub fn node(&self) -> Ref<'_, Node> {
168        Ref::map(self.doc.borrow(), |doc| {
169            doc.get_node(self.node_id)
170                .expect("Node does not exist in the Document")
171        })
172    }
173
174    pub fn node_mut(&self) -> RefMut<'_, Node> {
175        RefMut::map(self.doc.borrow_mut(), |doc| {
176            doc.get_node_mut(self.node_id)
177                .expect("Node does not exist in the Document")
178        })
179    }
180
181    fn node_not_exist_err<T>(&self) -> Pin<Box<dyn Future<Output = MountedResult<T>>>> {
182        let node_id = self.node_id;
183        let err = MountedError::OperationFailed(Box::new(NodeNotExistErr(node_id)));
184        Box::pin(async move { Err(err) })
185    }
186}
187
188#[derive(Debug)]
189struct NodeNotExistErr(NodeId);
190impl Display for NodeNotExistErr {
191    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
192        write!(f, "The node {} does not exist", self.0)
193    }
194}
195impl std::error::Error for NodeNotExistErr {}
196
197impl RenderedElementBacking for NodeHandle {
198    fn as_any(&self) -> &dyn std::any::Any {
199        self
200    }
201
202    fn get_scroll_offset(&self) -> Pin<Box<dyn Future<Output = MountedResult<PixelsVector2D>>>> {
203        let scroll_offset = self.node().scroll_offset;
204        Box::pin(async move { Ok(PixelsVector2D::new(scroll_offset.x, scroll_offset.y)) })
205    }
206
207    fn get_scroll_size(&self) -> Pin<Box<dyn Future<Output = MountedResult<PixelsSize>>>> {
208        let node = self.node();
209        let scroll_width = node.final_layout.scroll_width() as f64;
210        let scroll_height = node.final_layout.scroll_height() as f64;
211        Box::pin(async move { Ok(PixelsSize::new(scroll_width, scroll_height)) })
212    }
213
214    fn get_client_rect(&self) -> Pin<Box<dyn Future<Output = MountedResult<PixelsRect>>>> {
215        let Some(bounding_rect) = self.doc_mut().get_client_bounding_rect(self.node_id) else {
216            return self.node_not_exist_err();
217        };
218        let pixels_rect = PixelsRect::new(
219            Point2D::new(bounding_rect.x, bounding_rect.y),
220            Size2D::new(bounding_rect.width, bounding_rect.height),
221        );
222        Box::pin(async move { Ok(pixels_rect) })
223    }
224
225    fn scroll_to(
226        &self,
227        _options: ScrollToOptions,
228    ) -> Pin<Box<dyn Future<Output = MountedResult<()>>>> {
229        Box::pin(async { Err(MountedError::NotSupported) })
230    }
231
232    fn scroll(
233        &self,
234        _coordinates: PixelsVector2D,
235        _behavior: ScrollBehavior,
236    ) -> Pin<Box<dyn Future<Output = MountedResult<()>>>> {
237        Box::pin(async { Err(MountedError::NotSupported) })
238    }
239
240    fn set_focus(&self, focus: bool) -> Pin<Box<dyn Future<Output = MountedResult<()>>>> {
241        let mut doc = self.doc_mut();
242        if focus {
243            // TODO: queue focus events somehow
244            doc.set_focus_to(self.node_id);
245        } else if doc.get_focussed_node_id() == Some(self.node_id) {
246            // Q: Should this only clear focus if the node is focussed?
247            // TODO: queue blur events somehow
248            doc.clear_focus();
249        }
250
251        Box::pin(async { Ok(()) })
252    }
253}
254
255#[derive(Clone, Debug)]
256pub struct NativeFormData {
257    pub value: String,
258    pub values: Vec<(String, FormValue)>,
259}
260
261impl HasFormData for NativeFormData {
262    fn as_any(&self) -> &dyn Any {
263        self as &dyn Any
264    }
265
266    fn value(&self) -> String {
267        self.value.clone()
268    }
269
270    fn values(&self) -> Vec<(String, FormValue)> {
271        self.values.clone()
272    }
273    fn valid(&self) -> bool {
274        // todo: actually implement validation here.
275        true
276    }
277}
278
279impl HasFileData for NativeFormData {
280    fn files(&self) -> Vec<dioxus_html::FileData> {
281        vec![]
282    }
283}
284
285#[derive(Clone, Debug)]
286pub(crate) struct BlitzKeyboardData(pub(crate) BlitzKeyEvent);
287
288impl ModifiersInteraction for BlitzKeyboardData {
289    fn modifiers(&self) -> Modifiers {
290        self.0.modifiers
291    }
292}
293
294impl HasKeyboardData for BlitzKeyboardData {
295    fn key(&self) -> Key {
296        self.0.key.clone()
297    }
298
299    fn code(&self) -> Code {
300        self.0.code
301    }
302
303    fn location(&self) -> Location {
304        self.0.location
305    }
306
307    fn is_auto_repeating(&self) -> bool {
308        self.0.is_auto_repeating
309    }
310
311    fn is_composing(&self) -> bool {
312        self.0.is_composing
313    }
314
315    fn as_any(&self) -> &dyn Any {
316        self as &dyn Any
317    }
318}
319
320#[derive(Clone)]
321pub struct NativePointerData(pub(crate) BlitzPointerEvent);
322
323impl InteractionLocation for NativePointerData {
324    fn client_coordinates(&self) -> ClientPoint {
325        ClientPoint::new(self.0.client_x() as f64, self.0.client_y() as f64)
326    }
327
328    fn screen_coordinates(&self) -> ScreenPoint {
329        ScreenPoint::new(self.0.screen_x() as f64, self.0.screen_y() as f64)
330    }
331
332    fn page_coordinates(&self) -> PagePoint {
333        PagePoint::new(self.0.page_x() as f64, self.0.page_y() as f64)
334    }
335}
336
337impl InteractionElementOffset for NativePointerData {
338    fn element_coordinates(&self) -> ElementPoint {
339        ElementPoint::new(self.0.element_x() as f64, self.0.element_y() as f64)
340    }
341}
342
343impl ModifiersInteraction for NativePointerData {
344    fn modifiers(&self) -> Modifiers {
345        self.0.mods
346    }
347}
348
349impl PointerInteraction for NativePointerData {
350    fn trigger_button(&self) -> Option<MouseButton> {
351        Some(match self.0.button {
352            MouseEventButton::Main => MouseButton::Primary,
353            MouseEventButton::Auxiliary => MouseButton::Auxiliary,
354            MouseEventButton::Secondary => MouseButton::Secondary,
355            MouseEventButton::Fourth => MouseButton::Fourth,
356            MouseEventButton::Fifth => MouseButton::Fifth,
357        })
358    }
359
360    fn held_buttons(&self) -> MouseButtonSet {
361        dioxus_html::input_data::decode_mouse_button_set(self.0.buttons.bits() as u16)
362    }
363}
364impl HasMouseData for NativePointerData {
365    fn as_any(&self) -> &dyn Any {
366        self as &dyn Any
367    }
368}
369
370impl HasPointerData for NativePointerData {
371    fn as_any(&self) -> &dyn Any {
372        self as &dyn Any
373    }
374
375    fn is_primary(&self) -> bool {
376        self.0.is_primary
377    }
378
379    fn pointer_id(&self) -> i32 {
380        match self.0.id {
381            BlitzPointerId::Mouse => 0,
382            BlitzPointerId::Pen => 0,
383            BlitzPointerId::Finger(id) => id as i32,
384        }
385    }
386
387    fn pointer_type(&self) -> String {
388        match self.0.id {
389            BlitzPointerId::Mouse => String::from("mouse"),
390            BlitzPointerId::Pen => String::from("pen"),
391            BlitzPointerId::Finger(_) => String::from("touch"),
392        }
393    }
394
395    fn pressure(&self) -> f32 {
396        self.0.details.pressure as f32
397    }
398    fn tangential_pressure(&self) -> f32 {
399        self.0.details.tangential_pressure
400    }
401    fn tilt_x(&self) -> i32 {
402        self.0.details.tilt_x as i32
403    }
404    fn tilt_y(&self) -> i32 {
405        self.0.details.tilt_y as i32
406    }
407    fn twist(&self) -> i32 {
408        self.0.details.twist as i32
409    }
410
411    // TODO: implement these fields with real values
412    fn width(&self) -> f64 {
413        1.0
414    }
415    fn height(&self) -> f64 {
416        1.0
417    }
418}
419
420/// Touch event data exposed to Dioxus Native application code.
421///
422/// Blitz tracks input via pointer events, so a touch event is generated from the
423/// pointer event of the finger that triggered it (`touches_changed`). The full
424/// list of concurrent touches is carried on the triggering event's
425/// [`BlitzPointerEvent::active_pointers`] list and reported via `touches`.
426#[derive(Clone)]
427pub struct NativeTouchData(pub(crate) BlitzPointerEvent);
428
429impl ModifiersInteraction for NativeTouchData {
430    fn modifiers(&self) -> Modifiers {
431        self.0.mods
432    }
433}
434
435impl HasTouchData for NativeTouchData {
436    fn touches(&self) -> Vec<TouchPoint> {
437        // All pointers currently active on the surface (multi-touch).
438        self.0
439            .active_pointers
440            .borrow()
441            .iter()
442            .map(|event| TouchPoint::new(NativeTouchPointData(event.clone())))
443            .collect()
444    }
445
446    fn touches_changed(&self) -> Vec<TouchPoint> {
447        // Just the touch that triggered this event.
448        vec![TouchPoint::new(NativeTouchPointData(self.0.clone()))]
449    }
450
451    fn target_touches(&self) -> Vec<TouchPoint> {
452        // We don't track a per-touch target, so approximate `targetTouches`
453        // (touches that started on the event target) with all active touches.
454        self.touches()
455    }
456
457    fn as_any(&self) -> &dyn Any {
458        self as &dyn Any
459    }
460}
461
462#[derive(Clone)]
463pub struct NativeTouchPointData(BlitzPointerEvent);
464
465impl InteractionLocation for NativeTouchPointData {
466    fn client_coordinates(&self) -> ClientPoint {
467        ClientPoint::new(self.0.client_x() as f64, self.0.client_y() as f64)
468    }
469
470    fn screen_coordinates(&self) -> ScreenPoint {
471        ScreenPoint::new(self.0.screen_x() as f64, self.0.screen_y() as f64)
472    }
473
474    fn page_coordinates(&self) -> PagePoint {
475        PagePoint::new(self.0.page_x() as f64, self.0.page_y() as f64)
476    }
477}
478
479impl HasTouchPointData for NativeTouchPointData {
480    fn identifier(&self) -> i32 {
481        match self.0.id {
482            BlitzPointerId::Finger(id) => id as i32,
483            BlitzPointerId::Mouse | BlitzPointerId::Pen => 0,
484        }
485    }
486
487    fn force(&self) -> f64 {
488        self.0.details.pressure
489    }
490
491    fn radius(&self) -> ScreenPoint {
492        // TODO: expose real touch radius once blitz tracks it
493        ScreenPoint::new(1.0, 1.0)
494    }
495
496    fn rotation(&self) -> f64 {
497        // TODO: expose real touch rotation once blitz tracks it
498        0.0
499    }
500
501    fn as_any(&self) -> &dyn Any {
502        self as &dyn Any
503    }
504}
505
506#[derive(Clone)]
507pub struct NativeFocusData;
508impl HasFocusData for NativeFocusData {
509    fn as_any(&self) -> &dyn Any {
510        self as &dyn Any
511    }
512}
513
514#[derive(Clone)]
515pub struct NativeScrollData(pub(crate) BlitzScrollEvent);
516impl HasScrollData for NativeScrollData {
517    fn as_any(&self) -> &dyn Any {
518        self as &dyn Any
519    }
520
521    fn scroll_top(&self) -> f64 {
522        self.0.scroll_top
523    }
524
525    fn scroll_left(&self) -> f64 {
526        self.0.scroll_left
527    }
528
529    fn scroll_width(&self) -> i32 {
530        self.0.scroll_width
531    }
532
533    fn scroll_height(&self) -> i32 {
534        self.0.scroll_height
535    }
536
537    fn client_width(&self) -> i32 {
538        self.0.client_width
539    }
540
541    fn client_height(&self) -> i32 {
542        self.0.client_height
543    }
544}
545
546#[derive(Clone)]
547pub struct NativeWheelData(pub(crate) BlitzWheelEvent);
548impl HasWheelData for NativeWheelData {
549    fn as_any(&self) -> &dyn Any {
550        self as &dyn Any
551    }
552
553    fn delta(&self) -> WheelDelta {
554        match self.0.delta {
555            BlitzWheelDelta::Lines(x, y) => {
556                dioxus_html::geometry::WheelDelta::Lines(Vector3D::new(x, y, 0.0))
557            }
558            BlitzWheelDelta::Pixels(x, y) => {
559                dioxus_html::geometry::WheelDelta::Pixels(Vector3D::new(x, y, 0.0))
560            }
561        }
562    }
563}
564
565impl HasMouseData for NativeWheelData {
566    fn as_any(&self) -> &dyn Any {
567        self as &dyn Any
568    }
569}
570
571impl PointerInteraction for NativeWheelData {
572    fn trigger_button(&self) -> Option<MouseButton> {
573        None
574    }
575
576    fn held_buttons(&self) -> MouseButtonSet {
577        dioxus_html::input_data::decode_mouse_button_set(self.0.buttons.bits() as u16)
578    }
579}
580
581impl ModifiersInteraction for NativeWheelData {
582    fn modifiers(&self) -> Modifiers {
583        self.0.mods
584    }
585}
586
587impl InteractionElementOffset for NativeWheelData {
588    fn element_coordinates(&self) -> ElementPoint {
589        ElementPoint::new(self.0.element_x() as f64, self.0.element_y() as f64)
590    }
591}
592
593impl InteractionLocation for NativeWheelData {
594    fn client_coordinates(&self) -> ClientPoint {
595        ClientPoint::new(self.0.client_x() as f64, self.0.client_y() as f64)
596    }
597
598    fn screen_coordinates(&self) -> ScreenPoint {
599        ScreenPoint::new(self.0.screen_x() as f64, self.0.screen_y() as f64)
600    }
601
602    fn page_coordinates(&self) -> PagePoint {
603        PagePoint::new(self.0.page_x() as f64, self.0.page_y() as f64)
604    }
605}
606
607pub fn synthetic_click_event(node: &Node, modifiers: Modifiers) -> Box<dyn Any> {
608    Box::new(NativePointerData(
609        node.synthetic_click_event_data(modifiers),
610    ))
611}
612
613#[cfg(test)]
614mod tests {
615    use super::*;
616    use blitz_traits::events::{
617        BlitzPointerId, MouseEventButton, MouseEventButtons, Point, PointerCoords, PointerDetails,
618    };
619
620    fn finger_event(id: u64, x: f32, y: f32) -> BlitzPointerEvent {
621        BlitzPointerEvent {
622            id: BlitzPointerId::Finger(id),
623            is_primary: id == 0,
624            coords: PointerCoords {
625                page_x: x,
626                page_y: y,
627                screen_x: x,
628                screen_y: y,
629                client_x: x,
630                client_y: y,
631            },
632            button: MouseEventButton::Main,
633            buttons: MouseEventButtons::from(MouseEventButton::Main),
634            mods: Default::default(),
635            details: PointerDetails::default(),
636            element: Point::default(),
637            active_pointers: Default::default(),
638        }
639    }
640
641    #[test]
642    fn touches_reports_all_active_pointers() {
643        let f0 = finger_event(0, 10.0, 20.0);
644        let f1 = finger_event(1, 30.0, 40.0);
645
646        // The triggering event (second finger down) carries the list of all
647        // currently-active pointers.
648        let trigger = f1.clone();
649        {
650            let mut list = trigger.active_pointers.borrow_mut();
651            list.push(f0.clone());
652            list.push(f1.clone());
653        }
654
655        let data = NativeTouchData(trigger);
656
657        // `touches` reports every active pointer ...
658        let touches = data.touches();
659        assert_eq!(touches.len(), 2);
660        let coords: Vec<(f64, f64)> = touches
661            .iter()
662            .map(|t| {
663                let c = t.client_coordinates();
664                (c.x, c.y)
665            })
666            .collect();
667        assert!(coords.contains(&(10.0, 20.0)));
668        assert!(coords.contains(&(30.0, 40.0)));
669
670        // ... while `changed_touches` reports only the triggering touch.
671        assert_eq!(data.touches_changed().len(), 1);
672        let changed = data.touches_changed();
673        let changed_coords = changed[0].client_coordinates();
674        assert_eq!((changed_coords.x, changed_coords.y), (30.0, 40.0));
675    }
676
677    #[test]
678    fn touches_is_empty_when_no_pointers_are_active() {
679        // e.g. a `touchend` for the last finger: it has been removed from the
680        // active list before dispatch, so `touches` is empty but
681        // `changed_touches` still reports the finger that ended.
682        let data = NativeTouchData(finger_event(0, 1.0, 2.0));
683        assert!(data.touches().is_empty());
684        assert_eq!(data.touches_changed().len(), 1);
685    }
686}