Skip to main content

dioxus_native_dom/
dioxus_document.rs

1//! Integration between Dioxus and Blitz
2use crate::NodeId;
3use crate::events::{
4    BlitzKeyboardData, NativeConverter, NativeFocusData, NativeFormData, NativePointerData,
5    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
72    #[allow(unused)]
73    pub(crate) html_element_id: NodeId,
74    #[allow(unused)]
75    pub(crate) head_element_id: NodeId,
76    #[allow(unused)]
77    pub(crate) body_element_id: NodeId,
78    #[allow(unused)]
79    pub(crate) main_element_id: NodeId,
80}
81
82impl DioxusDocument {
83    /// Create a new [`DioxusDocument`] from a [`VirtualDom`].
84    pub fn new(vdom: VirtualDom, mut config: DocumentConfig) -> Self {
85        // Only really needs to happen once
86        set_event_converter(Box::new(NativeConverter {}));
87
88        config.base_url = Some(
89            config
90                .base_url
91                .unwrap_or_else(|| String::from("dioxus://index.html")),
92        );
93        let mut doc = BaseDocument::new(config);
94
95        // Include default stylesheet
96        doc.add_user_agent_stylesheet(DEFAULT_CSS);
97
98        // Create some minimal HTML to render the app into.
99        // HTML is equivalent to:
100        //
101        // <html>
102        // <head></head>
103        // <body>
104        //    <div id="main"></div>
105        // </body>
106        // </html>
107        //
108        // TODO: Support arbitrary "index.html" templates
109
110        // Create the html element
111        let mut mutr = doc.mutate();
112        let html_element_id = mutr.create_element(qual_name("html", None), vec![]);
113        mutr.append_children(mutr.doc.root_node().id, &[html_element_id]);
114
115        // Create the body element
116        let head_element_id = mutr.create_element(qual_name("head", None), vec![]);
117        mutr.append_children(html_element_id, &[head_element_id]);
118
119        // Create the body element
120        let body_element_id = mutr.create_element(qual_name("body", None), vec![]);
121        mutr.append_children(html_element_id, &[body_element_id]);
122
123        // Create another virtual element to hold the root <div id="main"></div> under the html element
124        let main_attr = blitz_dom::Attribute {
125            name: qual_name("id", None),
126            value: "main".to_string(),
127        };
128        let main_element_id = mutr.create_element(qual_name("main", None), vec![main_attr]);
129        mutr.append_children(body_element_id, &[main_element_id]);
130
131        drop(mutr);
132
133        let vdom_state = DioxusState::create(main_element_id);
134        Self {
135            vdom,
136            vdom_state,
137            inner: Rc::new(RefCell::new(doc)),
138            html_element_id,
139            head_element_id,
140            body_element_id,
141            main_element_id,
142        }
143    }
144
145    /// Run an initial build of the Dioxus vdom
146    pub fn initial_build(&mut self) {
147        let mut inner = self.inner.borrow_mut();
148        let mut writer = MutationWriter::new(&mut inner, &mut self.vdom_state);
149        self.vdom.rebuild(&mut writer);
150        drop(writer);
151        drop(inner);
152        self.flush_queued_mounted_events();
153    }
154
155    /// Used to respond to a `CreateHeadElement` event generated by Dioxus. These
156    /// events allow Dioxus to create elements in the `<head>` of the document.
157    #[doc(hidden)]
158    pub fn create_head_element(
159        &mut self,
160        name: &str,
161        attributes: &[(String, String)],
162        contents: &Option<String>,
163    ) {
164        let mut inner = self.inner.borrow_mut();
165        let mut mutr = inner.mutate();
166
167        let attributes = attributes
168            .iter()
169            .map(|(name, value)| Attribute {
170                name: qual_name(name, None),
171                value: value.clone(),
172            })
173            .collect();
174
175        let new_elem_id = mutr.create_element(qual_name(name, None), attributes);
176        mutr.append_children(self.head_element_id, &[new_elem_id]);
177        if let Some(contents) = contents {
178            let text_node_id = mutr.create_text_node(contents);
179            mutr.append_children(new_elem_id, &[text_node_id]);
180        }
181    }
182
183    pub(crate) fn flush_queued_mounted_events(&mut self) {
184        let mut queued_mounted_events = mem::take(&mut self.vdom_state.queued_mounted_events);
185        for element_id in queued_mounted_events.drain(..) {
186            let node_id = self.vdom_state.element_to_node_id(element_id);
187
188            if self.inner.borrow().get_node(node_id).is_some() {
189                let event = Event::new(
190                    Rc::new(PlatformEventData::new(Box::new(NodeHandle {
191                        doc: Rc::clone(&self.inner),
192                        node_id,
193                    }))) as Rc<dyn Any>,
194                    false,
195                );
196                self.vdom
197                    .runtime()
198                    .handle_event("mounted", event, element_id);
199            }
200        }
201
202        self.vdom_state.queued_mounted_events = queued_mounted_events;
203    }
204}
205
206// Implement DocumentLike and required traits for DioxusDocument
207impl Document for DioxusDocument {
208    fn id(&self) -> usize {
209        self.inner.borrow().id()
210    }
211
212    fn inner(&self) -> DocGuard<'_> {
213        DocGuard::RefCell(self.inner.borrow())
214    }
215
216    fn inner_mut(&mut self) -> DocGuardMut<'_> {
217        DocGuardMut::RefCell(self.inner.borrow_mut())
218    }
219
220    fn poll(&mut self, cx: Option<TaskContext>) -> bool {
221        {
222            let fut = self.vdom.wait_for_work();
223            let mut pinned_fut = pin!(fut);
224
225            static NOOP_WAKER: LazyLock<Waker> = LazyLock::new(noop_waker);
226            let mut cx = cx.unwrap_or_else(|| TaskContext::from_waker(&NOOP_WAKER));
227            match pinned_fut.as_mut().poll(&mut cx) {
228                std::task::Poll::Ready(_) => {}
229                std::task::Poll::Pending => return false,
230            }
231        }
232
233        let mut inner = self.inner.borrow_mut();
234        let mut writer = MutationWriter::new(&mut inner, &mut self.vdom_state);
235        self.vdom.render_immediate(&mut writer);
236        drop(writer);
237        drop(inner);
238        self.flush_queued_mounted_events();
239
240        true
241    }
242
243    fn handle_ui_event(&mut self, event: UiEvent) {
244        let handler = DioxusEventHandler {
245            vdom: &mut self.vdom,
246            vdom_state: &mut self.vdom_state,
247        };
248        let mut driver = EventDriver::new(&mut self.inner, handler);
249        driver.handle_ui_event(event);
250    }
251}
252
253pub struct DioxusEventHandler<'v> {
254    vdom: &'v mut VirtualDom,
255    vdom_state: &'v mut DioxusState,
256}
257
258impl EventHandler for DioxusEventHandler<'_> {
259    fn handle_event(
260        &mut self,
261        chain: &[usize],
262        event: &mut DomEvent,
263        doc: &mut dyn Document,
264        event_state: &mut EventState,
265    ) {
266        // As an optimisation we maintain a count of the total number event handlers of a given type
267        // If this count is zero then we can skip handling that kind of event entirely.
268        let event_kind_idx = event.data.discriminant() as usize;
269        let event_kind_count = self.vdom_state.event_handler_counts[event_kind_idx];
270        if event_kind_count == 0 {
271            return;
272        }
273
274        let event_data = match &event.data {
275            DomEventData::PointerMove(mevent)
276            | DomEventData::PointerDown(mevent)
277            | DomEventData::PointerUp(mevent)
278            | DomEventData::PointerCancel(mevent)
279            | DomEventData::PointerLeave(mevent)
280            | DomEventData::PointerEnter(mevent)
281            | DomEventData::PointerOver(mevent)
282            | DomEventData::PointerOut(mevent)
283            | DomEventData::MouseMove(mevent)
284            | DomEventData::MouseDown(mevent)
285            | DomEventData::MouseUp(mevent)
286            | DomEventData::MouseLeave(mevent)
287            | DomEventData::MouseEnter(mevent)
288            | DomEventData::MouseOver(mevent)
289            | DomEventData::MouseOut(mevent)
290            | DomEventData::Click(mevent)
291            | DomEventData::ContextMenu(mevent)
292            | DomEventData::DoubleClick(mevent) => {
293                Some(wrap_event_data(NativePointerData(mevent.clone())))
294            }
295
296            DomEventData::TouchStart(tevent)
297            | DomEventData::TouchMove(tevent)
298            | DomEventData::TouchEnd(tevent)
299            | DomEventData::TouchCancel(tevent) => {
300                Some(wrap_event_data(NativeTouchData(tevent.clone())))
301            }
302
303            DomEventData::Scroll(sevent) => Some(wrap_event_data(NativeScrollData(sevent.clone()))),
304            DomEventData::Wheel(wevent) => Some(wrap_event_data(NativeWheelData(wevent.clone()))),
305
306            DomEventData::Focus(_) => Some(wrap_event_data(NativeFocusData)),
307            DomEventData::Blur(_) => Some(wrap_event_data(NativeFocusData)),
308            DomEventData::FocusIn(_) => Some(wrap_event_data(NativeFocusData)),
309            DomEventData::FocusOut(_) => Some(wrap_event_data(NativeFocusData)),
310
311            DomEventData::KeyDown(kevent)
312            | DomEventData::KeyUp(kevent)
313            | DomEventData::KeyPress(kevent) => {
314                Some(wrap_event_data(BlitzKeyboardData(kevent.clone())))
315            }
316
317            DomEventData::Input(data) => Some(wrap_event_data(NativeFormData {
318                value: data.value.clone(),
319                values: vec![],
320            })),
321
322            // TODO: Implement IME handling
323            DomEventData::Ime(_) => None,
324
325            // AppleStandardKeybinding events are not exposed to script
326            DomEventData::AppleStandardKeybinding(_) => None,
327        };
328
329        let Some(event_data) = event_data else {
330            return;
331        };
332
333        // Get dioxus vdom id for node
334        let dioxus_id = chain
335            .iter()
336            .find_map(|node_id| doc.inner().get_node(*node_id).and_then(get_dioxus_id));
337        let Some(id) = dioxus_id else {
338            return;
339        };
340
341        // Handle event in vdom
342        let dx_event = Event::new(event_data.clone(), event.bubbles);
343        self.vdom
344            .runtime()
345            .handle_event(event.name(), dx_event.clone(), id);
346
347        // Update event state
348        if !dx_event.default_action_enabled() {
349            event_state.prevent_default();
350        }
351        if !dx_event.propagates() {
352            event_state.stop_propagation();
353        }
354    }
355}