Skip to main content

dioxus_native_dom/
dioxus_document.rs

1//! Integration between Dioxus and Blitz
2use crate::NodeId;
3use crate::events::{
4    BlitzKeyboardData, DocumentCommandQueue, NativeConverter, NativeFocusData, NativeFormData,
5    NativePointerData, NativeScrollData, NativeTouchData, NativeWheelData, NodeHandle,
6};
7use crate::mutation_writer::{DioxusState, MutationWriter};
8use crate::qual_name;
9use blitz_dom::{
10    Attribute, BaseDocument, DEFAULT_CSS, DocGuard, DocGuardMut, Document, DocumentConfig,
11    EventDriver, EventHandler, Node,
12};
13use blitz_traits::events::{DomEvent, DomEventData, EventState, UiEvent};
14use dioxus_core::{ElementId, Event, VirtualDom};
15use dioxus_html::{PlatformEventData, set_event_converter};
16use futures_util::task::noop_waker;
17use std::cell::RefCell;
18use std::future::Future;
19use std::mem;
20use std::pin::pin;
21use std::sync::LazyLock;
22use std::task::{Context as TaskContext, Waker};
23use std::{any::Any, rc::Rc};
24
25fn wrap_event_data<T: Any>(value: T) -> Rc<dyn Any> {
26    Rc::new(PlatformEventData::new(Box::new(value)))
27}
28
29/// Get the value of the "dioxus-data-id" attribute parsed aa usize
30fn get_dioxus_id(node: &Node) -> Option<ElementId> {
31    node.element_data()?
32        .attrs
33        .iter()
34        .find(|attr| *attr.name.local == *"data-dioxus-id")
35        .and_then(|attr| attr.value.parse::<usize>().ok())
36        .map(ElementId)
37}
38
39/// Integrates [`BaseDocument`] from  [`blitz-dom`](blitz_dom)  with [`VirtualDom`] from [`dioxus-core`](dioxus_core)
40///
41/// ### Example
42///
43/// ```rust
44/// use blitz_traits::shell::{Viewport, ColorScheme};
45/// use dioxus_native_dom::{DioxusDocument, DocumentConfig};
46/// use dioxus::prelude::*;
47///
48/// // Example Dioxus app
49/// fn app() -> Element {
50///     rsx! {
51///         div { "Hello, world!" }
52///     }
53/// }
54///
55/// fn main() {
56///    let vdom = VirtualDom::new(app);
57///    let mut doc = DioxusDocument::new(vdom, DocumentConfig {
58///         viewport: Some(Viewport::new(800, 600, 1.0, ColorScheme::Light)),
59///         ..Default::default()
60///    });
61///    doc.initial_build();
62/// }
63/// ```
64///
65/// You can just push events into the [`DioxusDocument`] with [`doc.handle_ui_event(..)`](Self::handle_ui_event)
66/// and then flush the changes with [`doc.poll(..)`](Self::poll)
67pub struct DioxusDocument {
68    pub inner: Rc<RefCell<BaseDocument>>,
69    pub vdom: VirtualDom,
70    pub vdom_state: DioxusState,
71    command_queue: DocumentCommandQueue,
72
73    #[allow(unused)]
74    pub(crate) html_element_id: NodeId,
75    #[allow(unused)]
76    pub(crate) head_element_id: NodeId,
77    #[allow(unused)]
78    pub(crate) body_element_id: NodeId,
79    #[allow(unused)]
80    pub(crate) main_element_id: NodeId,
81}
82
83impl DioxusDocument {
84    /// Create a new [`DioxusDocument`] from a [`VirtualDom`].
85    pub fn new(vdom: VirtualDom, mut config: DocumentConfig) -> Self {
86        // Only really needs to happen once
87        set_event_converter(Box::new(NativeConverter {}));
88
89        config.base_url = Some(
90            config
91                .base_url
92                .unwrap_or_else(|| String::from("dioxus://index.html")),
93        );
94        let mut doc = BaseDocument::new(config);
95
96        // Include default stylesheet
97        doc.add_user_agent_stylesheet(DEFAULT_CSS);
98
99        // Create some minimal HTML to render the app into.
100        // HTML is equivalent to:
101        //
102        // <html>
103        // <head></head>
104        // <body>
105        //    <div id="main"></div>
106        // </body>
107        // </html>
108        //
109        // TODO: Support arbitrary "index.html" templates
110
111        // Create the html element
112        let mut mutr = doc.mutate();
113        let html_element_id = mutr.create_element(qual_name("html", None), vec![]);
114        mutr.append_children(mutr.doc.root_node().id, &[html_element_id]);
115
116        // Create the body element
117        let head_element_id = mutr.create_element(qual_name("head", None), vec![]);
118        mutr.append_children(html_element_id, &[head_element_id]);
119
120        // Create the body element
121        let body_element_id = mutr.create_element(qual_name("body", None), vec![]);
122        mutr.append_children(html_element_id, &[body_element_id]);
123
124        // Create another virtual element to hold the root <div id="main"></div> under the html element
125        let main_attr = blitz_dom::Attribute {
126            name: qual_name("id", None),
127            value: "main".into(),
128        };
129        let main_element_id = mutr.create_element(qual_name("main", None), vec![main_attr]);
130        mutr.append_children(body_element_id, &[main_element_id]);
131
132        drop(mutr);
133
134        let vdom_state = DioxusState::create(main_element_id);
135        Self {
136            vdom,
137            vdom_state,
138            inner: Rc::new(RefCell::new(doc)),
139            command_queue: Rc::new(RefCell::new(Vec::new())),
140            html_element_id,
141            head_element_id,
142            body_element_id,
143            main_element_id,
144        }
145    }
146
147    /// Run an initial build of the Dioxus vdom
148    pub fn initial_build(&mut self) {
149        let mut inner = self.inner.borrow_mut();
150        let mut writer = MutationWriter::new(&mut inner, &mut self.vdom_state);
151        self.vdom.rebuild(&mut writer);
152        drop(writer);
153        drop(inner);
154        self.flush_queued_mounted_events();
155    }
156
157    /// Used to respond to a `CreateHeadElement` event generated by Dioxus. These
158    /// events allow Dioxus to create elements in the `<head>` of the document.
159    #[doc(hidden)]
160    pub fn create_head_element(
161        &mut self,
162        name: &str,
163        attributes: &[(String, String)],
164        contents: &Option<String>,
165    ) {
166        let mut inner = self.inner.borrow_mut();
167        let mut mutr = inner.mutate();
168
169        let attributes = attributes
170            .iter()
171            .map(|(name, value)| Attribute {
172                name: qual_name(name, None),
173                value: value.as_str().into(),
174            })
175            .collect();
176
177        let new_elem_id = mutr.create_element(qual_name(name, None), attributes);
178        mutr.append_children(self.head_element_id, &[new_elem_id]);
179        if let Some(contents) = contents {
180            let text_node_id = mutr.create_text_node(contents);
181            mutr.append_children(new_elem_id, &[text_node_id]);
182        }
183    }
184
185    pub(crate) fn flush_queued_mounted_events(&mut self) {
186        let mut queued_mounted_events = mem::take(&mut self.vdom_state.queued_mounted_events);
187        for element_id in queued_mounted_events.drain(..) {
188            let node_id = self.vdom_state.element_to_node_id(element_id);
189
190            if self.inner.borrow().get_node(node_id).is_some() {
191                let event = Event::new(
192                    Rc::new(PlatformEventData::new(Box::new(NodeHandle {
193                        doc: Rc::clone(&self.inner),
194                        command_queue: Rc::clone(&self.command_queue),
195                        node_id,
196                    }))) as Rc<dyn Any>,
197                    false,
198                );
199                self.vdom
200                    .runtime()
201                    .handle_event("mounted", event, element_id);
202            }
203        }
204
205        self.vdom_state.queued_mounted_events = queued_mounted_events;
206    }
207}
208
209// Implement DocumentLike and required traits for DioxusDocument
210impl Document for DioxusDocument {
211    fn id(&self) -> usize {
212        self.inner.borrow().id()
213    }
214
215    fn inner(&self) -> DocGuard<'_> {
216        DocGuard::RefCell(self.inner.borrow())
217    }
218
219    fn inner_mut(&mut self) -> DocGuardMut<'_> {
220        DocGuardMut::RefCell(self.inner.borrow_mut())
221    }
222
223    fn poll(&mut self, cx: Option<TaskContext>) -> bool {
224        static NOOP_WAKER: LazyLock<Waker> = LazyLock::new(noop_waker);
225        let waker = cx
226            .as_ref()
227            .map(|cx| cx.waker().clone())
228            .unwrap_or_else(|| NOOP_WAKER.clone());
229
230        // Poll any sub-documents, which may have pending async operations of
231        // their own (e.g. JavaScript timers when the sub-document is a
232        // `ScriptDocument` from blitz-script)
233        let subdoc_changes = self.inner.borrow_mut().poll_subdocuments(Some(&waker));
234
235        {
236            let fut = self.vdom.wait_for_work();
237            let mut pinned_fut = pin!(fut);
238
239            let mut cx = TaskContext::from_waker(&waker);
240            let ready = pinned_fut.as_mut().poll(&mut cx);
241            // Tasks woken above may have asked for focus. `set_focus` cannot take
242            // the document while a poll holds it, so it records the request
243            // instead; this is the first point where the borrow is free again.
244            //
245            // Applied on the pending path too, and before the early return:
246            // a request made on an otherwise idle frame would otherwise wait for
247            // whatever happens to poll next, which on a static page is nothing.
248            crate::events::flush_document_commands(&self.inner, &self.command_queue);
249            match ready {
250                std::task::Poll::Ready(_) => {}
251                std::task::Poll::Pending => return subdoc_changes,
252            }
253        }
254
255        let mut inner = self.inner.borrow_mut();
256        let mut writer = MutationWriter::new(&mut inner, &mut self.vdom_state);
257        self.vdom.render_immediate(&mut writer);
258        drop(writer);
259        drop(inner);
260        self.flush_queued_mounted_events();
261        // Again after the render: a `mounted` handler is the usual place an
262        // element asks for focus as soon as it exists, and those run above.
263        crate::events::flush_document_commands(&self.inner, &self.command_queue);
264
265        true
266    }
267
268    fn handle_ui_event(&mut self, event: UiEvent) {
269        let handler = DioxusEventHandler {
270            vdom: &mut self.vdom,
271            vdom_state: &mut self.vdom_state,
272        };
273        let mut driver = EventDriver::new(&mut self.inner, handler);
274        driver.handle_ui_event(event);
275    }
276}
277
278pub struct DioxusEventHandler<'v> {
279    vdom: &'v mut VirtualDom,
280    vdom_state: &'v mut DioxusState,
281}
282
283impl EventHandler for DioxusEventHandler<'_> {
284    fn handle_event(
285        &mut self,
286        chain: &[NodeId],
287        event: &mut DomEvent,
288        doc: &mut dyn Document,
289        event_state: &mut EventState,
290    ) {
291        // As an optimisation we maintain a count of the total number event handlers of a given type
292        // If this count is zero then we can skip handling that kind of event entirely.
293        let event_kind_idx = event.data.discriminant() as usize;
294        let event_kind_count = self.vdom_state.event_handler_counts[event_kind_idx];
295        if event_kind_count == 0 {
296            return;
297        }
298
299        let event_data = match &event.data {
300            DomEventData::PointerMove(mevent)
301            | DomEventData::PointerDown(mevent)
302            | DomEventData::PointerUp(mevent)
303            | DomEventData::PointerCancel(mevent)
304            | DomEventData::PointerLeave(mevent)
305            | DomEventData::PointerEnter(mevent)
306            | DomEventData::PointerOver(mevent)
307            | DomEventData::PointerOut(mevent)
308            | DomEventData::MouseMove(mevent)
309            | DomEventData::MouseDown(mevent)
310            | DomEventData::MouseUp(mevent)
311            | DomEventData::MouseLeave(mevent)
312            | DomEventData::MouseEnter(mevent)
313            | DomEventData::MouseOver(mevent)
314            | DomEventData::MouseOut(mevent)
315            | DomEventData::Click(mevent)
316            | DomEventData::ContextMenu(mevent)
317            | DomEventData::DoubleClick(mevent) => {
318                Some(wrap_event_data(NativePointerData(mevent.clone())))
319            }
320
321            DomEventData::TouchStart(tevent)
322            | DomEventData::TouchMove(tevent)
323            | DomEventData::TouchEnd(tevent)
324            | DomEventData::TouchCancel(tevent) => {
325                Some(wrap_event_data(NativeTouchData(tevent.clone())))
326            }
327
328            DomEventData::Scroll(sevent) => Some(wrap_event_data(NativeScrollData(sevent.clone()))),
329            DomEventData::Wheel(wevent) => Some(wrap_event_data(NativeWheelData(wevent.clone()))),
330
331            DomEventData::Focus(_) => Some(wrap_event_data(NativeFocusData)),
332            DomEventData::Blur(_) => Some(wrap_event_data(NativeFocusData)),
333            DomEventData::FocusIn(_) => Some(wrap_event_data(NativeFocusData)),
334            DomEventData::FocusOut(_) => Some(wrap_event_data(NativeFocusData)),
335
336            DomEventData::KeyDown(kevent)
337            | DomEventData::KeyUp(kevent)
338            | DomEventData::KeyPress(kevent) => {
339                Some(wrap_event_data(BlitzKeyboardData(kevent.clone())))
340            }
341
342            DomEventData::Input(data) => Some(wrap_event_data(NativeFormData {
343                value: data.value.clone(),
344                values: vec![],
345            })),
346
347            // TODO: Implement IME handling
348            DomEventData::Ime(_) => None,
349
350            // AppleStandardKeybinding events are not exposed to script
351            DomEventData::AppleStandardKeybinding(_) => None,
352            // Dioxus has no matching event payload for a submission, and the
353            // default action still runs, so a component that wants one listens
354            // for the click on its button.
355            DomEventData::Submit(_) => None,
356        };
357
358        let Some(event_data) = event_data else {
359            return;
360        };
361
362        // Get dioxus vdom id for node
363        let dioxus_id = chain
364            .iter()
365            .find_map(|node_id| doc.inner().get_node(*node_id).and_then(get_dioxus_id));
366        let Some(id) = dioxus_id else {
367            return;
368        };
369
370        // Handle event in vdom
371        let dx_event = Event::new(event_data.clone(), event.bubbles);
372        self.vdom
373            .runtime()
374            .handle_event(event.name(), dx_event.clone(), id);
375
376        // Update event state
377        if !dx_event.default_action_enabled() {
378            event_state.prevent_default();
379        }
380        if !dx_event.propagates() {
381            event_state.stop_propagation();
382        }
383    }
384}
385
386#[cfg(test)]
387mod tests {
388    use super::*;
389    use blitz_dom::DocumentConfig;
390    use dioxus::prelude::*;
391    use dioxus_core::ScopeId;
392    use std::cell::RefCell;
393    use std::collections::HashMap;
394
395    #[test]
396    // Regression test for a panic. The keyed `div`s are re-ordered as the (unordered) `HashMap` grows,
397    // which previously caused a crash when moving keyed nodes within their parent.
398    fn keyed_nodes_do_not_crash() {
399        type SharedData = Rc<RefCell<HashMap<usize, usize>>>;
400        let data: SharedData = Rc::new(RefCell::new(HashMap::new()));
401
402        fn app(data: SharedData) -> Element {
403            let entries: Vec<usize> = data.borrow().keys().copied().collect();
404            rsx!(
405                for id in entries {
406                    div {
407                        key: "item_{id}",
408                        "{id}"
409                    }
410                }
411            )
412        }
413
414        let vdom = VirtualDom::new_with_props(app, Rc::clone(&data));
415        let mut doc = DioxusDocument::new(vdom, DocumentConfig::default());
416        doc.initial_build();
417
418        // Mirror `examples/crash.rs`: incrementally insert 100 items, flushing
419        // the resulting mutations after each insert.
420        for i in 0..100 {
421            data.borrow_mut().insert(i, i);
422            doc.vdom.mark_dirty(ScopeId::APP);
423            doc.poll(None);
424        }
425
426        // The `<main>` element should end up with exactly one keyed `div` per
427        // inserted item, and applying the mutations must not have panicked.
428        let inner = doc.inner.borrow();
429        let main = inner.get_node(doc.main_element_id).unwrap();
430        assert_eq!(main.children.len(), 100);
431    }
432}