dioxus_native_dom/
dioxus_document.rs

1//! Integration between Dioxus and Blitz
2use crate::events::{BlitzKeyboardData, NativeClickData, NativeConverter, NativeFormData};
3use crate::mutation_writer::{DioxusState, MutationWriter};
4use crate::qual_name;
5use crate::NodeId;
6use blitz_dom::{
7    Attribute, BaseDocument, Document, DocumentConfig, EventDriver, EventHandler, Node, DEFAULT_CSS,
8};
9use blitz_traits::events::{DomEvent, DomEventData, EventState, UiEvent};
10use dioxus_core::{ElementId, Event, VirtualDom};
11use dioxus_html::{set_event_converter, PlatformEventData};
12use futures_util::task::noop_waker;
13use futures_util::{pin_mut, FutureExt};
14use std::ops::{Deref, DerefMut};
15use std::sync::LazyLock;
16use std::task::{Context as TaskContext, Waker};
17use std::{any::Any, rc::Rc};
18
19fn wrap_event_data<T: Any>(value: T) -> Rc<dyn Any> {
20    Rc::new(PlatformEventData::new(Box::new(value)))
21}
22
23/// Get the value of the "dioxus-data-id" attribute parsed aa usize
24fn get_dioxus_id(node: &Node) -> Option<ElementId> {
25    node.element_data()?
26        .attrs
27        .iter()
28        .find(|attr| *attr.name.local == *"data-dioxus-id")
29        .and_then(|attr| attr.value.parse::<usize>().ok())
30        .map(ElementId)
31}
32
33/// Integrates [`BaseDocument`] from  [`blitz-dom`](blitz_dom)  with [`VirtualDom`] from [`dioxus-core`](dioxus_core)
34///
35/// ### Example
36///
37/// ```rust
38/// use blitz_traits::shell::{Viewport, ColorScheme};
39/// use dioxus_native_dom::{DioxusDocument, DocumentConfig};
40/// use dioxus::prelude::*;
41///
42/// // Example Dioxus app
43/// fn app() -> Element {
44///     rsx! {
45///         div { "Hello, world!" }
46///     }
47/// }
48///
49/// fn main() {
50///    let vdom = VirtualDom::new(app);
51///    let mut doc = DioxusDocument::new(vdom, DocumentConfig {
52///         viewport: Some(Viewport::new(800, 600, 1.0, ColorScheme::Light)),
53///         ..Default::default()
54///    });
55///    doc.initial_build();
56/// }
57/// ```
58///
59/// You can just push events into the [`DioxusDocument`] with [`doc.handle_ui_event(..)`](Self::handle_ui_event)
60/// and then flush the changes with [`doc.poll(..)`](Self::poll)
61pub struct DioxusDocument {
62    pub inner: BaseDocument,
63    pub vdom: VirtualDom,
64    pub vdom_state: DioxusState,
65
66    #[allow(unused)]
67    pub(crate) html_element_id: NodeId,
68    #[allow(unused)]
69    pub(crate) head_element_id: NodeId,
70    #[allow(unused)]
71    pub(crate) body_element_id: NodeId,
72    #[allow(unused)]
73    pub(crate) main_element_id: NodeId,
74}
75
76impl DioxusDocument {
77    /// Create a new [`DioxusDocument`] from a [`VirtualDom`].
78    pub fn new(vdom: VirtualDom, mut config: DocumentConfig) -> Self {
79        // Only really needs to happen once
80        set_event_converter(Box::new(NativeConverter {}));
81
82        config.base_url = Some(
83            config
84                .base_url
85                .unwrap_or_else(|| String::from("dioxus://index.html")),
86        );
87        let mut doc = BaseDocument::new(config);
88
89        // Include default stylesheet
90        doc.add_user_agent_stylesheet(DEFAULT_CSS);
91
92        // Create some minimal HTML to render the app into.
93        // HTML is equivalent to:
94        //
95        // <html>
96        // <head></head>
97        // <body>
98        //    <div id="main"></div>
99        // </body>
100        // </html>
101        //
102        // TODO: Support arbitrary "index.html" templates
103
104        // Create the html element
105        let mut mutr = doc.mutate();
106        let html_element_id = mutr.create_element(qual_name("html", None), vec![]);
107        mutr.append_children(mutr.doc.root_node().id, &[html_element_id]);
108
109        // Create the body element
110        let head_element_id = mutr.create_element(qual_name("head", None), vec![]);
111        mutr.append_children(html_element_id, &[head_element_id]);
112
113        // Create the body element
114        let body_element_id = mutr.create_element(qual_name("body", None), vec![]);
115        mutr.append_children(html_element_id, &[body_element_id]);
116
117        // Create another virtual element to hold the root <div id="main"></div> under the html element
118        let main_attr = blitz_dom::Attribute {
119            name: qual_name("id", None),
120            value: "main".to_string(),
121        };
122        let main_element_id = mutr.create_element(qual_name("main", None), vec![main_attr]);
123        mutr.append_children(body_element_id, &[main_element_id]);
124
125        drop(mutr);
126
127        let vdom_state = DioxusState::create(main_element_id);
128        Self {
129            vdom,
130            vdom_state,
131            inner: doc,
132            html_element_id,
133            head_element_id,
134            body_element_id,
135            main_element_id,
136        }
137    }
138
139    /// Run an initial build of the Dioxus vdom
140    pub fn initial_build(&mut self) {
141        let mut writer = MutationWriter::new(&mut self.inner, &mut self.vdom_state);
142        self.vdom.rebuild(&mut writer);
143    }
144
145    /// Used to respond to a `CreateHeadElement` event generated by Dioxus. These
146    /// events allow Dioxus to create elements in the `<head>` of the document.
147    #[doc(hidden)]
148    pub fn create_head_element(
149        &mut self,
150        name: &str,
151        attributes: &[(String, String)],
152        contents: &Option<String>,
153    ) {
154        let mut mutr = self.inner.mutate();
155
156        let attributes = attributes
157            .iter()
158            .map(|(name, value)| Attribute {
159                name: qual_name(name, None),
160                value: value.clone(),
161            })
162            .collect();
163
164        let new_elem_id = mutr.create_element(qual_name(name, None), attributes);
165        mutr.append_children(self.head_element_id, &[new_elem_id]);
166        if let Some(contents) = contents {
167            let text_node_id = mutr.create_text_node(contents);
168            mutr.append_children(new_elem_id, &[text_node_id]);
169        }
170    }
171}
172
173// Implement DocumentLike and required traits for DioxusDocument
174impl Document for DioxusDocument {
175    fn id(&self) -> usize {
176        self.inner.id()
177    }
178
179    fn as_any_mut(&mut self) -> &mut dyn Any {
180        self
181    }
182
183    fn poll(&mut self, cx: Option<TaskContext>) -> bool {
184        {
185            let fut = self.vdom.wait_for_work();
186            pin_mut!(fut);
187
188            static NOOP_WAKER: LazyLock<Waker> = LazyLock::new(noop_waker);
189            let mut cx = cx.unwrap_or_else(|| TaskContext::from_waker(&NOOP_WAKER));
190            match fut.poll_unpin(&mut cx) {
191                std::task::Poll::Ready(_) => {}
192                std::task::Poll::Pending => return false,
193            }
194        }
195
196        let mut writer = MutationWriter::new(&mut self.inner, &mut self.vdom_state);
197        self.vdom.render_immediate(&mut writer);
198
199        true
200    }
201
202    fn handle_ui_event(&mut self, event: UiEvent) {
203        let handler = DioxusEventHandler {
204            vdom: &mut self.vdom,
205            vdom_state: &mut self.vdom_state,
206        };
207        let mut driver = EventDriver::new(self.inner.mutate(), handler);
208        driver.handle_ui_event(event);
209    }
210}
211
212impl Deref for DioxusDocument {
213    type Target = BaseDocument;
214    fn deref(&self) -> &BaseDocument {
215        &self.inner
216    }
217}
218impl DerefMut for DioxusDocument {
219    fn deref_mut(&mut self) -> &mut Self::Target {
220        &mut self.inner
221    }
222}
223impl From<DioxusDocument> for BaseDocument {
224    fn from(doc: DioxusDocument) -> BaseDocument {
225        doc.inner
226    }
227}
228
229pub struct DioxusEventHandler<'v> {
230    vdom: &'v mut VirtualDom,
231    #[allow(dead_code, reason = "WIP")]
232    vdom_state: &'v mut DioxusState,
233}
234
235impl EventHandler for DioxusEventHandler<'_> {
236    fn handle_event(
237        &mut self,
238        chain: &[usize],
239        event: &mut DomEvent,
240        mutr: &mut blitz_dom::DocumentMutator<'_>,
241        event_state: &mut EventState,
242    ) {
243        // As an optimisation we maintain a count of the total number event handlers of a given type
244        // If this count is zero then we can skip handling that kind of event entirely.
245        let event_kind_idx = event.data.discriminant() as usize;
246        let event_kind_count = self.vdom_state.event_handler_counts[event_kind_idx];
247        if event_kind_count == 0 {
248            return;
249        }
250
251        let event_data = match &event.data {
252            DomEventData::MouseMove(mevent)
253            | DomEventData::MouseDown(mevent)
254            | DomEventData::MouseUp(mevent)
255            | DomEventData::Click(mevent) => Some(wrap_event_data(NativeClickData(mevent.clone()))),
256
257            DomEventData::KeyDown(kevent)
258            | DomEventData::KeyUp(kevent)
259            | DomEventData::KeyPress(kevent) => {
260                Some(wrap_event_data(BlitzKeyboardData(kevent.clone())))
261            }
262
263            DomEventData::Input(data) => Some(wrap_event_data(NativeFormData {
264                value: data.value.clone(),
265                values: vec![],
266            })),
267
268            // TODO: Implement IME handling
269            DomEventData::Ime(_) => None,
270        };
271
272        let Some(event_data) = event_data else {
273            return;
274        };
275
276        for &node_id in chain {
277            // Get dioxus vdom id for node
278            let dioxus_id = mutr.doc.get_node(node_id).and_then(get_dioxus_id);
279            let Some(id) = dioxus_id else {
280                continue;
281            };
282
283            // Handle event in vdom
284            let dx_event = Event::new(event_data.clone(), event.bubbles);
285            self.vdom
286                .runtime()
287                .handle_event(event.name(), dx_event.clone(), id);
288
289            // Update event state
290            if !dx_event.default_action_enabled() {
291                event_state.prevent_default();
292            }
293            if !dx_event.propagates() {
294                event_state.stop_propagation();
295                break;
296            }
297        }
298    }
299}