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, 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
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 doc.add_user_agent_stylesheet(DEFAULT_CSS);
97
98 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 let head_element_id = mutr.create_element(qual_name("head", None), vec![]);
117 mutr.append_children(html_element_id, &[head_element_id]);
118
119 let body_element_id = mutr.create_element(qual_name("body", None), vec![]);
121 mutr.append_children(html_element_id, &[body_element_id]);
122
123 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 pub fn initial_build(&mut self) {
147 let mut inner = self.inner.borrow_mut();
148 let mut writer = self.vdom_state.writer(&mut inner);
149 self.vdom.rebuild(&mut writer);
150 drop(writer);
151 drop(inner);
152 self.flush_queued_mounted_events();
153 }
154
155 #[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
206impl 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 static NOOP_WAKER: LazyLock<Waker> = LazyLock::new(noop_waker);
222 let waker = cx
223 .as_ref()
224 .map(|cx| cx.waker().clone())
225 .unwrap_or_else(|| NOOP_WAKER.clone());
226
227 let subdoc_changes = self.inner.borrow_mut().poll_subdocuments(Some(&waker));
230
231 {
232 let fut = self.vdom.wait_for_work();
233 let mut pinned_fut = pin!(fut);
234
235 let mut cx = TaskContext::from_waker(&waker);
236 match pinned_fut.as_mut().poll(&mut cx) {
237 std::task::Poll::Ready(_) => {}
238 std::task::Poll::Pending => return subdoc_changes,
239 }
240 }
241
242 let mut inner = self.inner.borrow_mut();
243 let mut writer = self.vdom_state.writer(&mut inner);
244 self.vdom.render_immediate(&mut writer);
245 drop(writer);
246 drop(inner);
247 self.flush_queued_mounted_events();
248
249 true
250 }
251
252 fn handle_ui_event(&mut self, event: UiEvent) {
253 let handler = DioxusEventHandler {
254 vdom: &mut self.vdom,
255 vdom_state: &mut self.vdom_state,
256 };
257 let mut driver = EventDriver::new(&mut self.inner, handler);
258 driver.handle_ui_event(event);
259 }
260}
261
262pub struct DioxusEventHandler<'v> {
263 vdom: &'v mut VirtualDom,
264 vdom_state: &'v mut DioxusState,
265}
266
267impl EventHandler for DioxusEventHandler<'_> {
268 fn handle_event(
269 &mut self,
270 chain: &[NodeId],
271 event: &mut DomEvent,
272 doc: &mut dyn Document,
273 event_state: &mut EventState,
274 ) {
275 let event_kind_idx = event.data.discriminant() as usize;
278 let event_kind_count = self.vdom_state.event_handler_counts[event_kind_idx];
279 if event_kind_count == 0 {
280 return;
281 }
282
283 let event_data = match &event.data {
284 DomEventData::PointerMove(mevent)
285 | DomEventData::PointerDown(mevent)
286 | DomEventData::PointerUp(mevent)
287 | DomEventData::PointerCancel(mevent)
288 | DomEventData::PointerLeave(mevent)
289 | DomEventData::PointerEnter(mevent)
290 | DomEventData::PointerOver(mevent)
291 | DomEventData::PointerOut(mevent)
292 | DomEventData::MouseMove(mevent)
293 | DomEventData::MouseDown(mevent)
294 | DomEventData::MouseUp(mevent)
295 | DomEventData::MouseLeave(mevent)
296 | DomEventData::MouseEnter(mevent)
297 | DomEventData::MouseOver(mevent)
298 | DomEventData::MouseOut(mevent)
299 | DomEventData::Click(mevent)
300 | DomEventData::ContextMenu(mevent)
301 | DomEventData::DoubleClick(mevent) => {
302 Some(wrap_event_data(NativePointerData(mevent.clone())))
303 }
304
305 DomEventData::TouchStart(tevent)
306 | DomEventData::TouchMove(tevent)
307 | DomEventData::TouchEnd(tevent)
308 | DomEventData::TouchCancel(tevent) => {
309 Some(wrap_event_data(NativeTouchData(tevent.clone())))
310 }
311
312 DomEventData::Scroll(sevent) => Some(wrap_event_data(NativeScrollData(sevent.clone()))),
313 DomEventData::Wheel(wevent) => Some(wrap_event_data(NativeWheelData(wevent.clone()))),
314
315 DomEventData::Focus(_) => Some(wrap_event_data(NativeFocusData)),
316 DomEventData::Blur(_) => Some(wrap_event_data(NativeFocusData)),
317 DomEventData::FocusIn(_) => Some(wrap_event_data(NativeFocusData)),
318 DomEventData::FocusOut(_) => Some(wrap_event_data(NativeFocusData)),
319
320 DomEventData::KeyDown(kevent)
321 | DomEventData::KeyUp(kevent)
322 | DomEventData::KeyPress(kevent) => {
323 Some(wrap_event_data(BlitzKeyboardData(kevent.clone())))
324 }
325
326 DomEventData::Input(data) => Some(wrap_event_data(NativeFormData {
327 value: data.value.clone(),
328 values: vec![],
329 })),
330
331 DomEventData::Ime(_) => None,
333
334 DomEventData::AppleStandardKeybinding(_) => None,
336 };
337
338 let Some(event_data) = event_data else {
339 return;
340 };
341
342 let dioxus_id = chain
344 .iter()
345 .find_map(|node_id| doc.inner().get_node(*node_id).and_then(get_dioxus_id));
346 let Some(id) = dioxus_id else {
347 return;
348 };
349
350 let dx_event = Event::new(event_data.clone(), event.bubbles);
352 self.vdom
353 .runtime()
354 .handle_event(event.name(), dx_event.clone(), id);
355
356 if !dx_event.default_action_enabled() {
358 event_state.prevent_default();
359 }
360 if !dx_event.propagates() {
361 event_state.stop_propagation();
362 }
363 }
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369 use blitz_dom::DocumentConfig;
370 use dioxus::prelude::*;
371 use dioxus_core::ScopeId;
372 use std::cell::{Cell, RefCell};
373 use std::collections::HashMap;
374
375 #[test]
376 fn keyed_nodes_do_not_crash() {
379 type SharedData = Rc<RefCell<HashMap<usize, usize>>>;
380 let data: SharedData = Rc::new(RefCell::new(HashMap::new()));
381
382 fn app(data: SharedData) -> Element {
383 let entries: Vec<usize> = data.borrow().keys().copied().collect();
384 rsx!(
385 for id in entries {
386 div {
387 key: "item_{id}",
388 "{id}"
389 }
390 }
391 )
392 }
393
394 let vdom = VirtualDom::new_with_props(app, Rc::clone(&data));
395 let mut doc = DioxusDocument::new(vdom, DocumentConfig::default());
396 doc.initial_build();
397
398 for i in 0..100 {
401 data.borrow_mut().insert(i, i);
402 doc.vdom.mark_dirty(ScopeId::APP);
403 doc.poll(None);
404 }
405
406 let inner = doc.inner.borrow();
409 let main = inner.get_node(doc.main_element_id).unwrap();
410 assert_eq!(main.children.len(), 100);
411 }
412
413 fn node_count(doc: &DioxusDocument) -> usize {
414 doc.inner.borrow().tree().len()
415 }
416
417 fn rerender(doc: &mut DioxusDocument) {
418 doc.vdom.mark_dirty(ScopeId::APP);
419 poll_until_idle(doc);
420 }
421
422 fn poll_until_idle(doc: &mut DioxusDocument) {
423 for _ in 0..8 {
424 if !doc.poll(None) {
425 return;
426 }
427 }
428 panic!("document did not become idle");
429 }
430
431 fn main_text(doc: &DioxusDocument) -> String {
432 let inner = doc.inner.borrow();
433 let mut text = String::new();
434 let mut stack = vec![doc.main_element_id];
435 while let Some(id) = stack.pop() {
436 let node = inner.get_node(id).unwrap();
437 if let Some(data) = node.text_data() {
438 text.push_str(&data.content);
439 }
440 stack.extend(node.children.iter().rev().copied());
441 }
442 text
443 }
444
445 #[test]
446 fn removed_nodes_are_dropped() {
447 fn app() -> Element {
448 let item_count = consume_context::<Rc<Cell<usize>>>();
449 rsx!(
450 for id in 0..item_count.get() {
451 div { key: "{id}", span { "{id}" } }
452 }
453 )
454 }
455
456 let item_count = Rc::new(Cell::new(0usize));
457 let vdom = VirtualDom::new(app).with_root_context(Rc::clone(&item_count));
458 let mut doc = DioxusDocument::new(vdom, DocumentConfig::default());
459 doc.initial_build();
460
461 let cycle = |doc: &mut DioxusDocument| {
462 item_count.set(50);
463 rerender(doc);
464 let main_id = doc.main_element_id;
465 assert_eq!(
466 doc.inner.borrow().get_node(main_id).unwrap().children.len(),
467 50
468 );
469 item_count.set(0);
470 rerender(doc);
471 node_count(doc)
472 };
473
474 let baseline = cycle(&mut doc);
476 for _ in 0..5 {
477 assert_eq!(cycle(&mut doc), baseline);
478 }
479 }
480
481 #[test]
482 fn suspended_nodes_are_dropped_and_recreated() {
483 #[derive(Default)]
484 struct State {
485 suspended: Cell<bool>,
486 generation: Cell<usize>,
487 waker: Cell<Option<std::task::Waker>>,
488 }
489
490 #[component]
492 fn Child(generation: usize) -> Element {
493 let state = consume_context::<Rc<State>>();
494 let mut resolved = use_signal(|| false);
495 if state.suspended.get() {
496 resolved.set(false);
497 let task = spawn(async move {
498 std::future::poll_fn(|cx| {
499 if state.suspended.get() {
500 state.waker.set(Some(cx.waker().clone()));
501 std::task::Poll::Pending
502 } else {
503 std::task::Poll::Ready(())
504 }
505 })
506 .await;
507 resolved.set(true);
508 });
509 suspend(task)?;
510 }
511 rsx!( div { span { "content" } } )
512 }
513
514 fn app() -> Element {
515 let state = consume_context::<Rc<State>>();
516 rsx!(
517 SuspenseBoundary {
518 fallback: |_| rsx!( p { "loading" } ),
519 Child { generation: state.generation.get() }
520 }
521 )
522 }
523
524 let state = Rc::new(State::default());
525 let vdom = VirtualDom::new(app).with_root_context(Rc::clone(&state));
526 let mut doc = DioxusDocument::new(vdom, DocumentConfig::default());
527 doc.initial_build();
528 assert_eq!(main_text(&doc), "content");
529
530 let cycle = |doc: &mut DioxusDocument| {
531 state.suspended.set(true);
532 state.generation.set(state.generation.get() + 1);
533 rerender(doc);
534 assert_eq!(main_text(doc), "loading");
535 state.suspended.set(false);
536 state.waker.take().unwrap().wake();
537 poll_until_idle(doc);
538 assert_eq!(main_text(doc), "content");
539 node_count(doc)
540 };
541
542 let baseline = cycle(&mut doc);
543 for _ in 0..5 {
544 assert_eq!(cycle(&mut doc), baseline);
545 }
546 }
547
548 #[test]
549 fn todo_like_stress() {
550 use std::cell::RefCell;
551 #[derive(Clone, PartialEq)]
552 struct Item {
553 id: u32,
554 checked: bool,
555 editing: bool,
556 }
557 #[derive(Default)]
558 struct State {
559 items: RefCell<Vec<Item>>,
560 filter: Cell<u8>,
561 }
562
563 #[component]
564 fn Entry(item: Item) -> Element {
565 rsx! {
566 li { class: if item.checked { "completed" },
567 div { class: "view",
568 input { r#type: "checkbox", id: "cbg-{item.id}" }
569 label { "todo {item.id}" }
570 button { class: "destroy" }
571 }
572 if item.editing {
573 input { class: "edit", value: "todo {item.id}" }
574 }
575 }
576 }
577 }
578
579 fn app() -> Element {
580 let state = consume_context::<Rc<State>>();
581 let items = state.items.borrow().clone();
582 let filter = state.filter.get();
583 let active = items.iter().filter(|i| !i.checked).count();
584 let visible: Vec<Item> = items
585 .iter()
586 .filter(|i| match filter {
587 1 => !i.checked,
588 2 => i.checked,
589 _ => true,
590 })
591 .cloned()
592 .collect();
593 rsx! {
594 section {
595 if !items.is_empty() {
596 input { id: "toggle-all", r#type: "checkbox" }
597 label { r#for: "toggle-all" }
598 }
599 ul {
600 for item in visible {
601 Entry { key: "{item.id}", item }
602 }
603 }
604 if !items.is_empty() {
605 footer {
606 span { strong { "{active} " } span { match active { 1 => "item", _ => "items" } " left" } }
607 if items.iter().any(|i| i.checked) {
608 button { "Clear completed" }
609 }
610 }
611 }
612 }
613 }
614 }
615
616 let state = Rc::new(State::default());
617 let vdom = VirtualDom::new(app).with_root_context(Rc::clone(&state));
618 let mut doc = DioxusDocument::new(vdom, DocumentConfig::default());
619 doc.initial_build();
620
621 let mut seed = 0x2545F4914F6CDD1Du64;
622 let mut rng = move || {
623 seed ^= seed << 13;
624 seed ^= seed >> 7;
625 seed ^= seed << 17;
626 seed
627 };
628 let mut next_id = 0;
629 let mut counts = Vec::new();
630 for step in 1..=3000 {
631 let r = rng();
632 {
633 let mut items = state.items.borrow_mut();
634 let len = items.len();
635 match r % 8 {
636 0 | 1 => {
637 items.push(Item {
638 id: next_id,
639 checked: false,
640 editing: false,
641 });
642 next_id += 1;
643 }
644 2 if len > 0 => {
645 items.remove((r >> 8) as usize % len);
646 }
647 3 if len > 0 => {
648 let i = (r >> 8) as usize % len;
649 items[i].checked ^= true;
650 }
651 4 if len > 0 => {
652 let i = (r >> 8) as usize % len;
653 items[i].editing ^= true;
654 }
655 5 => state.filter.set(((r >> 8) % 3) as u8),
656 6 => items.retain(|i| !i.checked),
657 7 if len > 12 => items.clear(),
658 _ => {}
659 }
660 }
661 rerender(&mut doc);
662 if step % 1000 == 0 {
663 state.items.borrow_mut().clear();
664 rerender(&mut doc);
665 counts.push(node_count(&doc));
666 }
667 }
668 assert_eq!(counts[1], counts[2]);
670 }
671
672 #[test]
673 fn mounted_data_of_removed_element_reports_error() {
674 #[derive(Default)]
675 struct State {
676 show: Cell<bool>,
677 mounted: RefCell<Option<Rc<MountedData>>>,
678 }
679
680 fn app() -> Element {
681 let state = consume_context::<Rc<State>>();
682 rsx!(if state.show.get() {
683 div {
684 onmounted: move |e| *state.mounted.borrow_mut() = Some(e.data()),
685 }
686 })
687 }
688
689 let state = Rc::new(State::default());
690 state.show.set(true);
691 let vdom = VirtualDom::new(app).with_root_context(Rc::clone(&state));
692 let mut doc = DioxusDocument::new(vdom, DocumentConfig::default());
693 doc.initial_build();
694 let mounted = state.mounted.take().unwrap();
695 let focus = |focus: bool| {
696 let mut cx = std::task::Context::from_waker(std::task::Waker::noop());
697 let fut = std::pin::pin!(mounted.set_focus(focus));
698 matches!(fut.poll(&mut cx), std::task::Poll::Ready(Ok(())))
699 };
700 assert!(focus(true));
701 let focussed = |doc: &DioxusDocument| doc.inner.borrow().get_focussed_node_id();
702 let mounted_id = focussed(&doc).unwrap();
703 state.show.set(false);
704 rerender(&mut doc);
705 assert!(doc.inner.borrow().get_node(mounted_id).is_none());
706 assert_ne!(focussed(&doc), Some(mounted_id));
707
708 let focussed_after_removal = focussed(&doc);
710 assert!(focus(true));
711 assert!(focus(false));
712 assert_eq!(focussed(&doc), focussed_after_removal);
713
714 fn is_err<T>(fut: impl Future<Output = Result<T, MountedError>>) -> bool {
715 let mut cx = std::task::Context::from_waker(std::task::Waker::noop());
716 let fut = std::pin::pin!(fut);
717 matches!(fut.poll(&mut cx), std::task::Poll::Ready(Err(_)))
718 }
719 assert!(is_err(mounted.get_scroll_offset()));
720 assert!(is_err(mounted.get_scroll_size()));
721 assert!(is_err(mounted.get_client_rect()));
722 assert!(is_err(mounted.scroll_to(ScrollBehavior::Instant)));
723 assert!(is_err(
724 mounted.scroll(Default::default(), ScrollBehavior::Instant)
725 ));
726 }
727}