dioxus_native/
dioxus_document.rs

1//! Integration between Dioxus and Blitz
2
3use std::{any::Any, collections::HashMap, rc::Rc, sync::Arc};
4
5use blitz_dom::{
6    local_name, namespace_url,
7    net::Resource,
8    node::{Attribute, NodeSpecificData},
9    ns, Atom, BaseDocument, ElementNodeData, Node, NodeData, QualName, DEFAULT_CSS,
10};
11
12use blitz_traits::{net::NetProvider, ColorScheme, Document, DomEvent, DomEventData, Viewport};
13use dioxus_core::{ElementId, Event, VirtualDom};
14use dioxus_html::{set_event_converter, FormValue, PlatformEventData};
15use futures_util::{pin_mut, FutureExt};
16
17use super::event_handler::{BlitzKeyboardData, NativeClickData, NativeConverter, NativeFormData};
18use crate::mutation_writer::{DioxusState, MutationWriter};
19use crate::NodeId;
20
21pub(crate) fn qual_name(local_name: &str, namespace: Option<&str>) -> QualName {
22    QualName {
23        prefix: None,
24        ns: namespace.map(Atom::from).unwrap_or(ns!(html)),
25        local: Atom::from(local_name),
26    }
27}
28
29pub struct DioxusDocument {
30    pub(crate) vdom: VirtualDom,
31    pub(crate) vdom_state: DioxusState,
32    pub(crate) inner: BaseDocument,
33
34    #[allow(unused)]
35    pub(crate) html_element_id: NodeId,
36    #[allow(unused)]
37    pub(crate) head_element_id: NodeId,
38    #[allow(unused)]
39    pub(crate) body_element_id: NodeId,
40    #[allow(unused)]
41    pub(crate) main_element_id: NodeId,
42}
43
44// Implement DocumentLike and required traits for DioxusDocument
45
46impl AsRef<BaseDocument> for DioxusDocument {
47    fn as_ref(&self) -> &BaseDocument {
48        &self.inner
49    }
50}
51impl AsMut<BaseDocument> for DioxusDocument {
52    fn as_mut(&mut self) -> &mut BaseDocument {
53        &mut self.inner
54    }
55}
56impl From<DioxusDocument> for BaseDocument {
57    fn from(doc: DioxusDocument) -> BaseDocument {
58        doc.inner
59    }
60}
61impl Document for DioxusDocument {
62    type Doc = BaseDocument;
63
64    fn poll(&mut self, mut cx: std::task::Context) -> bool {
65        {
66            let fut = self.vdom.wait_for_work();
67            pin_mut!(fut);
68
69            match fut.poll_unpin(&mut cx) {
70                std::task::Poll::Ready(_) => {}
71                std::task::Poll::Pending => return false,
72            }
73        }
74
75        let mut writer = MutationWriter::new(&mut self.inner, &mut self.vdom_state);
76        self.vdom.render_immediate(&mut writer);
77
78        true
79    }
80
81    fn id(&self) -> usize {
82        self.inner.id()
83    }
84
85    fn handle_event(&mut self, event: &mut DomEvent) {
86        let chain = self.inner.node_chain(event.target);
87
88        set_event_converter(Box::new(NativeConverter {}));
89
90        let renderer_event = event.clone();
91
92        let mut prevent_default = false;
93        let mut stop_propagation = false;
94
95        match &event.data {
96            DomEventData::MouseMove(data)
97            | DomEventData::MouseDown(data)
98            | DomEventData::MouseUp(data) => {
99                let click_event_data = wrap_event_data(NativeClickData(data.clone()));
100
101                for node_id in chain.clone().into_iter() {
102                    let node = &self.inner.tree()[node_id];
103                    let dioxus_id = node.element_data().and_then(DioxusDocument::dioxus_id);
104
105                    if let Some(id) = dioxus_id {
106                        let click_event = Event::new(click_event_data.clone(), true);
107                        self.vdom
108                            .runtime()
109                            .handle_event(event.name(), click_event.clone(), id);
110                        prevent_default |= !click_event.default_action_enabled();
111                        stop_propagation |= !click_event.propagates();
112                    }
113
114                    if !event.bubbles || stop_propagation {
115                        break;
116                    }
117                }
118            }
119
120            DomEventData::Click(data) => {
121                let click_event_data = wrap_event_data(NativeClickData(data.clone()));
122
123                for node_id in chain.clone().into_iter() {
124                    let node = &self.inner.tree()[node_id];
125                    let dioxus_id = node.element_data().and_then(DioxusDocument::dioxus_id);
126                    let mut trigger_label = false;
127
128                    if let Some(id) = dioxus_id {
129                        // Trigger click event
130                        let click_event = Event::new(click_event_data.clone(), true);
131                        self.vdom
132                            .runtime()
133                            .handle_event("click", click_event.clone(), id);
134                        prevent_default |= !click_event.default_action_enabled();
135                        stop_propagation |= !click_event.propagates();
136
137                        if !prevent_default {
138                            let mut default_event =
139                                DomEvent::new(node_id, renderer_event.data.clone());
140                            self.inner.as_mut().handle_event(&mut default_event);
141                            prevent_default = true;
142
143                            let element = self.inner.tree()[node_id].element_data().unwrap();
144                            trigger_label = element.name.local == *"label";
145
146                            //TODO Check for other inputs which trigger input event on click here, eg radio
147                            let triggers_input_event = element.name.local == local_name!("input")
148                                && element.attr(local_name!("type")) == Some("checkbox");
149                            if triggers_input_event {
150                                let form_data =
151                                    wrap_event_data(self.input_event_form_data(&chain, element));
152                                let input_event = Event::new(form_data, true);
153                                self.vdom.runtime().handle_event("input", input_event, id);
154                            }
155                        }
156                    }
157
158                    //Clicking labels triggers click, and possibly input event, of bound input
159                    if trigger_label {
160                        if let Some((dioxus_id, node_id)) = self.label_bound_input_element(node_id)
161                        {
162                            let click_event = Event::new(click_event_data.clone(), true);
163                            self.vdom.runtime().handle_event(
164                                "click",
165                                click_event.clone(),
166                                dioxus_id,
167                            );
168
169                            // Handle default DOM event
170                            if click_event.default_action_enabled() {
171                                let DomEventData::Click(event) = &renderer_event.data else {
172                                    unreachable!();
173                                };
174                                let input_click_data = self
175                                    .inner
176                                    .get_node(node_id)
177                                    .unwrap()
178                                    .synthetic_click_event(event.mods);
179                                let mut default_event = DomEvent::new(node_id, input_click_data);
180                                self.inner.as_mut().handle_event(&mut default_event);
181                                prevent_default = true;
182
183                                // TODO: Generated click events should bubble separately
184                                // prevent_default |= !click_event.default_action_enabled();
185
186                                //TODO Check for other inputs which trigger input event on click here, eg radio
187                                let element_data = self
188                                    .inner
189                                    .get_node(node_id)
190                                    .unwrap()
191                                    .element_data()
192                                    .unwrap();
193                                let triggers_input_event =
194                                    element_data.attr(local_name!("type")) == Some("checkbox");
195                                let form_data = wrap_event_data(
196                                    self.input_event_form_data(&chain, element_data),
197                                );
198                                if triggers_input_event {
199                                    let input_event = Event::new(form_data, true);
200                                    self.vdom.runtime().handle_event(
201                                        "input",
202                                        input_event,
203                                        dioxus_id,
204                                    );
205                                }
206                            }
207                        }
208                    }
209
210                    if !event.bubbles || stop_propagation {
211                        break;
212                    }
213                }
214            }
215
216            DomEventData::KeyPress(kevent) => {
217                let key_event_data = wrap_event_data(BlitzKeyboardData(kevent.clone()));
218
219                for node_id in chain.clone().into_iter() {
220                    let node = &self.inner.tree()[node_id];
221                    let dioxus_id = node.element_data().and_then(DioxusDocument::dioxus_id);
222                    println!("{} {:?}", node_id, dioxus_id);
223
224                    if let Some(id) = dioxus_id {
225                        if kevent.state.is_pressed() {
226                            // Handle keydown event
227                            let event = Event::new(key_event_data.clone(), true);
228                            self.vdom
229                                .runtime()
230                                .handle_event("keydown", event.clone(), id);
231                            prevent_default |= !event.default_action_enabled();
232                            stop_propagation |= !event.propagates();
233
234                            if !prevent_default && kevent.text.is_some() {
235                                // Handle keypress event
236                                let event = Event::new(key_event_data.clone(), true);
237                                self.vdom
238                                    .runtime()
239                                    .handle_event("keypress", event.clone(), id);
240                                prevent_default |= !event.default_action_enabled();
241                                stop_propagation |= !event.propagates();
242
243                                if !prevent_default {
244                                    // Handle default DOM event
245                                    let mut default_event =
246                                        DomEvent::new(node_id, renderer_event.data.clone());
247                                    self.inner.as_mut().handle_event(&mut default_event);
248                                    prevent_default = true;
249
250                                    // Handle input event
251                                    let element =
252                                        self.inner.tree()[node_id].element_data().unwrap();
253                                    let triggers_input_event = &element.name.local == "input"
254                                        && matches!(
255                                            element.attr(local_name!("type")),
256                                            None | Some("text" | "password" | "email" | "search")
257                                        );
258                                    if triggers_input_event {
259                                        let form_data = wrap_event_data(dbg!(
260                                            self.input_event_form_data(&chain, element)
261                                        ));
262                                        let input_event = Event::new(form_data, true);
263                                        self.vdom.runtime().handle_event("input", input_event, id);
264                                    }
265                                }
266                            }
267                        } else {
268                            // Handle keyup event
269                            let event = Event::new(key_event_data.clone(), true);
270                            self.vdom.runtime().handle_event("keyup", event.clone(), id);
271                            prevent_default |= !event.default_action_enabled();
272                            stop_propagation |= !event.propagates();
273                        }
274                    }
275
276                    if !event.bubbles || stop_propagation {
277                        break;
278                    }
279                }
280            }
281
282            DomEventData::Hover => {}
283
284            // TODO: Implement IME and Hover events handling
285            DomEventData::Ime(_) => {}
286        }
287
288        if !event.cancelable || !prevent_default {
289            self.inner.as_mut().handle_event(event);
290        }
291    }
292}
293
294fn wrap_event_data<T: Any>(value: T) -> Rc<dyn Any> {
295    Rc::new(PlatformEventData::new(Box::new(value)))
296}
297
298impl DioxusDocument {
299    /// Generate the FormData from an input event
300    /// Currently only cares about input checkboxes
301    pub fn input_event_form_data(
302        &self,
303        parent_chain: &[usize],
304        element_node_data: &ElementNodeData,
305    ) -> NativeFormData {
306        let parent_form = parent_chain.iter().find_map(|id| {
307            let node = self.inner.get_node(*id)?;
308            let element_data = node.element_data()?;
309            if element_data.name.local == local_name!("form") {
310                Some(node)
311            } else {
312                None
313            }
314        });
315        let values = if let Some(parent_form) = parent_form {
316            let mut values = HashMap::<String, FormValue>::new();
317            for form_input in self.input_descendents(parent_form).into_iter() {
318                // Match html behaviour here. To be included in values:
319                // - input must have a name
320                // - if its an input, we only include it if checked
321                // - if value is not specified, it defaults to 'on'
322                if let Some(name) = form_input.attr(local_name!("name")) {
323                    if form_input.attr(local_name!("type")) == Some("checkbox")
324                        && form_input
325                            .element_data()
326                            .and_then(|data| data.checkbox_input_checked())
327                            .unwrap_or(false)
328                    {
329                        let value = form_input
330                            .attr(local_name!("value"))
331                            .unwrap_or("on")
332                            .to_string();
333                        values.insert(name.to_string(), FormValue(vec![value]));
334                    }
335                }
336            }
337            values
338        } else {
339            Default::default()
340        };
341        let value = match &element_node_data.node_specific_data {
342            NodeSpecificData::CheckboxInput(checked) => checked.to_string(),
343            NodeSpecificData::TextInput(input_data) => input_data.editor.text().to_string(),
344            _ => element_node_data
345                .attr(local_name!("value"))
346                .unwrap_or_default()
347                .to_string(),
348        };
349
350        NativeFormData { value, values }
351    }
352
353    /// Collect all the inputs which are descendents of a given node
354    fn input_descendents(&self, node: &Node) -> Vec<&Node> {
355        node.children
356            .iter()
357            .flat_map(|id| {
358                let mut res = Vec::<&Node>::new();
359                let Some(n) = self.inner.get_node(*id) else {
360                    return res;
361                };
362                let Some(element_data) = n.element_data() else {
363                    return res;
364                };
365                if element_data.name.local == local_name!("input") {
366                    res.push(n);
367                }
368                res.extend(self.input_descendents(n).iter());
369                res
370            })
371            .collect()
372    }
373
374    pub fn new(
375        vdom: VirtualDom,
376        net_provider: Option<Arc<dyn NetProvider<Data = Resource>>>,
377    ) -> Self {
378        let viewport = Viewport::new(0, 0, 1.0, ColorScheme::Light);
379        let mut doc = BaseDocument::new(viewport);
380
381        // Set net provider
382        if let Some(net_provider) = net_provider {
383            doc.set_net_provider(net_provider);
384        }
385
386        // Create a virtual "html" element to act as the root element, as we won't necessarily
387        // have a single root otherwise, while the rest of blitz requires that we do
388        let html_element_id = doc.create_node(NodeData::Element(ElementNodeData::new(
389            qual_name("html", None),
390            Vec::new(),
391        )));
392        let root_node_id = doc.root_node().id;
393        let html_element = doc.get_node_mut(html_element_id).unwrap();
394        html_element.parent = Some(root_node_id);
395        let root_node = doc.get_node_mut(root_node_id).unwrap();
396        root_node.children.push(html_element_id);
397
398        // Create the body element
399        let body_element_id = doc.create_node(NodeData::Element(ElementNodeData::new(
400            qual_name("body", None),
401            Vec::new(),
402        )));
403        let html_element = doc.get_node_mut(html_element_id).unwrap();
404        html_element.children.push(body_element_id);
405        let body_element = doc.get_node_mut(body_element_id).unwrap();
406        body_element.parent = Some(html_element_id);
407
408        // Create another virtual element to hold the root <div id="main"></div> under the html element
409        let main_element_id = doc.create_node(NodeData::Element(ElementNodeData::new(
410            qual_name("div", Some("id")),
411            vec![blitz_dom::node::Attribute {
412                name: qual_name("id", None),
413                value: "main".to_string(),
414            }],
415        )));
416        let body_element = doc.get_node_mut(body_element_id).unwrap();
417        body_element.children.push(main_element_id);
418        let main_element = doc.get_node_mut(main_element_id).unwrap();
419        main_element.parent = Some(body_element_id);
420
421        // Create the head element
422        let head_element_id = doc.create_node(NodeData::Element(ElementNodeData::new(
423            qual_name("head", None),
424            Vec::new(),
425        )));
426        let head_element = doc.get_node_mut(head_element_id).unwrap();
427        head_element.parent = Some(html_element_id);
428        let html_element = doc.get_node_mut(html_element_id).unwrap();
429        html_element.children.push(head_element_id);
430
431        // Include default and user-specified stylesheets
432        doc.add_user_agent_stylesheet(DEFAULT_CSS);
433
434        let state = DioxusState::create(&mut doc, main_element_id);
435        let mut doc = Self {
436            vdom,
437            vdom_state: state,
438            inner: doc,
439            html_element_id,
440            head_element_id,
441            body_element_id,
442            main_element_id,
443        };
444
445        doc.inner.set_base_url("dioxus://index.html");
446
447        // doc.initial_build();
448
449        // doc.inner.print_tree();
450
451        doc
452    }
453
454    pub fn initial_build(&mut self) {
455        // let mut writer = MutationWriter::new(&mut self.inner, &mut self.vdom_state);
456
457        // self.vdom.rebuild(&mut writer);
458        // dbg!(self.vdom.rebuild_to_vec());
459        // std::process::exit(0);
460        // dbg!(writer.state);
461    }
462
463    pub fn label_bound_input_element(&self, label_node_id: NodeId) -> Option<(ElementId, NodeId)> {
464        let bound_input_elements = self.inner.label_bound_input_elements(label_node_id);
465
466        // Filter down bound elements to those which have dioxus id
467        bound_input_elements.into_iter().find_map(|n| {
468            let target_element_data = n.element_data()?;
469            let node_id = n.id;
470            let dioxus_id = DioxusDocument::dioxus_id(target_element_data)?;
471            Some((dioxus_id, node_id))
472        })
473    }
474
475    fn dioxus_id(element_node_data: &ElementNodeData) -> Option<ElementId> {
476        Some(ElementId(
477            element_node_data
478                .attrs
479                .iter()
480                .find(|attr| *attr.name.local == *"data-dioxus-id")?
481                .value
482                .parse::<usize>()
483                .ok()?,
484        ))
485    }
486
487    pub fn create_head_element(
488        &mut self,
489        name: &str,
490        attributes: &[(String, String)],
491        contents: &Option<String>,
492    ) {
493        let mut stylesheet = None;
494        let mut title = None;
495        if name == "link" {
496            let is_stylesheet = attributes
497                .iter()
498                .any(|(name, value)| name == "rel" && value == "stylesheet");
499            if is_stylesheet {
500                stylesheet = attributes
501                    .iter()
502                    .find(|(name, _value)| name == "href")
503                    .map(|(_name, value)| value.clone());
504            }
505        }
506
507        if name == "title" {
508            title = attributes
509                .iter()
510                .find(|(name, _value)| name == "text")
511                .and_then(|(_name, _value)| contents.clone());
512        }
513
514        let attributes = attributes
515            .iter()
516            .map(|(name, value)| Attribute {
517                name: qual_name(name, None),
518                value: value.clone(),
519            })
520            .collect();
521
522        let new_element = self
523            .inner
524            .create_node(NodeData::Element(ElementNodeData::new(
525                qual_name(name, None),
526                attributes,
527            )));
528
529        if let Some(contents) = contents {
530            let text_node = self.inner.create_text_node(contents);
531            self.inner
532                .get_node_mut(new_element)
533                .unwrap()
534                .children
535                .push(text_node);
536        }
537
538        self.inner
539            .get_node_mut(self.head_element_id)
540            .unwrap()
541            .children
542            .push(new_element);
543
544        if let Some(url) = stylesheet {
545            crate::assets::fetch_linked_stylesheet(&self.inner, new_element, url);
546        }
547
548        if let Some(_title) = title {
549            println!("todo: set title");
550        }
551    }
552
553    // pub fn apply_mutations(&mut self) {
554    //     // Apply the mutations to the actual dom
555    //     let mut writer = MutationWriter {
556    //         doc: &mut self.inner,
557    //         state: &mut self.vdom_state,
558    //     };
559    //     self.vdom.render_immediate(&mut writer);
560
561    //     println!("APPLY MUTATIONS");
562    //     self.inner.print_tree();
563    // }
564}