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, CancelData, ClipboardData, CompositionData, DragData, FocusData, FormData,
8    FormValue, HasFileData, HasFocusData, HasFormData, HasKeyboardData, HasMouseData,
9    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_clipboard_data(&self, _event: &PlatformEventData) -> ClipboardData {
72        unimplemented!("todo: convert_clipboard_data in dioxus-native. requires support in blitz")
73    }
74
75    fn convert_composition_data(&self, _event: &PlatformEventData) -> CompositionData {
76        unimplemented!("todo: convert_composition_data in dioxus-native. requires support in blitz")
77    }
78
79    fn convert_drag_data(&self, _event: &PlatformEventData) -> DragData {
80        unimplemented!("todo: convert_drag_data in dioxus-native. requires support in blitz")
81    }
82
83    fn convert_image_data(&self, _event: &PlatformEventData) -> ImageData {
84        unimplemented!("todo: convert_image_data in dioxus-native. requires support in blitz")
85    }
86
87    fn convert_media_data(&self, _event: &PlatformEventData) -> MediaData {
88        unimplemented!("todo: convert_media_data in dioxus-native. requires support in blitz")
89    }
90
91    fn convert_mounted_data(&self, event: &PlatformEventData) -> MountedData {
92        event.downcast::<NodeHandle>().unwrap().clone().into()
93    }
94
95    fn convert_pointer_data(&self, event: &PlatformEventData) -> PointerData {
96        event
97            .downcast::<NativePointerData>()
98            .unwrap()
99            .clone()
100            .into()
101    }
102
103    fn convert_scroll_data(&self, event: &PlatformEventData) -> ScrollData {
104        event.downcast::<NativeScrollData>().unwrap().clone().into()
105    }
106
107    fn convert_selection_data(&self, _event: &PlatformEventData) -> SelectionData {
108        unimplemented!("todo: convert_selection_data in dioxus-native. requires support in blitz")
109    }
110
111    fn convert_toggle_data(&self, _event: &PlatformEventData) -> ToggleData {
112        unimplemented!("todo: convert_toggle_data in dioxus-native. requires support in blitz")
113    }
114
115    fn convert_touch_data(&self, event: &PlatformEventData) -> TouchData {
116        event.downcast::<NativeTouchData>().unwrap().clone().into()
117    }
118
119    fn convert_transition_data(&self, _event: &PlatformEventData) -> TransitionData {
120        unimplemented!("todo: convert_transition_data in dioxus-native. requires support in blitz")
121    }
122
123    fn convert_wheel_data(&self, event: &PlatformEventData) -> WheelData {
124        event.downcast::<NativeWheelData>().unwrap().clone().into()
125    }
126
127    fn convert_resize_data(&self, _event: &PlatformEventData) -> ResizeData {
128        unimplemented!("todo: convert_resize_data in dioxus-native. requires support in blitz")
129    }
130
131    fn convert_visible_data(&self, _event: &PlatformEventData) -> VisibleData {
132        unimplemented!("todo: convert_visible_data in dioxus-native. requires support in blitz")
133    }
134}
135
136pub(crate) enum DocumentCommand {
137    SetFocus { node_id: NodeId, focus: bool },
138}
139
140pub(crate) type DocumentCommandQueue = Rc<RefCell<Vec<DocumentCommand>>>;
141
142fn apply_focus(doc: &mut BaseDocument, node_id: NodeId, focus: bool) {
143    if focus {
144        doc.set_focus_to(node_id);
145    } else if doc.get_focussed_node_id() == Some(node_id) {
146        doc.clear_focus();
147    }
148}
149
150fn queue_focus(queue: &DocumentCommandQueue, node_id: NodeId, focus: bool) {
151    queue
152        .borrow_mut()
153        .push(DocumentCommand::SetFocus { node_id, focus });
154}
155
156/// Apply commands for one document that could not be applied when requested.
157///
158/// Call this only where the document is known not to be borrowed. Requests are
159/// taken out of the queue before any is applied, so a handler that queues
160/// another one does not extend this pass into an unbounded loop.
161pub(crate) fn flush_document_commands(
162    doc: &Rc<RefCell<BaseDocument>>,
163    queue: &DocumentCommandQueue,
164) {
165    let queued = std::mem::take(&mut *queue.borrow_mut());
166    for command in queued {
167        let DocumentCommand::SetFocus { node_id, focus } = command;
168        // Still fallible: a caller may hold the borrow for reasons of its own.
169        // Dropping the request is wrong, so it goes back on the queue for the
170        // next flush rather than panicking or being lost.
171        let applied = match doc.try_borrow_mut() {
172            Ok(mut doc_ref) => {
173                apply_focus(&mut doc_ref, node_id, focus);
174                true
175            }
176            Err(_) => false,
177        };
178        if !applied {
179            queue_focus(queue, node_id, focus);
180        }
181    }
182}
183
184#[derive(Clone)]
185pub struct NodeHandle {
186    pub(crate) doc: Rc<RefCell<BaseDocument>>,
187    pub(crate) command_queue: DocumentCommandQueue,
188    pub(crate) node_id: NodeId,
189}
190
191impl NodeHandle {
192    pub fn node_id(&self) -> NodeId {
193        self.node_id
194    }
195
196    pub fn doc(&self) -> Ref<'_, BaseDocument> {
197        self.doc.borrow()
198    }
199
200    /// Returns `None` if the document is currently mutably borrowed.
201    /// Use this from background tasks to avoid panicking during event handling.
202    pub fn try_doc(&self) -> Option<Ref<'_, BaseDocument>> {
203        self.doc.try_borrow().ok()
204    }
205
206    pub fn doc_mut(&self) -> RefMut<'_, BaseDocument> {
207        self.doc.borrow_mut()
208    }
209
210    /// Returns `None` if the document is already borrowed.
211    ///
212    /// The mutable counterpart to [`Self::try_doc`], for the same reason:
213    /// a background task that ticks on a timer will eventually land while the
214    /// document is held, and a dropped tick is cheaper than a panic.
215    pub fn try_doc_mut(&self) -> Option<RefMut<'_, BaseDocument>> {
216        self.doc.try_borrow_mut().ok()
217    }
218
219    pub fn node(&self) -> Ref<'_, Node> {
220        Ref::map(self.doc.borrow(), |doc| {
221            doc.get_node(self.node_id)
222                .expect("Node does not exist in the Document")
223        })
224    }
225
226    pub fn node_mut(&self) -> RefMut<'_, Node> {
227        RefMut::map(self.doc.borrow_mut(), |doc| {
228            doc.get_node_mut(self.node_id)
229                .expect("Node does not exist in the Document")
230        })
231    }
232
233    fn node_not_exist_err<T>(&self) -> Pin<Box<dyn Future<Output = MountedResult<T>>>> {
234        let node_id = self.node_id;
235        let err = MountedError::OperationFailed(Box::new(NodeNotExistErr(node_id)));
236        Box::pin(async move { Err(err) })
237    }
238}
239
240#[derive(Debug)]
241struct NodeNotExistErr(NodeId);
242impl Display for NodeNotExistErr {
243    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
244        write!(f, "The node {} does not exist", self.0)
245    }
246}
247impl std::error::Error for NodeNotExistErr {}
248
249impl RenderedElementBacking for NodeHandle {
250    fn as_any(&self) -> &dyn std::any::Any {
251        self
252    }
253
254    fn get_scroll_offset(&self) -> Pin<Box<dyn Future<Output = MountedResult<PixelsVector2D>>>> {
255        let scroll_offset = *self.node().scroll_offset();
256        Box::pin(async move { Ok(PixelsVector2D::new(scroll_offset.x, scroll_offset.y)) })
257    }
258
259    fn get_scroll_size(&self) -> Pin<Box<dyn Future<Output = MountedResult<PixelsSize>>>> {
260        let node = self.node();
261        let scroll_width = node.final_layout().scroll_width() as f64;
262        let scroll_height = node.final_layout().scroll_height() as f64;
263        Box::pin(async move { Ok(PixelsSize::new(scroll_width, scroll_height)) })
264    }
265
266    fn get_client_rect(&self) -> Pin<Box<dyn Future<Output = MountedResult<PixelsRect>>>> {
267        let Some(bounding_rect) = self.doc_mut().get_client_bounding_rect(self.node_id) else {
268            return self.node_not_exist_err();
269        };
270        let pixels_rect = PixelsRect::new(
271            Point2D::new(bounding_rect.x, bounding_rect.y),
272            Size2D::new(bounding_rect.width, bounding_rect.height),
273        );
274        Box::pin(async move { Ok(pixels_rect) })
275    }
276
277    fn scroll_to(
278        &self,
279        _options: ScrollToOptions,
280    ) -> Pin<Box<dyn Future<Output = MountedResult<()>>>> {
281        Box::pin(async { Err(MountedError::NotSupported) })
282    }
283
284    fn scroll(
285        &self,
286        _coordinates: PixelsVector2D,
287        _behavior: ScrollBehavior,
288    ) -> Pin<Box<dyn Future<Output = MountedResult<()>>>> {
289        Box::pin(async { Err(MountedError::NotSupported) })
290    }
291
292    fn set_focus(&self, focus: bool) -> Pin<Box<dyn Future<Output = MountedResult<()>>>> {
293        // Deliberately not `doc_mut()`. This is reached from a spawned task, and
294        // `DioxusDocument::poll` drives task wakeups *while it holds the
295        // document borrow*, so taking it here panics with `RefCell already
296        // borrowed`:
297        //
298        //     DioxusDocument::poll
299        //       -> Runtime::handle_task_wakeup
300        //         -> the component's focus task
301        //           -> doc_mut()   panic
302        //
303        // Deferring the request further does not avoid it, because every later
304        // tick is inside a poll too. So a request that cannot be applied now is
305        // recorded in this document's command queue and applied once the
306        // borrow is released.
307        match self.doc.try_borrow_mut() {
308            Ok(mut doc) => apply_focus(&mut doc, self.node_id, focus),
309            Err(_) => queue_focus(&self.command_queue, self.node_id, focus),
310        }
311
312        Box::pin(async { Ok(()) })
313    }
314}
315
316#[derive(Clone, Debug)]
317pub struct NativeFormData {
318    pub value: String,
319    pub values: Vec<(String, FormValue)>,
320}
321
322impl HasFormData for NativeFormData {
323    fn as_any(&self) -> &dyn Any {
324        self as &dyn Any
325    }
326
327    fn value(&self) -> String {
328        self.value.clone()
329    }
330
331    fn values(&self) -> Vec<(String, FormValue)> {
332        self.values.clone()
333    }
334    fn valid(&self) -> bool {
335        // todo: actually implement validation here.
336        true
337    }
338}
339
340impl HasFileData for NativeFormData {
341    fn files(&self) -> Vec<dioxus_html::FileData> {
342        vec![]
343    }
344}
345
346#[derive(Clone, Debug)]
347pub(crate) struct BlitzKeyboardData(pub(crate) BlitzKeyEvent);
348
349impl ModifiersInteraction for BlitzKeyboardData {
350    fn modifiers(&self) -> Modifiers {
351        self.0.modifiers
352    }
353}
354
355impl HasKeyboardData for BlitzKeyboardData {
356    fn key(&self) -> Key {
357        self.0.key.clone()
358    }
359
360    fn code(&self) -> Code {
361        self.0.code
362    }
363
364    fn location(&self) -> Location {
365        self.0.location
366    }
367
368    fn is_auto_repeating(&self) -> bool {
369        self.0.is_auto_repeating
370    }
371
372    fn is_composing(&self) -> bool {
373        self.0.is_composing
374    }
375
376    fn as_any(&self) -> &dyn Any {
377        self as &dyn Any
378    }
379}
380
381#[derive(Clone)]
382pub struct NativePointerData(pub(crate) BlitzPointerEvent);
383
384impl InteractionLocation for NativePointerData {
385    fn client_coordinates(&self) -> ClientPoint {
386        ClientPoint::new(self.0.client_x() as f64, self.0.client_y() as f64)
387    }
388
389    fn screen_coordinates(&self) -> ScreenPoint {
390        ScreenPoint::new(self.0.screen_x() as f64, self.0.screen_y() as f64)
391    }
392
393    fn page_coordinates(&self) -> PagePoint {
394        PagePoint::new(self.0.page_x() as f64, self.0.page_y() as f64)
395    }
396}
397
398impl InteractionElementOffset for NativePointerData {
399    fn element_coordinates(&self) -> ElementPoint {
400        ElementPoint::new(self.0.element_x() as f64, self.0.element_y() as f64)
401    }
402}
403
404impl ModifiersInteraction for NativePointerData {
405    fn modifiers(&self) -> Modifiers {
406        self.0.mods
407    }
408}
409
410impl PointerInteraction for NativePointerData {
411    fn trigger_button(&self) -> Option<MouseButton> {
412        Some(match self.0.button {
413            MouseEventButton::Main => MouseButton::Primary,
414            MouseEventButton::Auxiliary => MouseButton::Auxiliary,
415            MouseEventButton::Secondary => MouseButton::Secondary,
416            MouseEventButton::Fourth => MouseButton::Fourth,
417            MouseEventButton::Fifth => MouseButton::Fifth,
418        })
419    }
420
421    fn held_buttons(&self) -> MouseButtonSet {
422        dioxus_html::input_data::decode_mouse_button_set(self.0.buttons.bits() as u16)
423    }
424}
425impl HasMouseData for NativePointerData {
426    fn as_any(&self) -> &dyn Any {
427        self as &dyn Any
428    }
429}
430
431impl HasPointerData for NativePointerData {
432    fn as_any(&self) -> &dyn Any {
433        self as &dyn Any
434    }
435
436    fn is_primary(&self) -> bool {
437        self.0.is_primary
438    }
439
440    fn pointer_id(&self) -> i32 {
441        match self.0.id {
442            BlitzPointerId::Mouse => 0,
443            BlitzPointerId::Pen => 0,
444            BlitzPointerId::Finger(id) => id as i32,
445        }
446    }
447
448    fn pointer_type(&self) -> String {
449        match self.0.id {
450            BlitzPointerId::Mouse => String::from("mouse"),
451            BlitzPointerId::Pen => String::from("pen"),
452            BlitzPointerId::Finger(_) => String::from("touch"),
453        }
454    }
455
456    fn pressure(&self) -> f32 {
457        self.0.details.pressure as f32
458    }
459    fn tangential_pressure(&self) -> f32 {
460        self.0.details.tangential_pressure
461    }
462    fn tilt_x(&self) -> i32 {
463        self.0.details.tilt_x as i32
464    }
465    fn tilt_y(&self) -> i32 {
466        self.0.details.tilt_y as i32
467    }
468    fn twist(&self) -> i32 {
469        self.0.details.twist as i32
470    }
471
472    // TODO: implement these fields with real values
473    fn width(&self) -> f64 {
474        1.0
475    }
476    fn height(&self) -> f64 {
477        1.0
478    }
479}
480
481/// Touch event data exposed to Dioxus Native application code.
482///
483/// Blitz tracks input via pointer events, so a touch event is generated from the
484/// pointer event of the finger that triggered it (`touches_changed`). The full
485/// list of concurrent touches is carried on the triggering event's
486/// [`BlitzPointerEvent::active_pointers`] list and reported via `touches`.
487#[derive(Clone)]
488pub struct NativeTouchData(pub(crate) BlitzPointerEvent);
489
490impl ModifiersInteraction for NativeTouchData {
491    fn modifiers(&self) -> Modifiers {
492        self.0.mods
493    }
494}
495
496impl HasTouchData for NativeTouchData {
497    fn touches(&self) -> Vec<TouchPoint> {
498        // All pointers currently active on the surface (multi-touch).
499        self.0
500            .active_pointers
501            .borrow()
502            .iter()
503            .map(|event| TouchPoint::new(NativeTouchPointData(event.clone())))
504            .collect()
505    }
506
507    fn touches_changed(&self) -> Vec<TouchPoint> {
508        // Just the touch that triggered this event.
509        vec![TouchPoint::new(NativeTouchPointData(self.0.clone()))]
510    }
511
512    fn target_touches(&self) -> Vec<TouchPoint> {
513        // We don't track a per-touch target, so approximate `targetTouches`
514        // (touches that started on the event target) with all active touches.
515        self.touches()
516    }
517
518    fn as_any(&self) -> &dyn Any {
519        self as &dyn Any
520    }
521}
522
523#[derive(Clone)]
524pub struct NativeTouchPointData(BlitzPointerEvent);
525
526impl InteractionLocation for NativeTouchPointData {
527    fn client_coordinates(&self) -> ClientPoint {
528        ClientPoint::new(self.0.client_x() as f64, self.0.client_y() as f64)
529    }
530
531    fn screen_coordinates(&self) -> ScreenPoint {
532        ScreenPoint::new(self.0.screen_x() as f64, self.0.screen_y() as f64)
533    }
534
535    fn page_coordinates(&self) -> PagePoint {
536        PagePoint::new(self.0.page_x() as f64, self.0.page_y() as f64)
537    }
538}
539
540impl HasTouchPointData for NativeTouchPointData {
541    fn identifier(&self) -> i32 {
542        match self.0.id {
543            BlitzPointerId::Finger(id) => id as i32,
544            BlitzPointerId::Mouse | BlitzPointerId::Pen => 0,
545        }
546    }
547
548    fn force(&self) -> f64 {
549        self.0.details.pressure
550    }
551
552    fn radius(&self) -> ScreenPoint {
553        // TODO: expose real touch radius once blitz tracks it
554        ScreenPoint::new(1.0, 1.0)
555    }
556
557    fn rotation(&self) -> f64 {
558        // TODO: expose real touch rotation once blitz tracks it
559        0.0
560    }
561
562    fn as_any(&self) -> &dyn Any {
563        self as &dyn Any
564    }
565}
566
567#[derive(Clone)]
568pub struct NativeFocusData;
569impl HasFocusData for NativeFocusData {
570    fn as_any(&self) -> &dyn Any {
571        self as &dyn Any
572    }
573}
574
575#[derive(Clone)]
576pub struct NativeScrollData(pub(crate) BlitzScrollEvent);
577impl HasScrollData for NativeScrollData {
578    fn as_any(&self) -> &dyn Any {
579        self as &dyn Any
580    }
581
582    fn scroll_top(&self) -> f64 {
583        self.0.scroll_top
584    }
585
586    fn scroll_left(&self) -> f64 {
587        self.0.scroll_left
588    }
589
590    fn scroll_width(&self) -> i32 {
591        self.0.scroll_width
592    }
593
594    fn scroll_height(&self) -> i32 {
595        self.0.scroll_height
596    }
597
598    fn client_width(&self) -> i32 {
599        self.0.client_width
600    }
601
602    fn client_height(&self) -> i32 {
603        self.0.client_height
604    }
605}
606
607#[derive(Clone)]
608pub struct NativeWheelData(pub(crate) BlitzWheelEvent);
609impl HasWheelData for NativeWheelData {
610    fn as_any(&self) -> &dyn Any {
611        self as &dyn Any
612    }
613
614    fn delta(&self) -> WheelDelta {
615        match self.0.delta {
616            BlitzWheelDelta::Lines(x, y) => {
617                dioxus_html::geometry::WheelDelta::Lines(Vector3D::new(x, y, 0.0))
618            }
619            BlitzWheelDelta::Pixels(x, y) => {
620                dioxus_html::geometry::WheelDelta::Pixels(Vector3D::new(x, y, 0.0))
621            }
622        }
623    }
624}
625
626impl HasMouseData for NativeWheelData {
627    fn as_any(&self) -> &dyn Any {
628        self as &dyn Any
629    }
630}
631
632impl PointerInteraction for NativeWheelData {
633    fn trigger_button(&self) -> Option<MouseButton> {
634        None
635    }
636
637    fn held_buttons(&self) -> MouseButtonSet {
638        dioxus_html::input_data::decode_mouse_button_set(self.0.buttons.bits() as u16)
639    }
640}
641
642impl ModifiersInteraction for NativeWheelData {
643    fn modifiers(&self) -> Modifiers {
644        self.0.mods
645    }
646}
647
648impl InteractionElementOffset for NativeWheelData {
649    fn element_coordinates(&self) -> ElementPoint {
650        ElementPoint::new(self.0.element_x() as f64, self.0.element_y() as f64)
651    }
652}
653
654impl InteractionLocation for NativeWheelData {
655    fn client_coordinates(&self) -> ClientPoint {
656        ClientPoint::new(self.0.client_x() as f64, self.0.client_y() as f64)
657    }
658
659    fn screen_coordinates(&self) -> ScreenPoint {
660        ScreenPoint::new(self.0.screen_x() as f64, self.0.screen_y() as f64)
661    }
662
663    fn page_coordinates(&self) -> PagePoint {
664        PagePoint::new(self.0.page_x() as f64, self.0.page_y() as f64)
665    }
666}
667
668pub fn synthetic_click_event(node: &Node, modifiers: Modifiers) -> Box<dyn Any> {
669    Box::new(NativePointerData(
670        node.synthetic_click_event_data(modifiers),
671    ))
672}
673
674#[cfg(test)]
675mod tests {
676    use super::*;
677    use blitz_traits::events::{
678        BlitzPointerId, MouseEventButton, MouseEventButtons, Point, PointerCoords, PointerDetails,
679    };
680
681    fn finger_event(id: u64, x: f32, y: f32) -> BlitzPointerEvent {
682        BlitzPointerEvent {
683            id: BlitzPointerId::Finger(id),
684            is_primary: id == 0,
685            coords: PointerCoords {
686                page_x: x,
687                page_y: y,
688                screen_x: x,
689                screen_y: y,
690                client_x: x,
691                client_y: y,
692            },
693            button: MouseEventButton::Main,
694            buttons: MouseEventButtons::from(MouseEventButton::Main),
695            mods: Default::default(),
696            details: PointerDetails::default(),
697            element: Point::default(),
698            active_pointers: Default::default(),
699        }
700    }
701
702    #[test]
703    fn touches_reports_all_active_pointers() {
704        let f0 = finger_event(0, 10.0, 20.0);
705        let f1 = finger_event(1, 30.0, 40.0);
706
707        // The triggering event (second finger down) carries the list of all
708        // currently-active pointers.
709        let trigger = f1.clone();
710        {
711            let mut list = trigger.active_pointers.borrow_mut();
712            list.push(f0.clone());
713            list.push(f1.clone());
714        }
715
716        let data = NativeTouchData(trigger);
717
718        // `touches` reports every active pointer ...
719        let touches = data.touches();
720        assert_eq!(touches.len(), 2);
721        let coords: Vec<(f64, f64)> = touches
722            .iter()
723            .map(|t| {
724                let c = t.client_coordinates();
725                (c.x, c.y)
726            })
727            .collect();
728        assert!(coords.contains(&(10.0, 20.0)));
729        assert!(coords.contains(&(30.0, 40.0)));
730
731        // ... while `changed_touches` reports only the triggering touch.
732        assert_eq!(data.touches_changed().len(), 1);
733        let changed = data.touches_changed();
734        let changed_coords = changed[0].client_coordinates();
735        assert_eq!((changed_coords.x, changed_coords.y), (30.0, 40.0));
736    }
737
738    /// A focus request made while the document is borrowed must survive rather
739    /// than panic.
740    ///
741    /// This is not hypothetical: `DioxusDocument::poll` drives task wakeups
742    /// while holding the document borrow, so a component that asks for focus
743    /// from a spawned task lands here every time. Before the queue, this test
744    /// panicked with `RefCell already borrowed` on the `set_focus` line.
745    ///
746    /// The borrow below stands in for the one `poll` is holding — what matters
747    /// is only that the document is borrowed when the request is made.
748    #[test]
749    fn focus_requested_while_the_document_is_borrowed_is_applied_later() {
750        use blitz_dom::DocumentConfig;
751
752        let doc = Rc::new(RefCell::new(BaseDocument::new(DocumentConfig::default())));
753        let root_id = doc.borrow().root_node().id;
754        let handle = NodeHandle {
755            doc: Rc::clone(&doc),
756            command_queue: Rc::new(RefCell::new(Vec::new())),
757            node_id: root_id,
758        };
759
760        {
761            let _held = doc.borrow_mut();
762            // Must not panic, and must not apply yet: the borrow is held.
763            let _ = handle.set_focus(true);
764        }
765
766        assert_eq!(
767            doc.borrow().get_focussed_node_id(),
768            None,
769            "the request cannot have been applied while the document was borrowed"
770        );
771
772        flush_document_commands(&doc, &handle.command_queue);
773
774        assert_eq!(
775            doc.borrow().get_focussed_node_id(),
776            Some(root_id),
777            "the queued request should be applied once the borrow is released"
778        );
779    }
780
781    /// The uncontended path must not be made lazy by the queue: with nothing
782    /// holding the borrow, focus applies immediately and needs no flush.
783    #[test]
784    fn focus_applies_immediately_when_the_document_is_free() {
785        use blitz_dom::DocumentConfig;
786
787        let doc = Rc::new(RefCell::new(BaseDocument::new(DocumentConfig::default())));
788        let root_id = doc.borrow().root_node().id;
789        let handle = NodeHandle {
790            doc: Rc::clone(&doc),
791            command_queue: Rc::new(RefCell::new(Vec::new())),
792            node_id: root_id,
793        };
794
795        let _ = handle.set_focus(true);
796
797        assert_eq!(doc.borrow().get_focussed_node_id(), Some(root_id));
798    }
799
800    #[test]
801    fn flushing_one_document_does_not_apply_another_documents_focus() {
802        use blitz_dom::DocumentConfig;
803
804        let first = Rc::new(RefCell::new(BaseDocument::new(DocumentConfig::default())));
805        let second = Rc::new(RefCell::new(BaseDocument::new(DocumentConfig::default())));
806        let first_id = first.borrow().root_node().id;
807        let second_id = second.borrow().root_node().id;
808        let first_queue = Rc::new(RefCell::new(Vec::new()));
809        let second_queue = Rc::new(RefCell::new(Vec::new()));
810        let first_handle = NodeHandle {
811            doc: Rc::clone(&first),
812            command_queue: Rc::clone(&first_queue),
813            node_id: first_id,
814        };
815        let second_handle = NodeHandle {
816            doc: Rc::clone(&second),
817            command_queue: Rc::clone(&second_queue),
818            node_id: second_id,
819        };
820
821        {
822            let _first_borrow = first.borrow_mut();
823            let _second_borrow = second.borrow_mut();
824            let _ = first_handle.set_focus(true);
825            let _ = second_handle.set_focus(true);
826        }
827
828        // This stands in for polling only the first DioxusDocument. A global
829        // command queue incorrectly drains the second document's command too.
830        flush_document_commands(&first, &first_queue);
831
832        assert_eq!(first.borrow().get_focussed_node_id(), Some(first_id));
833        assert_eq!(
834            second.borrow().get_focussed_node_id(),
835            None,
836            "polling one document must not execute another document's commands",
837        );
838    }
839
840    #[test]
841    fn touches_is_empty_when_no_pointers_are_active() {
842        // e.g. a `touchend` for the last finger: it has been removed from the
843        // active list before dispatch, so `touches` is empty but
844        // `changed_touches` still reports the finger that ended.
845        let data = NativeTouchData(finger_event(0, 1.0, 2.0));
846        assert!(data.touches().is_empty());
847        assert_eq!(data.touches_changed().len(), 1);
848    }
849}