1use crate::NodeId;
3use crate::events::{
4 BlitzKeyboardData, NativeConverter, NativeFocusData, NativeFormData, NativePointerData,
5 NativeScrollData, NativeTouchData, NativeWheelData, NodeHandle,
6};
7use crate::mutation_writer::DioxusState;
8use crate::qual_name;
9use blitz_dom::{
10 Attribute, BaseDocument, DocGuard, DocGuardMut, Document, DocumentConfig, EventDriver,
11 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
29fn 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::from_raw)
37}
38
39pub 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 pub fn new(vdom: VirtualDom, mut config: DocumentConfig) -> Self {
85 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 let mut mutr = doc.mutate();
109 let html_element_id = mutr.create_element(qual_name("html", None), vec![]);
110 mutr.append_children(mutr.doc.root_node().id, &[html_element_id]);
111
112 let head_element_id = mutr.create_element(qual_name("head", None), vec![]);
114 mutr.append_children(html_element_id, &[head_element_id]);
115
116 let body_element_id = mutr.create_element(qual_name("body", None), vec![]);
118 mutr.append_children(html_element_id, &[body_element_id]);
119
120 let main_attr = blitz_dom::Attribute {
122 name: qual_name("id", None),
123 value: "main".to_string(),
124 };
125 let main_element_id = mutr.create_element(qual_name("main", None), vec![main_attr]);
126 mutr.append_children(body_element_id, &[main_element_id]);
127
128 drop(mutr);
129
130 let vdom_state = DioxusState::create(main_element_id);
131 Self {
132 vdom,
133 vdom_state,
134 inner: Rc::new(RefCell::new(doc)),
135 html_element_id,
136 head_element_id,
137 body_element_id,
138 main_element_id,
139 }
140 }
141
142 pub fn initial_build(&mut self) {
144 self.vdom.rebuild(&mut self.vdom_state.buffered_mutations);
147 self.vdom_state
148 .apply_buffered_mutations(&mut self.inner.borrow_mut());
149 self.flush_queued_mounted_events();
150 }
151
152 #[doc(hidden)]
155 pub fn create_head_element(
156 &mut self,
157 name: &str,
158 attributes: &[(String, String)],
159 contents: &Option<String>,
160 ) {
161 let mut inner = self.inner.borrow_mut();
162 let mut mutr = inner.mutate();
163
164 let attributes = attributes
165 .iter()
166 .map(|(name, value)| Attribute {
167 name: qual_name(name, None),
168 value: value.clone(),
169 })
170 .collect();
171
172 let new_elem_id = mutr.create_element(qual_name(name, None), attributes);
173 mutr.append_children(self.head_element_id, &[new_elem_id]);
174 if let Some(contents) = contents {
175 let text_node_id = mutr.create_text_node(contents);
176 mutr.append_children(new_elem_id, &[text_node_id]);
177 }
178 }
179
180 pub(crate) fn flush_queued_mounted_events(&mut self) {
181 let mut queued_mounted_events = mem::take(&mut self.vdom_state.queued_mounted_events);
182 for element_id in queued_mounted_events.drain(..) {
183 let node_id = self.vdom_state.element_to_node_id(element_id);
184
185 if self.inner.borrow().get_node(node_id).is_some() {
186 let event = Event::new(
187 Rc::new(PlatformEventData::new(Box::new(NodeHandle {
188 doc: Rc::clone(&self.inner),
189 node_id,
190 }))) as Rc<dyn Any>,
191 false,
192 );
193 self.vdom
194 .runtime()
195 .handle_event("mounted", event, element_id);
196 }
197 }
198
199 self.vdom_state.queued_mounted_events = queued_mounted_events;
200 }
201}
202
203impl Document for DioxusDocument {
205 fn id(&self) -> usize {
206 self.inner.borrow().id()
207 }
208
209 fn inner(&self) -> DocGuard<'_> {
210 DocGuard::RefCell(self.inner.borrow())
211 }
212
213 fn inner_mut(&mut self) -> DocGuardMut<'_> {
214 DocGuardMut::RefCell(self.inner.borrow_mut())
215 }
216
217 fn poll(&mut self, cx: Option<TaskContext>) -> bool {
218 static NOOP_WAKER: LazyLock<Waker> = LazyLock::new(noop_waker);
219 let waker = cx
220 .as_ref()
221 .map(|cx| cx.waker().clone())
222 .unwrap_or_else(|| NOOP_WAKER.clone());
223
224 let subdoc_changes = self.inner.borrow_mut().poll_subdocuments(Some(&waker));
227
228 {
229 let fut = self.vdom.wait_for_work();
230 let mut pinned_fut = pin!(fut);
231
232 let mut cx = TaskContext::from_waker(&waker);
233 match pinned_fut.as_mut().poll(&mut cx) {
234 std::task::Poll::Ready(_) => {}
235 std::task::Poll::Pending => return subdoc_changes,
236 }
237 }
238
239 self.vdom
242 .render_immediate(&mut self.vdom_state.buffered_mutations);
243 self.vdom_state
244 .apply_buffered_mutations(&mut self.inner.borrow_mut());
245 self.flush_queued_mounted_events();
246
247 true
248 }
249
250 fn handle_ui_event(&mut self, event: UiEvent) {
251 let handler = DioxusEventHandler {
252 vdom: &mut self.vdom,
253 vdom_state: &mut self.vdom_state,
254 };
255 let mut driver = EventDriver::new(&mut self.inner, handler);
256 driver.handle_ui_event(event);
257 }
258}
259
260pub struct DioxusEventHandler<'v> {
261 vdom: &'v mut VirtualDom,
262 vdom_state: &'v mut DioxusState,
263}
264
265impl EventHandler for DioxusEventHandler<'_> {
266 fn handle_event(
267 &mut self,
268 chain: &[NodeId],
269 event: &mut DomEvent,
270 doc: &mut dyn Document,
271 event_state: &mut EventState,
272 ) {
273 let event_kind_idx = event.data.discriminant() as usize;
276 let event_kind_count = self.vdom_state.event_handler_counts[event_kind_idx];
277 if event_kind_count == 0 {
278 return;
279 }
280
281 let event_data = match &event.data {
282 DomEventData::PointerMove(mevent)
283 | DomEventData::PointerDown(mevent)
284 | DomEventData::PointerUp(mevent)
285 | DomEventData::PointerCancel(mevent)
286 | DomEventData::PointerLeave(mevent)
287 | DomEventData::PointerEnter(mevent)
288 | DomEventData::PointerOver(mevent)
289 | DomEventData::PointerOut(mevent)
290 | DomEventData::MouseMove(mevent)
291 | DomEventData::MouseDown(mevent)
292 | DomEventData::MouseUp(mevent)
293 | DomEventData::MouseLeave(mevent)
294 | DomEventData::MouseEnter(mevent)
295 | DomEventData::MouseOver(mevent)
296 | DomEventData::MouseOut(mevent)
297 | DomEventData::Click(mevent)
298 | DomEventData::ContextMenu(mevent)
299 | DomEventData::DoubleClick(mevent) => {
300 Some(wrap_event_data(NativePointerData(mevent.clone())))
301 }
302
303 DomEventData::TouchStart(tevent)
304 | DomEventData::TouchMove(tevent)
305 | DomEventData::TouchEnd(tevent)
306 | DomEventData::TouchCancel(tevent) => {
307 Some(wrap_event_data(NativeTouchData(tevent.clone())))
308 }
309
310 DomEventData::Scroll(sevent) => Some(wrap_event_data(NativeScrollData(sevent.clone()))),
311 DomEventData::Wheel(wevent) => Some(wrap_event_data(NativeWheelData(wevent.clone()))),
312
313 DomEventData::Focus(_) => Some(wrap_event_data(NativeFocusData)),
314 DomEventData::Blur(_) => Some(wrap_event_data(NativeFocusData)),
315 DomEventData::FocusIn(_) => Some(wrap_event_data(NativeFocusData)),
316 DomEventData::FocusOut(_) => Some(wrap_event_data(NativeFocusData)),
317
318 DomEventData::KeyDown(kevent)
319 | DomEventData::KeyUp(kevent)
320 | DomEventData::KeyPress(kevent) => {
321 Some(wrap_event_data(BlitzKeyboardData(kevent.clone())))
322 }
323
324 DomEventData::Input(data) => Some(wrap_event_data(NativeFormData {
325 value: data.value.clone(),
326 values: vec![],
327 })),
328
329 DomEventData::Ime(_) => None,
331
332 DomEventData::AppleStandardKeybinding(_) => None,
334 };
335
336 let Some(event_data) = event_data else {
337 return;
338 };
339
340 let dioxus_id = chain
342 .iter()
343 .find_map(|node_id| doc.inner().get_node(*node_id).and_then(get_dioxus_id));
344 let Some(id) = dioxus_id else {
345 return;
346 };
347
348 let dx_event = Event::new(event_data.clone(), event.bubbles);
350 self.vdom
351 .runtime()
352 .handle_event(event.name(), dx_event.clone(), id);
353
354 if !dx_event.default_action_enabled() {
356 event_state.prevent_default();
357 }
358 if !dx_event.propagates() {
359 event_state.stop_propagation();
360 }
361 }
362}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367 use blitz_dom::DocumentConfig;
368 use dioxus::prelude::*;
369 use dioxus_core::ScopeId;
370 use std::cell::{Cell, RefCell};
371 use std::collections::HashMap;
372
373 #[test]
374 fn keyed_nodes_do_not_crash() {
377 type SharedData = Rc<RefCell<HashMap<usize, usize>>>;
378 let data: SharedData = Rc::new(RefCell::new(HashMap::new()));
379
380 fn app(data: SharedData) -> Element {
381 let entries: Vec<usize> = data.borrow().keys().copied().collect();
382 rsx!(
383 for id in entries {
384 div {
385 key: "item_{id}",
386 "{id}"
387 }
388 }
389 )
390 }
391
392 let vdom = VirtualDom::new_with_props(app, Rc::clone(&data));
393 let mut doc = DioxusDocument::new(vdom, DocumentConfig::default());
394 doc.initial_build();
395
396 for i in 0..100 {
399 data.borrow_mut().insert(i, i);
400 doc.vdom.mark_dirty(ScopeId::APP);
401 doc.poll(None);
402 }
403
404 let inner = doc.inner.borrow();
407 let main = inner.get_node(doc.main_element_id).unwrap();
408 assert_eq!(main.children.len(), 100);
409 }
410
411 fn node_count(doc: &DioxusDocument) -> usize {
412 doc.inner.borrow().tree().len()
413 }
414
415 fn rerender(doc: &mut DioxusDocument) {
416 doc.vdom.mark_dirty(ScopeId::APP);
417 poll_until_idle(doc);
418 }
419
420 fn poll_until_idle(doc: &mut DioxusDocument) {
421 for _ in 0..8 {
422 if !doc.poll(None) {
423 return;
424 }
425 }
426 panic!("document did not become idle");
427 }
428
429 fn main_text(doc: &DioxusDocument) -> String {
430 let inner = doc.inner.borrow();
431 let mut text = String::new();
432 let mut stack = vec![doc.main_element_id];
433 while let Some(id) = stack.pop() {
434 let node = inner.get_node(id).unwrap();
435 if let Some(data) = node.text_data() {
436 text.push_str(&data.content);
437 }
438 stack.extend(node.children.iter().rev().copied());
439 }
440 text
441 }
442
443 #[test]
444 fn removed_nodes_are_dropped() {
445 fn app() -> Element {
446 let item_count = consume_context::<Rc<Cell<usize>>>();
447 rsx!(
448 for id in 0..item_count.get() {
449 div { key: "{id}", span { "{id}" } }
450 }
451 )
452 }
453
454 let item_count = Rc::new(Cell::new(0usize));
455 let vdom = VirtualDom::new(app).with_root_context(Rc::clone(&item_count));
456 let mut doc = DioxusDocument::new(vdom, DocumentConfig::default());
457 doc.initial_build();
458
459 let cycle = |doc: &mut DioxusDocument| {
460 item_count.set(50);
461 rerender(doc);
462 let main_id = doc.main_element_id;
463 assert_eq!(
464 doc.inner.borrow().get_node(main_id).unwrap().children.len(),
465 50
466 );
467 item_count.set(0);
468 rerender(doc);
469 node_count(doc)
470 };
471
472 let baseline = cycle(&mut doc);
474 for _ in 0..5 {
475 assert_eq!(cycle(&mut doc), baseline);
476 }
477 }
478
479 #[test]
480 fn suspended_nodes_are_dropped_and_recreated() {
481 #[derive(Default)]
482 struct State {
483 suspended: Cell<bool>,
484 generation: Cell<usize>,
485 waker: Cell<Option<std::task::Waker>>,
486 }
487
488 #[component]
490 fn Child(generation: usize) -> Element {
491 let state = consume_context::<Rc<State>>();
492 let mut resolved = use_signal(|| false);
493 if state.suspended.get() {
494 resolved.set(false);
495 let task = spawn(async move {
496 std::future::poll_fn(|cx| {
497 if state.suspended.get() {
498 state.waker.set(Some(cx.waker().clone()));
499 std::task::Poll::Pending
500 } else {
501 std::task::Poll::Ready(())
502 }
503 })
504 .await;
505 resolved.set(true);
506 });
507 suspend(task)?;
508 }
509 rsx!( div { span { "content" } } )
510 }
511
512 fn app() -> Element {
513 let state = consume_context::<Rc<State>>();
514 rsx!(
515 SuspenseBoundary {
516 fallback: |_| rsx!( p { "loading" } ),
517 Child { generation: state.generation.get() }
518 }
519 )
520 }
521
522 let state = Rc::new(State::default());
523 let vdom = VirtualDom::new(app).with_root_context(Rc::clone(&state));
524 let mut doc = DioxusDocument::new(vdom, DocumentConfig::default());
525 doc.initial_build();
526 assert_eq!(main_text(&doc), "content");
527
528 let cycle = |doc: &mut DioxusDocument| {
529 state.suspended.set(true);
530 state.generation.set(state.generation.get() + 1);
531 rerender(doc);
532 assert_eq!(main_text(doc), "loading");
533 state.suspended.set(false);
534 state.waker.take().unwrap().wake();
535 poll_until_idle(doc);
536 assert_eq!(main_text(doc), "content");
537 node_count(doc)
538 };
539
540 let baseline = cycle(&mut doc);
541 for _ in 0..5 {
542 assert_eq!(cycle(&mut doc), baseline);
543 }
544 }
545
546 #[test]
547 fn todo_like_stress() {
548 use std::cell::RefCell;
549 #[derive(Clone, PartialEq)]
550 struct Item {
551 id: u32,
552 checked: bool,
553 editing: bool,
554 }
555 #[derive(Default)]
556 struct State {
557 items: RefCell<Vec<Item>>,
558 filter: Cell<u8>,
559 }
560
561 #[component]
562 fn Entry(item: Item) -> Element {
563 rsx! {
564 li { class: if item.checked { "completed" },
565 div { class: "view",
566 input { r#type: "checkbox", id: "cbg-{item.id}" }
567 label { "todo {item.id}" }
568 button { class: "destroy" }
569 }
570 if item.editing {
571 input { class: "edit", value: "todo {item.id}" }
572 }
573 }
574 }
575 }
576
577 fn app() -> Element {
578 let state = consume_context::<Rc<State>>();
579 let items = state.items.borrow().clone();
580 let filter = state.filter.get();
581 let active = items.iter().filter(|i| !i.checked).count();
582 let visible: Vec<Item> = items
583 .iter()
584 .filter(|i| match filter {
585 1 => !i.checked,
586 2 => i.checked,
587 _ => true,
588 })
589 .cloned()
590 .collect();
591 rsx! {
592 section {
593 if !items.is_empty() {
594 input { id: "toggle-all", r#type: "checkbox" }
595 label { r#for: "toggle-all" }
596 }
597 ul {
598 for item in visible {
599 Entry { key: "{item.id}", item }
600 }
601 }
602 if !items.is_empty() {
603 footer {
604 span { strong { "{active} " } span { match active { 1 => "item", _ => "items" } " left" } }
605 if items.iter().any(|i| i.checked) {
606 button { "Clear completed" }
607 }
608 }
609 }
610 }
611 }
612 }
613
614 let state = Rc::new(State::default());
615 let vdom = VirtualDom::new(app).with_root_context(Rc::clone(&state));
616 let mut doc = DioxusDocument::new(vdom, DocumentConfig::default());
617 doc.initial_build();
618
619 let mut seed = 0x2545F4914F6CDD1Du64;
620 let mut rng = move || {
621 seed ^= seed << 13;
622 seed ^= seed >> 7;
623 seed ^= seed << 17;
624 seed
625 };
626 let mut next_id = 0;
627 let mut counts = Vec::new();
628 for step in 1..=3000 {
629 let r = rng();
630 {
631 let mut items = state.items.borrow_mut();
632 let len = items.len();
633 match r % 8 {
634 0 | 1 => {
635 items.push(Item {
636 id: next_id,
637 checked: false,
638 editing: false,
639 });
640 next_id += 1;
641 }
642 2 if len > 0 => {
643 items.remove((r >> 8) as usize % len);
644 }
645 3 if len > 0 => {
646 let i = (r >> 8) as usize % len;
647 items[i].checked ^= true;
648 }
649 4 if len > 0 => {
650 let i = (r >> 8) as usize % len;
651 items[i].editing ^= true;
652 }
653 5 => state.filter.set(((r >> 8) % 3) as u8),
654 6 => items.retain(|i| !i.checked),
655 7 if len > 12 => items.clear(),
656 _ => {}
657 }
658 }
659 rerender(&mut doc);
660 if step % 1000 == 0 {
661 state.items.borrow_mut().clear();
662 rerender(&mut doc);
663 counts.push(node_count(&doc));
664 }
665 }
666 assert_eq!(counts[1], counts[2]);
668 }
669
670 #[test]
671 fn task_polled_during_render_can_access_document() {
674 #[derive(Default)]
675 struct State {
676 focus: Cell<bool>,
677 mounted: RefCell<Option<Rc<MountedData>>>,
678 }
679
680 fn app() -> Element {
681 let state = consume_context::<Rc<State>>();
682 if state.focus.get() {
683 let mounted = state.mounted.borrow().clone().unwrap();
684 spawn(async move {
685 mounted.set_focus(true).await.unwrap();
686 mounted.get_client_rect().await.unwrap();
687 });
688 }
689 let focus = state.focus.get();
690 rsx!(input {
691 "data-focus": "{focus}",
692 onmounted: move |e| *state.mounted.borrow_mut() = Some(e.data()),
693 })
694 }
695
696 let state = Rc::new(State::default());
697 let vdom = VirtualDom::new(app).with_root_context(Rc::clone(&state));
698 let mut doc = DioxusDocument::new(vdom, DocumentConfig::default());
699 doc.initial_build();
700 state.focus.set(true);
701 rerender(&mut doc);
702 let inner = doc.inner.borrow();
703 let focussed = inner.get_node(inner.get_focussed_node_id().unwrap());
704 assert_eq!(
705 focussed.unwrap().attr(crate::LocalName::from("data-focus")),
706 Some("true")
707 );
708 }
709
710 #[test]
711 fn mounted_data_of_removed_element_reports_error() {
712 #[derive(Default)]
713 struct State {
714 show: Cell<bool>,
715 mounted: RefCell<Option<Rc<MountedData>>>,
716 }
717
718 fn app() -> Element {
719 let state = consume_context::<Rc<State>>();
720 rsx!(if state.show.get() {
721 div {
722 onmounted: move |e| *state.mounted.borrow_mut() = Some(e.data()),
723 }
724 })
725 }
726
727 let state = Rc::new(State::default());
728 state.show.set(true);
729 let vdom = VirtualDom::new(app).with_root_context(Rc::clone(&state));
730 let mut doc = DioxusDocument::new(vdom, DocumentConfig::default());
731 doc.initial_build();
732 let mounted = state.mounted.take().unwrap();
733 let focus = |focus: bool| {
734 let mut cx = std::task::Context::from_waker(std::task::Waker::noop());
735 let fut = std::pin::pin!(mounted.set_focus(focus));
736 matches!(fut.poll(&mut cx), std::task::Poll::Ready(Ok(())))
737 };
738 assert!(focus(true));
739 let focussed = |doc: &DioxusDocument| doc.inner.borrow().get_focussed_node_id();
740 let mounted_id = focussed(&doc).unwrap();
741 state.show.set(false);
742 rerender(&mut doc);
743 assert!(doc.inner.borrow().get_node(mounted_id).is_none());
744 assert_ne!(focussed(&doc), Some(mounted_id));
745
746 let focussed_after_removal = focussed(&doc);
748 assert!(focus(true));
749 assert!(focus(false));
750 assert_eq!(focussed(&doc), focussed_after_removal);
751
752 fn is_err<T>(fut: impl Future<Output = Result<T, MountedError>>) -> bool {
753 let mut cx = std::task::Context::from_waker(std::task::Waker::noop());
754 let fut = std::pin::pin!(fut);
755 matches!(fut.poll(&mut cx), std::task::Poll::Ready(Err(_)))
756 }
757 assert!(is_err(mounted.get_scroll_offset()));
758 assert!(is_err(mounted.get_scroll_size()));
759 assert!(is_err(mounted.get_client_rect()));
760 assert!(is_err(mounted.scroll_to(ScrollBehavior::Instant)));
761 assert!(is_err(
762 mounted.scroll(Default::default(), ScrollBehavior::Instant)
763 ));
764 }
765}