From 18d6b1ad6fedf96fd80b507fcab0adda3c726db2 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 30 Nov 2022 17:21:10 -0500 Subject: [PATCH] feat: get web working properly --- Cargo.toml | 6 +- examples/simple_todo.rs | 32 + examples/todomvc.rs | 7 +- packages/core/src/arena.rs | 32 +- packages/core/src/create.rs | 2 +- packages/core/src/diff.rs | 70 +- packages/core/src/mutations.rs | 8 +- packages/core/src/scope_arena.rs | 7 +- packages/core/src/scopes.rs | 2 +- packages/core/src/virtual_dom.rs | 30 +- packages/core/tests/kitchen_sink.rs | 2 +- packages/core/tests/miri_full_app.rs | 49 ++ packages/desktop/src/controller.rs | 8 +- packages/desktop/src/desktop_context.rs | 5 +- packages/desktop/src/lib.rs | 2 +- packages/html/src/events.rs | 174 ++--- packages/html/src/lib.rs | 4 +- packages/html/src/web_sys_bind/events.rs | 6 +- packages/interpreter/src/bindings.rs | 95 ++- packages/interpreter/src/interpreter.js | 13 +- packages/web/Cargo.toml | 2 +- packages/web/src/dom.rs | 475 +++++-------- packages/web/src/lib.rs | 186 +++-- packages/web/src/olddom.rs | 856 ----------------------- packages/web/src/rehydrate.rs | 21 +- packages/web/tests/hydrate.rs | 2 +- 26 files changed, 674 insertions(+), 1422 deletions(-) create mode 100644 examples/simple_todo.rs create mode 100644 packages/core/tests/miri_full_app.rs delete mode 100644 packages/web/src/olddom.rs diff --git a/Cargo.toml b/Cargo.toml index a523e1fca..ae0a7490c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,13 +12,13 @@ members = [ "packages/desktop", "packages/mobile", "packages/interpreter", + # "packages/tui", "packages/fermi", - "packages/tui", "packages/liveview", "packages/autofmt", "packages/rsx", - "packages/native-core", - "packages/native-core-macro", + # "packages/native-core", + # "packages/native-core-macro", # "packages/edit-stream", "docs/guide", ] diff --git a/examples/simple_todo.rs b/examples/simple_todo.rs new file mode 100644 index 000000000..454e4b508 --- /dev/null +++ b/examples/simple_todo.rs @@ -0,0 +1,32 @@ +use dioxus::prelude::*; + +fn main() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + let mut idx = use_state(cx, || 0); + let onhover = |h| println!("go!"); + + cx.render(rsx! { + div { + button { onclick: move |_| idx += 1, "+" } + button { onclick: move |_| idx -= 1, "-" } + ul { + (0..**idx).map(|i| rsx! { + Child { i: i, onhover: onhover } + }) + } + } + }) +} + +#[inline_props] +fn Child<'a>(cx: Scope<'a>, i: i32, onhover: EventHandler<'a, MouseEvent>) -> Element { + cx.render(rsx! { + li { + onmouseover: move |e| onhover.call(e), + "{i}" + } + }) +} diff --git a/examples/todomvc.rs b/examples/todomvc.rs index 71ceee8e1..f74f56063 100644 --- a/examples/todomvc.rs +++ b/examples/todomvc.rs @@ -57,7 +57,10 @@ pub fn app(cx: Scope<()>) -> Element { placeholder: "What needs to be done?", value: "{draft}", autofocus: "true", - oninput: move |evt| draft.set(evt.value.clone()), + oninput: move |evt| { + println!("calling oninput"); + draft.set(evt.value.clone()); + }, onkeydown: move |evt| { if evt.key() == Key::Enter && !draft.is_empty() { todos.make_mut().insert( @@ -121,6 +124,8 @@ pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element { let completed = if todo.checked { "completed" } else { "" }; let editing = if **is_editing { "editing" } else { "" }; + println!("rendering todo entry"); + cx.render(rsx!{ li { class: "{completed} {editing}", diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index c27dca5ed..efc947ff3 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -13,7 +13,7 @@ pub(crate) struct ElementRef { pub path: ElementPath, // The actual template - pub template: *mut VNode<'static>, + pub template: *const VNode<'static>, } #[derive(Clone, Copy)] @@ -58,10 +58,22 @@ impl VirtualDom { } pub(crate) fn try_reclaim(&mut self, el: ElementId) -> Option { - assert_ne!(el, ElementId(0)); + if el.0 == 0 { + panic!( + "Invalid element set to 0 - {:#?}", + std::backtrace::Backtrace::force_capture() + ) + } + + println!("reclaiming {:?}", el); self.elements.try_remove(el.0) } + pub(crate) fn update_template(&mut self, el: ElementId, node: &VNode) { + let node: *const VNode = node as *const _; + self.elements[el.0].template = unsafe { std::mem::transmute(node) }; + } + // Drop a scope and all its children pub(crate) fn drop_scope(&mut self, id: ScopeId) { let scope = self.scopes.get(id.0).unwrap(); @@ -73,21 +85,15 @@ impl VirtualDom { } } - let scope = self.scopes.get(id.0).unwrap(); - - if let Some(root) = unsafe { scope.as_ref().previous_frame().try_load_node() } { - let root = unsafe { root.extend_lifetime_ref() }; - if let RenderReturn::Sync(Ok(node)) = root { - self.drop_scope_inner(node) - } - } - - let scope = self.scopes.get(id.0).unwrap().as_ref(); + let scope = self.scopes.get_mut(id.0).unwrap(); + scope.props.take(); // Drop all the hooks once the children are dropped // this means we'll drop hooks bottom-up - for hook in scope.hook_list.borrow_mut().drain(..) { + for hook in scope.hook_list.get_mut().drain(..) { + println!("dropping hook !"); drop(unsafe { BumpBox::from_raw(hook) }); + println!("hook dropped !"); } } diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index 771b1dfa6..2f11eff1f 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -129,7 +129,7 @@ impl<'b> VirtualDom { AttributeValue::Listener(_) => { self.mutations.push(NewEventListener { // all listeners start with "on" - event_name: &unbounded_name[2..], + name: &unbounded_name[2..], scope: cur_scope, id, }) diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 5ed82fbcd..4ac5553b2 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -1,3 +1,5 @@ +use std::cell::Cell; + use crate::{ arena::ElementId, factory::RenderReturn, @@ -59,10 +61,17 @@ impl<'b> VirtualDom { } fn diff_node(&mut self, left_template: &'b VNode<'b>, right_template: &'b VNode<'b>) { + println!( + "diffing node {:#?},\n\n{:#?}", + left_template.template.id, right_template.template.id + ); + if left_template.template.id != right_template.template.id { return self.light_diff_templates(left_template, right_template); } + println!("diffing attributs..."); + for (left_attr, right_attr) in left_template .dynamic_attrs .iter() @@ -73,6 +82,11 @@ impl<'b> VirtualDom { .mounted_element .set(left_attr.mounted_element.get()); + // We want to make sure anything listener that gets pulled is valid + if let AttributeValue::Listener(_) = right_attr.value { + self.update_template(left_attr.mounted_element.get(), right_template); + } + if left_attr.value != right_attr.value || left_attr.volatile { // todo: add more types of attribute values match right_attr.value { @@ -92,6 +106,8 @@ impl<'b> VirtualDom { } } + println!("diffing dyn nodes..."); + for (idx, (left_node, right_node)) in left_template .dynamic_nodes .iter() @@ -101,13 +117,24 @@ impl<'b> VirtualDom { match (left_node, right_node) { (Text(left), Text(right)) => self.diff_vtext(left, right), (Fragment(left), Fragment(right)) => self.diff_non_empty_fragment(left, right), + (Placeholder(left), Placeholder(right)) => { + right.set(left.get()); + } (Component(left), Component(right)) => { self.diff_vcomponent(left, right, right_template, idx) } - _ => self.replace(left_template, right_template), + (Placeholder(left), Fragment(right)) => { + self.replace_placeholder_with_nodes(left, right) + } + (Fragment(left), Placeholder(right)) => { + self.replace_nodes_with_placeholder(left, right) + } + _ => todo!(), }; } + println!("applying roots..."); + // Make sure the roots get transferred over for (left, right) in left_template .root_ids @@ -118,6 +145,30 @@ impl<'b> VirtualDom { } } + fn replace_placeholder_with_nodes(&mut self, l: &'b Cell, r: &'b [VNode<'b>]) { + let m = self.create_children(r); + let id = l.get(); + self.mutations.push(Mutation::ReplaceWith { id, m }); + self.reclaim(id); + } + + fn replace_nodes_with_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b Cell) { + // Create the placeholder first, ensuring we get a dedicated ID for the placeholder + let placeholder = self.next_element(&l[0], &[]); + r.set(placeholder); + self.mutations + .push(Mutation::CreatePlaceholder { id: placeholder }); + + // Remove the old nodes, except for onea + let first = self.replace_inner(&l[0]); + self.remove_nodes(&l[1..]); + + self.mutations + .push(Mutation::ReplaceWith { id: first, m: 1 }); + + self.try_reclaim(first); + } + fn diff_vcomponent( &mut self, left: &'b VComponent<'b>, @@ -140,6 +191,7 @@ impl<'b> VirtualDom { }; self.mutations .push(Mutation::ReplaceWith { id, m: created }); + self.drop_scope(left.scope.get().unwrap()); return; } @@ -154,15 +206,12 @@ impl<'b> VirtualDom { // If the props are static, then we try to memoize by setting the new with the old // The target scopestate still has the reference to the old props, so there's no need to update anything // This also implicitly drops the new props since they're not used - if left.static_props && unsafe { old.memoize(new.as_ref()) } { + if left.static_props && unsafe { old.as_ref().unwrap().memoize(new.as_ref()) } { return; } - // If the props are dynamic *or* the memoization failed, then we need to diff the props - // First, move over the props from the old to the new, dropping old props in the process - self.scopes[scope_id.0].props = unsafe { std::mem::transmute(new.as_ref()) }; - right.props.set(Some(new)); + self.scopes[scope_id.0].props = unsafe { std::mem::transmute(new) }; // Now run the component and diff it self.run_scope(scope_id); @@ -335,7 +384,7 @@ impl<'b> VirtualDom { }; } - fn diff_non_empty_fragment(&mut self, new: &'b [VNode<'b>], old: &'b [VNode<'b>]) { + fn diff_non_empty_fragment(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { let new_is_keyed = new[0].key.is_some(); let old_is_keyed = old[0].key.is_some(); debug_assert!( @@ -346,6 +395,9 @@ impl<'b> VirtualDom { old.iter().all(|o| o.key.is_some() == old_is_keyed), "all siblings must be keyed or all siblings must be non-keyed" ); + + println!("Diffing fragment {:?}, {:?}", old.len(), new.len()); + if new_is_keyed && old_is_keyed { self.diff_keyed_children(old, new); } else { @@ -368,7 +420,9 @@ impl<'b> VirtualDom { debug_assert!(!new.is_empty()); debug_assert!(!old.is_empty()); - match old.len().cmp(&new.len()) { + println!("Diffing non keyed children"); + + match dbg!(old.len().cmp(&new.len())) { Ordering::Greater => self.remove_nodes(&old[new.len()..]), Ordering::Less => self.create_and_insert_after(&new[old.len()..], old.last().unwrap()), Ordering::Equal => {} diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index 70ed3684c..750013703 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -158,7 +158,7 @@ pub enum Mutation<'a> { /// Create a new Event Listener. NewEventListener { /// The name of the event to listen for. - event_name: &'a str, + name: &'a str, /// The ID of the node to attach the listener to. scope: ScopeId, @@ -169,11 +169,11 @@ pub enum Mutation<'a> { /// Remove an existing Event Listener. RemoveEventListener { + /// The name of the event to remove. + name: &'a str, + /// The ID of the node to remove. id: ElementId, - - /// The name of the event to remove. - event: &'a str, }, Remove { id: ElementId, diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index 62f6afa33..722621ec4 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -28,7 +28,7 @@ impl VirtualDom { parent, id, height, - props, + props: Some(props), placeholder: Default::default(), node_arena_1: BumpFrame::new(50), node_arena_2: BumpFrame::new(50), @@ -86,6 +86,8 @@ impl VirtualDom { } pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> &RenderReturn { + println!("Running scope {:?}", scope_id); + // Cycle to the next frame and then reset it // This breaks any latent references, invalidating every pointer referencing into it. // Remove all the outdated listeners @@ -100,7 +102,8 @@ impl VirtualDom { scope.hook_idx.set(0); // safety: due to how we traverse the tree, we know that the scope is not currently aliased - let props: &dyn AnyProps = mem::transmute(&*scope.props); + let props = scope.props.as_ref().unwrap().as_ref(); + let props: &dyn AnyProps = mem::transmute(props); props.render(scope).extend_lifetime() }; diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index ae944792b..776414563 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -82,7 +82,7 @@ pub struct ScopeState { pub(crate) tasks: Rc, pub(crate) spawned_tasks: HashSet, - pub(crate) props: Box>, + pub(crate) props: Option>>, pub(crate) placeholder: Cell>, } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 31a693b9b..77619e674 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -362,6 +362,8 @@ impl VirtualDom { let target_path = el_ref.path; for (idx, attr) in template.dynamic_attrs.iter().enumerate() { + println!("{:?} \n {:?} \n {:?}", attr, name, element); + let this_path = template.template.attr_paths[idx]; // listeners are required to be prefixed with "on", but they come back to the virtualdom with that missing @@ -383,6 +385,8 @@ impl VirtualDom { } } + println!("calling listeners: {:?}", listeners); + // Now that we've accumulated all the parent attributes for the target element, call them in reverse order // We check the bubble state between each call to see if the event has been stopped from bubbling for listener in listeners.drain(..).rev() { @@ -399,6 +403,8 @@ impl VirtualDom { parent_path = template.parent.and_then(|id| self.elements.get(id.0)); } + + println!("all listeners exhausted"); } /// Wait for the scheduler to have any work. @@ -448,12 +454,15 @@ impl VirtualDom { } } - /// Swap the current mutations with a new - fn finalize(&mut self) -> Mutations { - // todo: make this a routine - let mut out = Mutations::default(); - std::mem::swap(&mut self.mutations, &mut out); - out + /// Process all events in the queue until there are no more left + pub fn process_events(&mut self) { + while let Ok(Some(msg)) = self.rx.try_next() { + match msg { + SchedulerMsg::Immediate(id) => self.mark_dirty(id), + SchedulerMsg::TaskNotified(task) => self.handle_task_wakeup(task), + SchedulerMsg::SuspenseNotified(id) => self.handle_suspense_wakeup(id), + } + } } /// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom from scratch. @@ -515,6 +524,7 @@ impl VirtualDom { /// /// If no suspense trees are present pub async fn render_with_deadline(&mut self, deadline: impl Future) -> Mutations { + println!("render with deadline"); pin_mut!(deadline); loop { @@ -600,6 +610,14 @@ impl VirtualDom { } } } + + /// Swap the current mutations with a new + fn finalize(&mut self) -> Mutations { + // todo: make this a routine + let mut out = Mutations::default(); + std::mem::swap(&mut self.mutations, &mut out); + out + } } impl Drop for VirtualDom { diff --git a/packages/core/tests/kitchen_sink.rs b/packages/core/tests/kitchen_sink.rs index 7071cb120..7f719f772 100644 --- a/packages/core/tests/kitchen_sink.rs +++ b/packages/core/tests/kitchen_sink.rs @@ -70,7 +70,7 @@ fn dual_stream() { [ LoadTemplate { name: "template", index: 0, id: ElementId(1) }, SetAttribute { name: "class", value: "123", id: ElementId(1), ns: None }, - NewEventListener { event_name: "click", scope: ScopeId(0), id: ElementId(1) }, + NewEventListener { name: "click", scope: ScopeId(0), id: ElementId(1) }, HydrateText { path: &[0, 0], value: "123", id: ElementId(2) }, AppendChildren { m: 1 } ], diff --git a/packages/core/tests/miri_full_app.rs b/packages/core/tests/miri_full_app.rs new file mode 100644 index 000000000..6cd3d2bd8 --- /dev/null +++ b/packages/core/tests/miri_full_app.rs @@ -0,0 +1,49 @@ +use dioxus::prelude::*; +use dioxus_core::ElementId; +use std::rc::Rc; + +#[test] +fn miri_rollover() { + let mut dom = VirtualDom::new(app); + + _ = dom.rebuild(); + + for x in 0..3 { + dom.handle_event("click", Rc::new(MouseData::default()), ElementId(2), true); + dom.process_events(); + dom.render_immediate(); + } +} + +fn app(cx: Scope) -> Element { + let mut idx = use_state(cx, || 0); + let onhover = |h| println!("go!"); + + cx.render(rsx! { + div { + button { + onclick: move |_| { + idx += 1; + println!("Clicked"); + }, + "+" + } + button { onclick: move |_| idx -= 1, "-" } + ul { + (0..**idx).map(|i| rsx! { + Child { i: i, onhover: onhover } + }) + } + } + }) +} + +#[inline_props] +fn Child<'a>(cx: Scope<'a>, i: i32, onhover: EventHandler<'a, MouseEvent>) -> Element { + cx.render(rsx! { + li { + onmouseover: move |e| onhover.call(e), + "{i}" + } + }) +} diff --git a/packages/desktop/src/controller.rs b/packages/desktop/src/controller.rs index 3aff11352..19ac205b4 100644 --- a/packages/desktop/src/controller.rs +++ b/packages/desktop/src/controller.rs @@ -56,7 +56,7 @@ impl DesktopController { let mut queue = edit_queue.lock().unwrap(); queue.push(serde_json::to_string(&edits.template_mutations).unwrap()); queue.push(serde_json::to_string(&edits.edits).unwrap()); - proxy.send_event(UserWindowEvent::Update).unwrap(); + proxy.send_event(UserWindowEvent::EditsReady).unwrap(); } loop { @@ -67,7 +67,7 @@ impl DesktopController { let name = value.event.clone(); let el_id = ElementId(value.mounted_dom_id); if let Some(evt) = decode_event(value) { - dom.handle_event(&name, evt, el_id, true); + dom.handle_event(&name, evt, el_id, dioxus_html::events::event_bubbles(&name)); } } } @@ -81,7 +81,7 @@ impl DesktopController { let mut queue = edit_queue.lock().unwrap(); queue.push(serde_json::to_string(&muts.template_mutations).unwrap()); queue.push(serde_json::to_string(&muts.edits).unwrap()); - let _ = proxy.send_event(UserWindowEvent::Update); + let _ = proxy.send_event(UserWindowEvent::EditsReady); } } }) @@ -116,6 +116,8 @@ impl DesktopController { let (_id, view) = self.webviews.iter_mut().next().unwrap(); + println!("processing pending edits {:?}", new_queue.len()); + for edit in new_queue.drain(..) { view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit)) .unwrap(); diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 757a35360..71357e0ab 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -161,7 +161,8 @@ impl DesktopContext { #[derive(Debug)] pub enum UserWindowEvent { - Update, + EditsReady, + Initialize, CloseWindow, DragWindow, @@ -213,7 +214,7 @@ pub(super) fn handler( println!("user_event: {:?}", user_event); match user_event { - Update => desktop.try_load_ready_webviews(), + Initialize | EditsReady => desktop.try_load_ready_webviews(), CloseWindow => *control_flow = ControlFlow::Exit, DragWindow => { // if the drag_window has any errors, we don't do anything diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index cbdf3a4c4..a729a2f6f 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -154,7 +154,7 @@ pub fn launch_with_props(root: Component

, props: P, mut cf "initialize" => { is_ready.store(true, std::sync::atomic::Ordering::Relaxed); println!("initializing..."); - let _ = proxy.send_event(UserWindowEvent::Update); + let _ = proxy.send_event(UserWindowEvent::EditsReady); } "browser_open" => { let data = message.params(); diff --git a/packages/html/src/events.rs b/packages/html/src/events.rs index 7e903b99e..5107e2cda 100644 --- a/packages/html/src/events.rs +++ b/packages/html/src/events.rs @@ -60,90 +60,90 @@ pub use touch::*; pub use transition::*; pub use wheel::*; -// pub fn event_bubbles(evt: &str) -> bool { -// match evt { -// "copy" => true, -// "cut" => true, -// "paste" => true, -// "compositionend" => true, -// "compositionstart" => true, -// "compositionupdate" => true, -// "keydown" => true, -// "keypress" => true, -// "keyup" => true, -// "focus" => false, -// "focusout" => true, -// "focusin" => true, -// "blur" => false, -// "change" => true, -// "input" => true, -// "invalid" => true, -// "reset" => true, -// "submit" => true, -// "click" => true, -// "contextmenu" => true, -// "doubleclick" => true, -// "dblclick" => true, -// "drag" => true, -// "dragend" => true, -// "dragenter" => false, -// "dragexit" => false, -// "dragleave" => true, -// "dragover" => true, -// "dragstart" => true, -// "drop" => true, -// "mousedown" => true, -// "mouseenter" => false, -// "mouseleave" => false, -// "mousemove" => true, -// "mouseout" => true, -// "scroll" => false, -// "mouseover" => true, -// "mouseup" => true, -// "pointerdown" => true, -// "pointermove" => true, -// "pointerup" => true, -// "pointercancel" => true, -// "gotpointercapture" => true, -// "lostpointercapture" => true, -// "pointerenter" => false, -// "pointerleave" => false, -// "pointerover" => true, -// "pointerout" => true, -// "select" => true, -// "touchcancel" => true, -// "touchend" => true, -// "touchmove" => true, -// "touchstart" => true, -// "wheel" => true, -// "abort" => false, -// "canplay" => false, -// "canplaythrough" => false, -// "durationchange" => false, -// "emptied" => false, -// "encrypted" => true, -// "ended" => false, -// "error" => false, -// "loadeddata" => false, -// "loadedmetadata" => false, -// "loadstart" => false, -// "pause" => false, -// "play" => false, -// "playing" => false, -// "progress" => false, -// "ratechange" => false, -// "seeked" => false, -// "seeking" => false, -// "stalled" => false, -// "suspend" => false, -// "timeupdate" => false, -// "volumechange" => false, -// "waiting" => false, -// "animationstart" => true, -// "animationend" => true, -// "animationiteration" => true, -// "transitionend" => true, -// "toggle" => true, -// _ => panic!("unsupported event type {:?}", evt), -// } -// } +pub fn event_bubbles(evt: &str) -> bool { + match evt { + "copy" => true, + "cut" => true, + "paste" => true, + "compositionend" => true, + "compositionstart" => true, + "compositionupdate" => true, + "keydown" => true, + "keypress" => true, + "keyup" => true, + "focus" => false, + "focusout" => true, + "focusin" => true, + "blur" => false, + "change" => true, + "input" => true, + "invalid" => true, + "reset" => true, + "submit" => true, + "click" => true, + "contextmenu" => true, + "doubleclick" => true, + "dblclick" => true, + "drag" => true, + "dragend" => true, + "dragenter" => false, + "dragexit" => false, + "dragleave" => true, + "dragover" => true, + "dragstart" => true, + "drop" => true, + "mousedown" => true, + "mouseenter" => false, + "mouseleave" => false, + "mousemove" => true, + "mouseout" => true, + "scroll" => false, + "mouseover" => true, + "mouseup" => true, + "pointerdown" => true, + "pointermove" => true, + "pointerup" => true, + "pointercancel" => true, + "gotpointercapture" => true, + "lostpointercapture" => true, + "pointerenter" => false, + "pointerleave" => false, + "pointerover" => true, + "pointerout" => true, + "select" => true, + "touchcancel" => true, + "touchend" => true, + "touchmove" => true, + "touchstart" => true, + "wheel" => true, + "abort" => false, + "canplay" => false, + "canplaythrough" => false, + "durationchange" => false, + "emptied" => false, + "encrypted" => true, + "ended" => false, + "error" => false, + "loadeddata" => false, + "loadedmetadata" => false, + "loadstart" => false, + "pause" => false, + "play" => false, + "playing" => false, + "progress" => false, + "ratechange" => false, + "seeked" => false, + "seeking" => false, + "stalled" => false, + "suspend" => false, + "timeupdate" => false, + "volumechange" => false, + "waiting" => false, + "animationstart" => true, + "animationend" => true, + "animationiteration" => true, + "transitionend" => true, + "toggle" => true, + _ => true, + } +} diff --git a/packages/html/src/lib.rs b/packages/html/src/lib.rs index 871d78e3f..325116a34 100644 --- a/packages/html/src/lib.rs +++ b/packages/html/src/lib.rs @@ -18,8 +18,8 @@ pub mod events; pub mod geometry; mod global_attributes; pub mod input_data; -// #[cfg(feature = "wasm-bind")] -// mod web_sys_bind; +#[cfg(feature = "wasm-bind")] +mod web_sys_bind; pub use elements::*; pub use events::*; diff --git a/packages/html/src/web_sys_bind/events.rs b/packages/html/src/web_sys_bind/events.rs index 197eed737..5f0e5b2a3 100644 --- a/packages/html/src/web_sys_bind/events.rs +++ b/packages/html/src/web_sys_bind/events.rs @@ -1,9 +1,9 @@ -use crate::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint}; -use crate::input_data::{decode_key_location, decode_mouse_button_set, MouseButton}; -use crate::on::{ +use crate::events::{ AnimationData, CompositionData, KeyboardData, MouseData, PointerData, TouchData, TransitionData, WheelData, }; +use crate::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint}; +use crate::input_data::{decode_key_location, decode_mouse_button_set, MouseButton}; use keyboard_types::{Code, Key, Modifiers}; use std::convert::TryInto; use std::str::FromStr; diff --git a/packages/interpreter/src/bindings.rs b/packages/interpreter/src/bindings.rs index 0587368b5..708842e23 100644 --- a/packages/interpreter/src/bindings.rs +++ b/packages/interpreter/src/bindings.rs @@ -2,7 +2,7 @@ use js_sys::Function; use wasm_bindgen::prelude::*; -use web_sys::{Element, Node}; +use web_sys::Element; #[wasm_bindgen(module = "/src/interpreter.js")] extern "C" { @@ -12,86 +12,83 @@ extern "C" { pub fn new(arg: Element) -> Interpreter; #[wasm_bindgen(method)] - pub fn SetNode(this: &Interpreter, id: usize, node: Node); + pub fn AppendChildren(this: &Interpreter, many: u32); #[wasm_bindgen(method)] - pub fn AppendChildren(this: &Interpreter, root: Option, children: Vec); + pub fn AssignId(this: &Interpreter, path: &[u8], id: u32); #[wasm_bindgen(method)] - pub fn ReplaceWith(this: &Interpreter, root: Option, nodes: Vec); + pub fn CreateElement(this: &Interpreter, tag: &str); #[wasm_bindgen(method)] - pub fn InsertAfter(this: &Interpreter, root: Option, nodes: Vec); + pub fn CreateElementNs(this: &Interpreter, tag: &str, ns: &str); #[wasm_bindgen(method)] - pub fn InsertBefore(this: &Interpreter, root: Option, nodes: Vec); + pub fn CreatePlaceholder(this: &Interpreter, id: u32); #[wasm_bindgen(method)] - pub fn Remove(this: &Interpreter, root: Option); + pub fn CreateStaticPlaceholder(this: &Interpreter); #[wasm_bindgen(method)] - pub fn CreateTextNode(this: &Interpreter, text: JsValue, root: Option); + pub fn CreateTextPlaceholder(this: &Interpreter); #[wasm_bindgen(method)] - pub fn CreateElement(this: &Interpreter, tag: &str, root: Option, children: u32); + pub fn CreateStaticText(this: &Interpreter, value: &str); #[wasm_bindgen(method)] - pub fn CreateElementNs( - this: &Interpreter, - tag: &str, - root: Option, - ns: &str, - children: u32, - ); + pub fn CreateTextNode(this: &Interpreter, value: JsValue, id: u32); #[wasm_bindgen(method)] - pub fn CreatePlaceholder(this: &Interpreter, root: Option); + pub fn HydrateText(this: &Interpreter, path: &[u8], value: &str, id: u32); + + #[wasm_bindgen(method)] + pub fn LoadTemplate(this: &Interpreter, name: &str, index: u32, id: u32); + + #[wasm_bindgen(method)] + pub fn ReplaceWith(this: &Interpreter, id: u32, m: u32); + + #[wasm_bindgen(method)] + pub fn ReplacePlaceholder(this: &Interpreter, path: &[u8], m: u32); + + #[wasm_bindgen(method)] + pub fn InsertAfter(this: &Interpreter, id: u32, n: u32); + + #[wasm_bindgen(method)] + pub fn InsertBefore(this: &Interpreter, id: u32, n: u32); + + #[wasm_bindgen(method)] + pub fn SaveTemplate(this: &Interpreter, name: &str, m: u32); + + #[wasm_bindgen(method)] + pub fn SetAttribute(this: &Interpreter, id: u32, name: &str, value: JsValue, ns: Option<&str>); + + #[wasm_bindgen(method)] + pub fn SetStaticAttribute(this: &Interpreter, name: &str, value: JsValue, ns: Option<&str>); + + #[wasm_bindgen(method)] + pub fn SetBoolAttribute(this: &Interpreter, id: u32, name: &str, value: bool); + + #[wasm_bindgen(method)] + pub fn SetText(this: &Interpreter, id: u32, text: JsValue); #[wasm_bindgen(method)] pub fn NewEventListener( this: &Interpreter, name: &str, - root: Option, + id: u32, handler: &Function, bubbles: bool, ); #[wasm_bindgen(method)] - pub fn RemoveEventListener(this: &Interpreter, root: Option, name: &str, bubbles: bool); + pub fn RemoveEventListener(this: &Interpreter, name: &str, id: u32); #[wasm_bindgen(method)] - pub fn SetText(this: &Interpreter, root: Option, text: JsValue); + pub fn RemoveAttribute(this: &Interpreter, id: u32, field: &str, ns: Option<&str>); #[wasm_bindgen(method)] - pub fn SetAttribute( - this: &Interpreter, - root: Option, - field: &str, - value: JsValue, - ns: Option<&str>, - ); + pub fn Remove(this: &Interpreter, id: u32); #[wasm_bindgen(method)] - pub fn RemoveAttribute(this: &Interpreter, root: Option, field: &str, ns: Option<&str>); - - #[wasm_bindgen(method)] - pub fn CloneNode(this: &Interpreter, root: Option, new_id: u64); - - #[wasm_bindgen(method)] - pub fn CloneNodeChildren(this: &Interpreter, root: Option, new_ids: Vec); - - #[wasm_bindgen(method)] - pub fn FirstChild(this: &Interpreter); - - #[wasm_bindgen(method)] - pub fn NextSibling(this: &Interpreter); - - #[wasm_bindgen(method)] - pub fn ParentNode(this: &Interpreter); - - #[wasm_bindgen(method)] - pub fn StoreWithId(this: &Interpreter, id: u64); - - #[wasm_bindgen(method)] - pub fn SetLastNode(this: &Interpreter, id: u64); + pub fn PushRoot(this: &Interpreter, id: u32); } diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index bcfd77865..9bede888c 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -265,7 +265,14 @@ export class Interpreter { SaveTemplate(name, m) { this.templates[name] = this.stack.splice(this.stack.length - m); } + CreateStaticText(text) { + this.CreateTextNode(text); + } + CreateTextPlaceholder() { + this.CreateRawText("placeholder"); + } handleEdit(edit) { + console.log(edit); switch (edit.type) { case "AppendChildren": this.AppendChildren(edit.m); @@ -283,13 +290,13 @@ export class Interpreter { this.CreatePlaceholder(edit.id); break; case "CreateStaticText": - this.CreateTextNode(edit.value); + this.CreateStaticText(edit.value) break; case "CreateStaticPlaceholder": this.CreateStaticPlaceholder(); break; case "CreateTextPlaceholder": - this.CreateRawText("placeholder"); + this.CreateTextPlaceholder(); break; case "CreateStaticText": this.CreateRawText(edit.value); @@ -301,7 +308,7 @@ export class Interpreter { this.HydrateText(edit.path, edit.value, edit.id); break; case "LoadTemplate": - this.LoadTemplate(edit.name, edit.index); + this.LoadTemplate(edit.name, edit.index, edit.id); break; case "SaveTemplate": this.SaveTemplate(edit.name, edit.m); diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 7d8e6a3cc..5be66b63d 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -77,7 +77,7 @@ features = [ ] [features] -default = ["panic_hook", "hydrate"] +default = ["panic_hook"] panic_hook = ["console_error_panic_hook"] hydrate = [] diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 98e89ee47..20e50c0fd 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -7,11 +7,12 @@ //! - tests to ensure dyn_into works for various event types. //! - Partial delegation?> -use dioxus_core::{DomEdit, ElementId, SchedulerMsg, UserEvent}; -use dioxus_html::event_bubbles; +use dioxus_core::{ElementId, Mutation, Mutations}; +use dioxus_html::{event_bubbles, CompositionData, FormData}; use dioxus_interpreter_js::Interpreter; +use futures_channel::mpsc; use js_sys::Function; -use std::{any::Any, rc::Rc, sync::Arc}; +use std::{any::Any, rc::Rc}; use wasm_bindgen::{closure::Closure, JsCast, JsValue}; use web_sys::{Document, Element, Event, HtmlElement}; @@ -26,68 +27,26 @@ pub struct WebsysDom { } impl WebsysDom { - pub fn new(cfg: Config, sender_callback: Rc) -> Self { + pub fn new(cfg: Config, event_channel: mpsc::UnboundedSender) -> Self { // eventually, we just want to let the interpreter do all the work of decoding events into our event type let callback: Box = Box::new(move |event: &web_sys::Event| { - let mut target = event - .target() - .expect("missing target") - .dyn_into::() - .expect("not a valid element"); + _ = event_channel.unbounded_send(event.clone()); - let typ = event.type_(); + // if let Ok(synthetic_event) = decoded { + // // Try to prevent default if the attribute is set + // if let Some(node) = target.dyn_ref::() { + // if let Some(name) = node.get_attribute("dioxus-prevent-default") { + // if name == synthetic_event.name + // || name.trim_start_matches("on") == synthetic_event.name + // { + // log::trace!("Preventing default"); + // event.prevent_default(); + // } + // } + // } - let decoded: anyhow::Result = loop { - match target.get_attribute("data-dioxus-id").map(|f| f.parse()) { - Some(Ok(id)) => { - break Ok(UserEvent { - name: event_name_from_typ(&typ), - data: virtual_event_from_websys_event(event.clone(), target.clone()), - element: Some(ElementId(id)), - scope_id: None, - priority: dioxus_core::EventPriority::Medium, - bubbles: event.bubbles(), - }); - } - Some(Err(e)) => { - break Err(e.into()); - } - None => { - // walk the tree upwards until we actually find an event target - if let Some(parent) = target.parent_element() { - target = parent; - } else { - break Ok(UserEvent { - name: event_name_from_typ(&typ), - data: virtual_event_from_websys_event( - event.clone(), - target.clone(), - ), - element: None, - scope_id: None, - priority: dioxus_core::EventPriority::Low, - bubbles: event.bubbles(), - }); - } - } - } - }; - - if let Ok(synthetic_event) = decoded { - // Try to prevent default if the attribute is set - if let Some(node) = target.dyn_ref::() { - if let Some(name) = node.get_attribute("dioxus-prevent-default") { - if name == synthetic_event.name - || name.trim_start_matches("on") == synthetic_event.name - { - log::trace!("Preventing default"); - event.prevent_default(); - } - } - } - - sender_callback.as_ref()(SchedulerMsg::Event(synthetic_event)) - } + // sender_callback.as_ref()(SchedulerMsg::Event(synthetic_event)) + // } }); // a match here in order to avoid some error during runtime browser test @@ -104,75 +63,56 @@ impl WebsysDom { } } - pub fn apply_edits(&mut self, mut edits: Vec) { + pub fn apply_edits(&mut self, mut edits: Vec) { + use Mutation::*; + let i = &self.interpreter; + for edit in edits.drain(..) { match edit { - DomEdit::AppendChildren { root, children } => { - self.interpreter.AppendChildren(root, children); - } - DomEdit::ReplaceWith { root, nodes } => self.interpreter.ReplaceWith(root, nodes), - DomEdit::InsertAfter { root, nodes } => self.interpreter.InsertAfter(root, nodes), - DomEdit::InsertBefore { root, nodes } => self.interpreter.InsertBefore(root, nodes), - DomEdit::Remove { root } => self.interpreter.Remove(root), - - DomEdit::CreateElement { - root, - tag, - children, - } => self.interpreter.CreateElement(tag, root, children), - DomEdit::CreateElementNs { - root, - tag, + AppendChildren { m } => i.AppendChildren(m as u32), + AssignId { path, id } => i.AssignId(path, id.0 as u32), + CreateElement { name } => i.CreateElement(name), + CreateElementNamespace { name, namespace } => i.CreateElementNs(name, namespace), + CreatePlaceholder { id } => i.CreatePlaceholder(id.0 as u32), + CreateStaticPlaceholder => i.CreateStaticPlaceholder(), + CreateTextPlaceholder => i.CreateTextPlaceholder(), + CreateStaticText { value } => i.CreateStaticText(value), + CreateTextNode { value, id } => i.CreateTextNode(value.into(), id.0 as u32), + HydrateText { path, value, id } => i.HydrateText(path, value, id.0 as u32), + LoadTemplate { name, index, id } => i.LoadTemplate(name, index as u32, id.0 as u32), + ReplaceWith { id, m } => i.ReplaceWith(id.0 as u32, m as u32), + ReplacePlaceholder { path, m } => i.ReplacePlaceholder(path, m as u32), + InsertAfter { id, m } => i.InsertAfter(id.0 as u32, m as u32), + InsertBefore { id, m } => i.InsertBefore(id.0 as u32, m as u32), + SaveTemplate { name, m } => i.SaveTemplate(name, m as u32), + SetAttribute { + name, + value, + id, ns, - children, - } => self.interpreter.CreateElementNs(tag, root, ns, children), - DomEdit::CreatePlaceholder { root } => self.interpreter.CreatePlaceholder(root), - DomEdit::NewEventListener { - event_name, root, .. - } => { + } => i.SetAttribute(id.0 as u32, name, value.into(), ns), + SetStaticAttribute { name, value, ns } => { + i.SetStaticAttribute(name, value.into(), ns) + } + SetBoolAttribute { name, value, id } => { + i.SetBoolAttribute(id.0 as u32, name, value) + } + SetText { value, id } => i.SetText(id.0 as u32, value.into()), + NewEventListener { name, scope, id } => { let handler: &Function = self.handler.as_ref().unchecked_ref(); self.interpreter.NewEventListener( - event_name, - root, + name, + id.0 as u32, handler, - event_bubbles(event_name), + event_bubbles(&name[2..]), ); } - - DomEdit::RemoveEventListener { root, event } => self - .interpreter - .RemoveEventListener(root, event, event_bubbles(event)), - - DomEdit::RemoveAttribute { root, name, ns } => { - self.interpreter.RemoveAttribute(root, name, ns) - } - - DomEdit::CreateTextNode { text, root } => { - let text = JsValue::from_str(text); - self.interpreter.CreateTextNode(text, root) - } - DomEdit::SetText { root, text } => { - let text = JsValue::from_str(text); - self.interpreter.SetText(root, text) - } - DomEdit::SetAttribute { - root, - field, - value, - ns, - } => { - let value = JsValue::from_str(&value.to_string()); - self.interpreter.SetAttribute(root, field, value, ns) - } - DomEdit::CloneNode { id, new_id } => self.interpreter.CloneNode(id, new_id), - DomEdit::CloneNodeChildren { id, new_ids } => { - self.interpreter.CloneNodeChildren(id, new_ids) - } - DomEdit::FirstChild {} => self.interpreter.FirstChild(), - DomEdit::NextSibling {} => self.interpreter.NextSibling(), - DomEdit::ParentNode {} => self.interpreter.ParentNode(), - DomEdit::StoreWithId { id } => self.interpreter.StoreWithId(id), - DomEdit::SetLastNode { id } => self.interpreter.SetLastNode(id), + RemoveEventListener { name, id } => i.RemoveEventListener(name, id.0 as u32), + Remove { id } => i.Remove(id.0 as u32), + PushRoot { id } => i.PushRoot(id.0 as u32), + // Mutation::RemoveEventListener { root, name: event } => self + // .interpreter + // .RemoveEventListener(root, event, event_bubbles(event)), } } } @@ -187,129 +127,54 @@ unsafe impl Sync for DioxusWebsysEvent {} // todo: some of these events are being casted to the wrong event type. // We need tests that simulate clicks/etc and make sure every event type works. -fn virtual_event_from_websys_event( - event: web_sys::Event, - target: Element, -) -> Arc { - use dioxus_html::on::*; +pub fn virtual_event_from_websys_event(event: web_sys::Event, target: Element) -> Rc { + use dioxus_html::events::*; match event.type_().as_str() { - "copy" | "cut" | "paste" => Arc::new(ClipboardData {}), + "copy" | "cut" | "paste" => Rc::new(ClipboardData {}), "compositionend" | "compositionstart" | "compositionupdate" => { - let evt: &web_sys::CompositionEvent = event.dyn_ref().unwrap(); - Arc::new(CompositionData { - data: evt.data().unwrap_or_default(), - }) + make_composition_event(&event) } - "keydown" | "keypress" | "keyup" => Arc::new(KeyboardData::from(event)), - "focus" | "blur" | "focusout" | "focusin" => Arc::new(FocusData {}), + "keydown" | "keypress" | "keyup" => Rc::new(KeyboardData::from(event)), + "focus" | "blur" | "focusout" | "focusin" => Rc::new(FocusData {}), - // todo: these handlers might get really slow if the input box gets large and allocation pressure is heavy - // don't have a good solution with the serialized event problem - "change" | "input" | "invalid" | "reset" | "submit" => { - let value: String = target - .dyn_ref() - .map(|input: &web_sys::HtmlInputElement| { - // todo: special case more input types - match input.type_().as_str() { - "checkbox" => { - match input.checked() { - true => "true".to_string(), - false => "false".to_string(), - } - }, - _ => { - input.value() - } - } - }) - .or_else(|| { - target - .dyn_ref() - .map(|input: &web_sys::HtmlTextAreaElement| input.value()) - }) - // select elements are NOT input events - because - why woudn't they be?? - .or_else(|| { - target - .dyn_ref() - .map(|input: &web_sys::HtmlSelectElement| input.value()) - }) - .or_else(|| { - target - .dyn_ref::() - .unwrap() - .text_content() - }) - .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener"); + "change" | "input" | "invalid" | "reset" | "submit" => read_input_to_data(target), - let mut values = std::collections::HashMap::new(); - - // try to fill in form values - if let Some(form) = target.dyn_ref::() { - let elements = form.elements(); - for x in 0..elements.length() { - let element = elements.item(x).unwrap(); - if let Some(name) = element.get_attribute("name") { - let value: Option = element - .dyn_ref() - .map(|input: &web_sys::HtmlInputElement| { - match input.type_().as_str() { - "checkbox" => { - match input.checked() { - true => Some("true".to_string()), - false => Some("false".to_string()), - } - }, - "radio" => { - match input.checked() { - true => Some(input.value()), - false => None, - } - } - _ => Some(input.value()) - } - }) - .or_else(|| element.dyn_ref().map(|input: &web_sys::HtmlTextAreaElement| Some(input.value()))) - .or_else(|| element.dyn_ref().map(|input: &web_sys::HtmlSelectElement| Some(input.value()))) - .or_else(|| Some(element.dyn_ref::().unwrap().text_content())) - .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener"); - if let Some(value) = value { - values.insert(name, value); - } - } - } - } - - Arc::new(FormData { value, values }) - } "click" | "contextmenu" | "dblclick" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit" | "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter" | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => { - Arc::new(MouseData::from(event)) + Rc::new(MouseData::from(event)) } "pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture" | "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => { - Arc::new(PointerData::from(event)) + Rc::new(PointerData::from(event)) } - "select" => Arc::new(SelectionData {}), - "touchcancel" | "touchend" | "touchmove" | "touchstart" => Arc::new(TouchData::from(event)), + "select" => Rc::new(SelectionData {}), + "touchcancel" | "touchend" | "touchmove" | "touchstart" => Rc::new(TouchData::from(event)), - "scroll" => Arc::new(()), - "wheel" => Arc::new(WheelData::from(event)), + "scroll" => Rc::new(()), + "wheel" => Rc::new(WheelData::from(event)), "animationstart" | "animationend" | "animationiteration" => { - Arc::new(AnimationData::from(event)) + Rc::new(AnimationData::from(event)) } - "transitionend" => Arc::new(TransitionData::from(event)), + "transitionend" => Rc::new(TransitionData::from(event)), "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted" | "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play" | "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend" - | "timeupdate" | "volumechange" | "waiting" => Arc::new(MediaData {}), - "toggle" => Arc::new(ToggleData {}), + | "timeupdate" | "volumechange" | "waiting" => Rc::new(MediaData {}), + "toggle" => Rc::new(ToggleData {}), - _ => Arc::new(()), + _ => Rc::new(()), } } +fn make_composition_event(event: &Event) -> Rc { + let evt: &web_sys::CompositionEvent = event.dyn_ref().unwrap(); + Rc::new(CompositionData { + data: evt.data().unwrap_or_default(), + }) +} + pub(crate) fn load_document() -> Document { web_sys::window() .expect("should have access to the Window") @@ -317,92 +182,86 @@ pub(crate) fn load_document() -> Document { .expect("should have access to the Document") } -fn event_name_from_typ(typ: &str) -> &'static str { - match typ { - "copy" => "copy", - "cut" => "cut", - "paste" => "paste", - "compositionend" => "compositionend", - "compositionstart" => "compositionstart", - "compositionupdate" => "compositionupdate", - "keydown" => "keydown", - "keypress" => "keypress", - "keyup" => "keyup", - "focus" => "focus", - "focusout" => "focusout", - "focusin" => "focusin", - "blur" => "blur", - "change" => "change", - "input" => "input", - "invalid" => "invalid", - "reset" => "reset", - "submit" => "submit", - "click" => "click", - "contextmenu" => "contextmenu", - "doubleclick" => "doubleclick", - "dblclick" => "dblclick", - "drag" => "drag", - "dragend" => "dragend", - "dragenter" => "dragenter", - "dragexit" => "dragexit", - "dragleave" => "dragleave", - "dragover" => "dragover", - "dragstart" => "dragstart", - "drop" => "drop", - "mousedown" => "mousedown", - "mouseenter" => "mouseenter", - "mouseleave" => "mouseleave", - "mousemove" => "mousemove", - "mouseout" => "mouseout", - "mouseover" => "mouseover", - "mouseup" => "mouseup", - "pointerdown" => "pointerdown", - "pointermove" => "pointermove", - "pointerup" => "pointerup", - "pointercancel" => "pointercancel", - "gotpointercapture" => "gotpointercapture", - "lostpointercapture" => "lostpointercapture", - "pointerenter" => "pointerenter", - "pointerleave" => "pointerleave", - "pointerover" => "pointerover", - "pointerout" => "pointerout", - "select" => "select", - "touchcancel" => "touchcancel", - "touchend" => "touchend", - "touchmove" => "touchmove", - "touchstart" => "touchstart", - "scroll" => "scroll", - "wheel" => "wheel", - "animationstart" => "animationstart", - "animationend" => "animationend", - "animationiteration" => "animationiteration", - "transitionend" => "transitionend", - "abort" => "abort", - "canplay" => "canplay", - "canplaythrough" => "canplaythrough", - "durationchange" => "durationchange", - "emptied" => "emptied", - "encrypted" => "encrypted", - "ended" => "ended", - "error" => "error", - "loadeddata" => "loadeddata", - "loadedmetadata" => "loadedmetadata", - "loadstart" => "loadstart", - "pause" => "pause", - "play" => "play", - "playing" => "playing", - "progress" => "progress", - "ratechange" => "ratechange", - "seeked" => "seeked", - "seeking" => "seeking", - "stalled" => "stalled", - "suspend" => "suspend", - "timeupdate" => "timeupdate", - "volumechange" => "volumechange", - "waiting" => "waiting", - "toggle" => "toggle", - a => { - panic!("unsupported event type {:?}", a); +fn read_input_to_data(target: Element) -> Rc { + // todo: these handlers might get really slow if the input box gets large and allocation pressure is heavy + // don't have a good solution with the serialized event problem + + let value: String = target + .dyn_ref() + .map(|input: &web_sys::HtmlInputElement| { + // todo: special case more input types + match input.type_().as_str() { + "checkbox" => { + match input.checked() { + true => "true".to_string(), + false => "false".to_string(), + } + }, + _ => { + input.value() + } + } + }) + .or_else(|| { + target + .dyn_ref() + .map(|input: &web_sys::HtmlTextAreaElement| input.value()) + }) + // select elements are NOT input events - because - why woudn't they be?? + .or_else(|| { + target + .dyn_ref() + .map(|input: &web_sys::HtmlSelectElement| input.value()) + }) + .or_else(|| { + target + .dyn_ref::() + .unwrap() + .text_content() + }) + .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener"); + + let mut values = std::collections::HashMap::new(); + + // try to fill in form values + if let Some(form) = target.dyn_ref::() { + let elements = form.elements(); + for x in 0..elements.length() { + let element = elements.item(x).unwrap(); + if let Some(name) = element.get_attribute("name") { + let value: Option = element + .dyn_ref() + .map(|input: &web_sys::HtmlInputElement| { + match input.type_().as_str() { + "checkbox" => { + match input.checked() { + true => Some("true".to_string()), + false => Some("false".to_string()), + } + }, + "radio" => { + match input.checked() { + true => Some(input.value()), + false => None, + } + } + _ => Some(input.value()) + } + }) + .or_else(|| element.dyn_ref().map(|input: &web_sys::HtmlTextAreaElement| Some(input.value()))) + .or_else(|| element.dyn_ref().map(|input: &web_sys::HtmlSelectElement| Some(input.value()))) + .or_else(|| Some(element.dyn_ref::().unwrap().text_content())) + .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener"); + if let Some(value) = value { + values.insert(name, value); + } + } } } + + Rc::new(FormData { + value, + values, + files: None, + }) } diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 4c0831ef5..5b33bb9a4 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -54,21 +54,23 @@ // - Do the VDOM work during the idlecallback // - Do DOM work in the next requestAnimationFrame callback -use std::rc::Rc; +use std::{rc::Rc, time::Duration}; pub use crate::cfg::Config; +use crate::dom::virtual_event_from_websys_event; pub use crate::util::use_eval; -use dioxus_core::prelude::Component; -use dioxus_core::SchedulerMsg; -use dioxus_core::VirtualDom; +use dioxus_core::{Element, ElementId, Scope, VirtualDom}; +use futures_util::{pin_mut, FutureExt, StreamExt}; +use gloo_timers::future::sleep; +use web_sys::Event; mod cache; mod cfg; mod dom; -#[cfg(any(feature = "hot-reload", debug_assertions))] -mod hot_reload; -#[cfg(feature = "hydrate")] -mod rehydrate; +// #[cfg(any(feature = "hot-reload", debug_assertions))] +// mod hot_reload; +// #[cfg(feature = "hydrate")] +// mod rehydrate; // mod ric_raf; mod util; @@ -92,7 +94,7 @@ mod util; /// render!(div {"hello world"}) /// } /// ``` -pub fn launch(root_component: Component) { +pub fn launch(root_component: fn(Scope) -> Element) { launch_with_props(root_component, (), Config::default()); } @@ -115,7 +117,7 @@ pub fn launch(root_component: Component) { /// }) /// } /// ``` -pub fn launch_cfg(root: Component, config: Config) { +pub fn launch_cfg(root: fn(Scope) -> Element, config: Config) { launch_with_props(root, (), config) } @@ -143,10 +145,11 @@ pub fn launch_cfg(root: Component, config: Config) { /// render!(div {"hello {cx.props.name}"}) /// } /// ``` -pub fn launch_with_props(root_component: Component, root_properties: T, config: Config) -where - T: Send + 'static, -{ +pub fn launch_with_props( + root_component: fn(Scope) -> Element, + root_properties: T, + config: Config, +) { wasm_bindgen_futures::spawn_local(run_with_props(root_component, root_properties, config)); } @@ -162,7 +165,9 @@ where /// wasm_bindgen_futures::spawn_local(app_fut); /// } /// ``` -pub async fn run_with_props(root: Component, root_props: T, cfg: Config) { +pub async fn run_with_props(root: fn(Scope) -> Element, root_props: T, cfg: Config) { + log::info!("Starting up"); + let mut dom = VirtualDom::new_with_props(root, root_props); #[cfg(feature = "panic_hook")] @@ -170,8 +175,8 @@ pub async fn run_with_props(root: Component, root_props: T console_error_panic_hook::set_once(); } - #[cfg(any(feature = "hot-reload", debug_assertions))] - hot_reload::init(&dom); + // #[cfg(any(feature = "hot-reload", debug_assertions))] + // hot_reload::init(&dom); for s in crate::cache::BUILTIN_INTERNED_STRINGS { wasm_bindgen::intern(s); @@ -180,50 +185,48 @@ pub async fn run_with_props(root: Component, root_props: T wasm_bindgen::intern(s); } - let tasks = dom.get_scheduler_channel(); + // a let should_hydrate = cfg.hydrate; - let sender_callback: Rc = - Rc::new(move |event| tasks.unbounded_send(event).unwrap()); + let (tx, mut rx) = futures_channel::mpsc::unbounded(); - let should_hydrate = cfg.hydrate; + let mut websys_dom = dom::WebsysDom::new(cfg, tx); - let mut websys_dom = dom::WebsysDom::new(cfg, sender_callback); + log::info!("rebuilding app"); - log::trace!("rebuilding app"); - - if should_hydrate { - // todo: we need to split rebuild and initialize into two phases - // it's a waste to produce edits just to get the vdom loaded - let _ = dom.rebuild(); - - #[cfg(feature = "hydrate")] - #[allow(unused_variables)] - if let Err(err) = websys_dom.rehydrate(&dom) { - log::error!( - "Rehydration failed {:?}. Rebuild DOM into element from scratch", - &err - ); - - websys_dom.root.set_text_content(None); - - // errrrr we should split rebuild into two phases - // one that initializes things and one that produces edits - let edits = dom.rebuild(); - - websys_dom.apply_edits(edits.edits); - } - } else { - let edits = dom.rebuild(); - websys_dom.apply_edits(edits.edits); - } - - // let mut work_loop = ric_raf::RafLoop::new(); + let edits = dom.rebuild(); + websys_dom.apply_edits(edits.template_mutations); + websys_dom.apply_edits(edits.edits); loop { log::trace!("waiting for work"); // if virtualdom has nothing, wait for it to have something before requesting idle time // if there is work then this future resolves immediately. - dom.wait_for_work().await; + + let mut res = { + let work = dom.wait_for_work().fuse(); + pin_mut!(work); + + futures_util::select! { + _ = work => None, + evt = rx.next() => evt + } + }; + + while let Some(evt) = res { + let name = evt.type_(); + let element = walk_event_for_id(&evt); + let bubbles = dioxus_html::event_bubbles(name.as_str()); + + if let Some((element, target)) = element { + let data = virtual_event_from_websys_event(evt, target); + dom.handle_event(name.as_str(), data, element, bubbles); + } + res = rx.try_next().transpose().unwrap().ok(); + } + + let deadline = sleep(Duration::from_millis(50)); + + let edits = dom.render_with_deadline(deadline).await; log::trace!("working.."); @@ -232,14 +235,85 @@ pub async fn run_with_props(root: Component, root_props: T // run the virtualdom work phase until the frame deadline is reached // let mutations = dom.work_with_deadline(|| (&mut deadline).now_or_never().is_some()); - let mutations = dom.work_with_deadline(|| false); // wait for the animation frame to fire so we can apply our changes // work_loop.wait_for_raf().await; - for edit in mutations { - // actually apply our changes during the animation frame - websys_dom.apply_edits(edit.edits); + // for edit in mutations { + // // actually apply our changes during the animation frame + websys_dom.apply_edits(edits.template_mutations); + websys_dom.apply_edits(edits.edits); + // } + } +} + +fn walk_event_for_id(event: &Event) -> Option<(ElementId, web_sys::Element)> { + use wasm_bindgen::{closure::Closure, JsCast, JsValue}; + use web_sys::{Document, Element, Event, HtmlElement}; + + let mut target = event + .target() + .expect("missing target") + .dyn_into::() + .expect("not a valid element"); + + // break Ok(UserEvent { + // name: event_name_from_typ(&typ), + // data: virtual_event_from_websys_event(event.clone(), target.clone()), + // element: Some(ElementId(id)), + // scope_id: None, + // priority: dioxus_core::EventPriority::Medium, + // bubbles: event.bubbles(), + // }); + + // break Ok(UserEvent { + // name: event_name_from_typ(&typ), + // data: virtual_event_from_websys_event(event.clone(), target.clone()), + // element: None, + // scope_id: None, + // priority: dioxus_core::EventPriority::Low, + // bubbles: event.bubbles(), + // }); + + loop { + match target.get_attribute("data-dioxus-id").map(|f| f.parse()) { + Some(Ok(id)) => return Some((ElementId(id), target)), + Some(Err(_)) => return None, + + // walk the tree upwards until we actually find an event target + None => match target.parent_element() { + Some(parent) => target = parent, + None => return None, + }, } } } + +// if should_hydrate { +// // todo: we need to split rebuild and initialize into two phases +// // it's a waste to produce edits just to get the vdom loaded +// let _ = dom.rebuild(); + +// #[cfg(feature = "hydrate")] +// #[allow(unused_variables)] +// if let Err(err) = websys_dom.rehydrate(&dom) { +// log::error!( +// "Rehydration failed {:?}. Rebuild DOM into element from scratch", +// &err +// ); + +// websys_dom.root.set_text_content(None); + +// // errrrr we should split rebuild into two phases +// // one that initializes things and one that produces edits +// let edits = dom.rebuild(); + +// websys_dom.apply_edits(edits.edits); +// } +// } else { +// let edits = dom.rebuild(); +// websys_dom.apply_edits(edits.template_mutations); +// websys_dom.apply_edits(edits.edits); +// } + +// let mut work_loop = ric_raf::RafLoop::new(); diff --git a/packages/web/src/olddom.rs b/packages/web/src/olddom.rs deleted file mode 100644 index e07d7478f..000000000 --- a/packages/web/src/olddom.rs +++ /dev/null @@ -1,856 +0,0 @@ -//! Implementation of a renderer for Dioxus on the web. -//! -//! Oustanding todos: -//! - Removing event listeners (delegation) -//! - Passive event listeners -//! - no-op event listener patch for safari -//! - tests to ensure dyn_into works for various event types. -//! - Partial delegation?> - -use dioxus_core::{DomEdit, ElementId, SchedulerMsg, ScopeId, UserEvent}; -use rustc_hash::FxHashMap; -use std::{any::Any, fmt::Debug, rc::Rc, sync::Arc}; -use wasm_bindgen::{closure::Closure, JsCast}; -use web_sys::{ - CssStyleDeclaration, Document, Element, Event, HtmlElement, HtmlInputElement, - HtmlOptionElement, HtmlTextAreaElement, Node, -}; - -use crate::{nodeslab::NodeSlab, Config}; - -pub struct WebsysDom { - stack: Stack, - - /// A map from ElementID (index) to Node - pub(crate) nodes: NodeSlab, - - document: Document, - - pub(crate) root: Element, - - sender_callback: Rc, - - // map of listener types to number of those listeners - // This is roughly a delegater - // TODO: check how infero delegates its events - some are more performant - listeners: FxHashMap<&'static str, ListenerEntry>, -} - -type ListenerEntry = (usize, Closure); - -impl WebsysDom { - pub fn new(cfg: Config, sender_callback: Rc) -> Self { - let document = load_document(); - - let nodes = NodeSlab::new(2000); - let listeners = FxHashMap::default(); - - let mut stack = Stack::with_capacity(10); - - let root = load_document().get_element_by_id(&cfg.rootname).unwrap(); - let root_node = root.clone().dyn_into::().unwrap(); - stack.push(root_node); - - Self { - stack, - nodes, - listeners, - document, - sender_callback, - root, - } - } - - pub fn apply_edits(&mut self, mut edits: Vec) { - for edit in edits.drain(..) { - match edit { - DomEdit::PushRoot { root } => self.push(root), - DomEdit::AppendChildren { many } => self.append_children(many), - DomEdit::ReplaceWith { m, root } => self.replace_with(m, root), - DomEdit::Remove { root } => self.remove(root), - DomEdit::CreateTextNode { text, root: id } => self.create_text_node(text, id), - DomEdit::CreateElement { tag, root: id } => self.create_element(tag, None, id), - DomEdit::CreateElementNs { tag, root: id, ns } => { - self.create_element(tag, Some(ns), id) - } - DomEdit::CreatePlaceholder { root: id } => self.create_placeholder(id), - DomEdit::NewEventListener { - event_name, - scope, - root: mounted_node_id, - } => self.new_event_listener(event_name, scope, mounted_node_id), - - DomEdit::RemoveEventListener { event, root } => { - self.remove_event_listener(event, root) - } - - DomEdit::SetText { text, root } => self.set_text(text, root), - DomEdit::SetAttribute { - field, - value, - ns, - root, - } => self.set_attribute(field, value, ns, root), - DomEdit::RemoveAttribute { name, root } => self.remove_attribute(name, root), - - DomEdit::InsertAfter { n, root } => self.insert_after(n, root), - DomEdit::InsertBefore { n, root } => self.insert_before(n, root), - } - } - } - fn push(&mut self, root: u64) { - let key = root as usize; - let domnode = &self.nodes[key]; - - let real_node: Node = match domnode { - Some(n) => n.clone(), - None => todo!(), - }; - - self.stack.push(real_node); - } - - fn append_children(&mut self, many: u32) { - let root: Node = self - .stack - .list - .get(self.stack.list.len() - (1 + many as usize)) - .unwrap() - .clone(); - - // We need to make sure to add comments between text nodes - // We ensure that the text siblings are patched by preventing the browser from merging - // neighboring text nodes. Originally inspired by some of React's work from 2016. - // -> https://reactjs.org/blog/2016/04/07/react-v15.html#major-changes - // -> https://github.com/facebook/react/pull/5753 - /* - todo: we need to track this for replacing/insert after/etc - */ - let mut last_node_was_text = false; - - for child in self - .stack - .list - .drain((self.stack.list.len() - many as usize)..) - { - if child.dyn_ref::().is_some() { - if last_node_was_text { - let comment_node = self - .document - .create_comment("dioxus") - .dyn_into::() - .unwrap(); - root.append_child(&comment_node).unwrap(); - } - last_node_was_text = true; - } else { - last_node_was_text = false; - } - root.append_child(&child).unwrap(); - } - } - - fn replace_with(&mut self, m: u32, root: u64) { - let old = self.nodes[root as usize].as_ref().unwrap(); - - let arr: js_sys::Array = self - .stack - .list - .drain((self.stack.list.len() - m as usize)..) - .collect(); - - if let Some(el) = old.dyn_ref::() { - el.replace_with_with_node(&arr).unwrap(); - } else if let Some(el) = old.dyn_ref::() { - el.replace_with_with_node(&arr).unwrap(); - } else if let Some(el) = old.dyn_ref::() { - el.replace_with_with_node(&arr).unwrap(); - } - } - - fn remove(&mut self, root: u64) { - let node = self.nodes[root as usize].as_ref().unwrap(); - if let Some(element) = node.dyn_ref::() { - element.remove(); - } else { - if let Some(parent) = node.parent_node() { - parent.remove_child(&node).unwrap(); - } - } - } - - fn create_placeholder(&mut self, id: u64) { - self.create_element("pre", None, id); - self.set_attribute("hidden", "", None, id); - } - - fn create_text_node(&mut self, text: &str, id: u64) { - let textnode = self - .document - .create_text_node(text) - .dyn_into::() - .unwrap(); - - self.stack.push(textnode.clone()); - - self.nodes[(id as usize)] = Some(textnode); - } - - fn create_element(&mut self, tag: &str, ns: Option<&'static str>, id: u64) { - let tag = wasm_bindgen::intern(tag); - - let el = match ns { - Some(ns) => self - .document - .create_element_ns(Some(ns), tag) - .unwrap() - .dyn_into::() - .unwrap(), - None => self - .document - .create_element(tag) - .unwrap() - .dyn_into::() - .unwrap(), - }; - - use smallstr::SmallString; - use std::fmt::Write; - - let mut s: SmallString<[u8; 8]> = smallstr::SmallString::new(); - write!(s, "{}", id).unwrap(); - - let el2 = el.dyn_ref::().unwrap(); - el2.set_attribute("dioxus-id", s.as_str()).unwrap(); - - self.stack.push(el.clone()); - self.nodes[(id as usize)] = Some(el); - } - - fn new_event_listener(&mut self, event: &'static str, _scope: ScopeId, _real_id: u64) { - let event = wasm_bindgen::intern(event); - - // attach the correct attributes to the element - // these will be used by accessing the event's target - // This ensures we only ever have one handler attached to the root, but decide - // dynamically when we want to call a listener. - - let el = self.stack.top(); - - let el = el.dyn_ref::().unwrap(); - - el.set_attribute("dioxus-event", event).unwrap(); - - // Register the callback to decode - - if let Some(entry) = self.listeners.get_mut(event) { - entry.0 += 1; - } else { - let trigger = self.sender_callback.clone(); - - let c: Box = Box::new(move |event: &web_sys::Event| { - // "Result" cannot be received from JS - // Instead, we just build and immediately execute a closure that returns result - match decode_trigger(event) { - Ok(synthetic_event) => { - let target = event.target().unwrap(); - if let Some(node) = target.dyn_ref::() { - if let Some(name) = node.get_attribute("dioxus-prevent-default") { - if name == synthetic_event.name - || name.trim_start_matches("on") == synthetic_event.name - { - log::trace!("Preventing default"); - event.prevent_default(); - } - } - } - - trigger.as_ref()(SchedulerMsg::Event(synthetic_event)) - } - Err(e) => log::error!("Error decoding Dioxus event attribute. {:#?}", e), - }; - }); - - let handler = Closure::wrap(c); - - self.root - .add_event_listener_with_callback(event, (&handler).as_ref().unchecked_ref()) - .unwrap(); - - // Increment the listeners - self.listeners.insert(event.into(), (1, handler)); - } - } - - fn remove_event_listener(&mut self, _event: &str, _root: u64) { - todo!() - } - - fn set_text(&mut self, text: &str, root: u64) { - let el = self.nodes[root as usize].as_ref().unwrap(); - el.set_text_content(Some(text)) - } - - fn set_attribute(&mut self, name: &str, value: &str, ns: Option<&str>, root: u64) { - let node = self.nodes[root as usize].as_ref().unwrap(); - if ns == Some("style") { - if let Some(el) = node.dyn_ref::() { - let el = el.dyn_ref::().unwrap(); - let style_dc: CssStyleDeclaration = el.style(); - style_dc.set_property(name, value).unwrap(); - } - } else { - let fallback = || { - let el = node.dyn_ref::().unwrap(); - el.set_attribute(name, value).unwrap() - }; - match name { - "dangerous_inner_html" => { - if let Some(el) = node.dyn_ref::() { - el.set_inner_html(value); - } - } - "value" => { - if let Some(input) = node.dyn_ref::() { - /* - if the attribute being set is the same as the value of the input, then don't bother setting it. - This is used in controlled components to keep the cursor in the right spot. - - this logic should be moved into the virtualdom since we have the notion of "volatile" - */ - if input.value() != value { - input.set_value(value); - } - } else if let Some(node) = node.dyn_ref::() { - if name == "value" { - node.set_value(value); - } - } else { - fallback(); - } - } - "checked" => { - if let Some(input) = node.dyn_ref::() { - match value { - "true" => input.set_checked(true), - "false" => input.set_checked(false), - _ => fallback(), - } - } else { - fallback(); - } - } - "selected" => { - if let Some(node) = node.dyn_ref::() { - node.set_selected(true); - } else { - fallback(); - } - } - _ => { - // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 - if value == "false" { - if let Some(el) = node.dyn_ref::() { - match name { - "allowfullscreen" - | "allowpaymentrequest" - | "async" - | "autofocus" - | "autoplay" - | "checked" - | "controls" - | "default" - | "defer" - | "disabled" - | "formnovalidate" - | "hidden" - | "ismap" - | "itemscope" - | "loop" - | "multiple" - | "muted" - | "nomodule" - | "novalidate" - | "open" - | "playsinline" - | "readonly" - | "required" - | "reversed" - | "selected" - | "truespeed" => { - let _ = el.remove_attribute(name); - } - _ => { - let _ = el.set_attribute(name, value); - } - }; - } - } else { - fallback(); - } - } - } - } - } - - fn remove_attribute(&mut self, name: &str, root: u64) { - let node = self.nodes[root as usize].as_ref().unwrap(); - if let Some(node) = node.dyn_ref::() { - node.remove_attribute(name).unwrap(); - } - if let Some(node) = node.dyn_ref::() { - // Some attributes are "volatile" and don't work through `removeAttribute`. - if name == "value" { - node.set_value(""); - } - if name == "checked" { - node.set_checked(false); - } - } - - if let Some(node) = node.dyn_ref::() { - if name == "selected" { - node.set_selected(true); - } - } - } - - fn insert_after(&mut self, n: u32, root: u64) { - let old = self.nodes[root as usize].as_ref().unwrap(); - - let arr: js_sys::Array = self - .stack - .list - .drain((self.stack.list.len() - n as usize)..) - .collect(); - - if let Some(el) = old.dyn_ref::() { - el.after_with_node(&arr).unwrap(); - } else if let Some(el) = old.dyn_ref::() { - el.after_with_node(&arr).unwrap(); - } else if let Some(el) = old.dyn_ref::() { - el.after_with_node(&arr).unwrap(); - } - } - - fn insert_before(&mut self, n: u32, root: u64) { - let anchor = self.nodes[root as usize].as_ref().unwrap(); - - if n == 1 { - let before = self.stack.pop(); - - anchor - .parent_node() - .unwrap() - .insert_before(&before, Some(&anchor)) - .unwrap(); - } else { - let arr: js_sys::Array = self - .stack - .list - .drain((self.stack.list.len() - n as usize)..) - .collect(); - - if let Some(el) = anchor.dyn_ref::() { - el.before_with_node(&arr).unwrap(); - } else if let Some(el) = anchor.dyn_ref::() { - el.before_with_node(&arr).unwrap(); - } else if let Some(el) = anchor.dyn_ref::() { - el.before_with_node(&arr).unwrap(); - } - } - } -} - -#[derive(Debug, Default)] -struct Stack { - list: Vec, -} - -impl Stack { - #[inline] - fn with_capacity(cap: usize) -> Self { - Stack { - list: Vec::with_capacity(cap), - } - } - - #[inline] - fn push(&mut self, node: Node) { - self.list.push(node); - } - - #[inline] - fn pop(&mut self) -> Node { - self.list.pop().unwrap() - } - - fn top(&self) -> &Node { - match self.list.last() { - Some(a) => a, - None => panic!("Called 'top' of an empty stack, make sure to push the root first"), - } - } -} - -pub struct DioxusWebsysEvent(web_sys::Event); - -// safety: currently the web is not multithreaded and our VirtualDom exists on the same thread -unsafe impl Send for DioxusWebsysEvent {} -unsafe impl Sync for DioxusWebsysEvent {} - -// todo: some of these events are being casted to the wrong event type. -// We need tests that simulate clicks/etc and make sure every event type works. -fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc { - use dioxus_html::on::*; - use dioxus_html::KeyCode; - - match event.type_().as_str() { - "copy" | "cut" | "paste" => Arc::new(ClipboardData {}), - "compositionend" | "compositionstart" | "compositionupdate" => { - let evt: &web_sys::CompositionEvent = event.dyn_ref().unwrap(); - Arc::new(CompositionData { - data: evt.data().unwrap_or_default(), - }) - } - "keydown" | "keypress" | "keyup" => { - let evt: &web_sys::KeyboardEvent = event.dyn_ref().unwrap(); - Arc::new(KeyboardData { - alt_key: evt.alt_key(), - char_code: evt.char_code(), - key: evt.key(), - key_code: KeyCode::from_raw_code(evt.key_code() as u8), - ctrl_key: evt.ctrl_key(), - location: evt.location() as usize, - meta_key: evt.meta_key(), - repeat: evt.repeat(), - shift_key: evt.shift_key(), - which: evt.which() as usize, - }) - } - "focus" | "blur" => Arc::new(FocusData {}), - - // todo: these handlers might get really slow if the input box gets large and allocation pressure is heavy - // don't have a good solution with the serialized event problem - "change" | "input" | "invalid" | "reset" | "submit" => { - let evt: &web_sys::Event = event.dyn_ref().unwrap(); - - let target: web_sys::EventTarget = evt.target().unwrap(); - let value: String = (&target) - .dyn_ref() - .map(|input: &web_sys::HtmlInputElement| { - // todo: special case more input types - match input.type_().as_str() { - "checkbox" => { - match input.checked() { - true => "true".to_string(), - false => "false".to_string(), - } - }, - _ => { - input.value() - } - } - }) - .or_else(|| { - target - .dyn_ref() - .map(|input: &web_sys::HtmlTextAreaElement| input.value()) - }) - // select elements are NOT input events - because - why woudn't they be?? - .or_else(|| { - target - .dyn_ref() - .map(|input: &web_sys::HtmlSelectElement| input.value()) - }) - .or_else(|| { - target - .dyn_ref::() - .unwrap() - .text_content() - }) - .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener"); - - Arc::new(FormData { value }) - } - "click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit" - | "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter" - | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => { - let evt: &web_sys::MouseEvent = event.dyn_ref().unwrap(); - Arc::new(MouseData { - alt_key: evt.alt_key(), - button: evt.button(), - buttons: evt.buttons(), - client_x: evt.client_x(), - client_y: evt.client_y(), - ctrl_key: evt.ctrl_key(), - meta_key: evt.meta_key(), - screen_x: evt.screen_x(), - screen_y: evt.screen_y(), - shift_key: evt.shift_key(), - page_x: evt.page_x(), - page_y: evt.page_y(), - }) - } - "pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture" - | "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => { - let evt: &web_sys::PointerEvent = event.dyn_ref().unwrap(); - Arc::new(PointerData { - alt_key: evt.alt_key(), - button: evt.button(), - buttons: evt.buttons(), - client_x: evt.client_x(), - client_y: evt.client_y(), - ctrl_key: evt.ctrl_key(), - meta_key: evt.meta_key(), - page_x: evt.page_x(), - page_y: evt.page_y(), - screen_x: evt.screen_x(), - screen_y: evt.screen_y(), - shift_key: evt.shift_key(), - pointer_id: evt.pointer_id(), - width: evt.width(), - height: evt.height(), - pressure: evt.pressure(), - tangential_pressure: evt.tangential_pressure(), - tilt_x: evt.tilt_x(), - tilt_y: evt.tilt_y(), - twist: evt.twist(), - pointer_type: evt.pointer_type(), - is_primary: evt.is_primary(), - // get_modifier_state: evt.get_modifier_state(), - }) - } - "select" => Arc::new(SelectionData {}), - "touchcancel" | "touchend" | "touchmove" | "touchstart" => { - let evt: &web_sys::TouchEvent = event.dyn_ref().unwrap(); - Arc::new(TouchData { - alt_key: evt.alt_key(), - ctrl_key: evt.ctrl_key(), - meta_key: evt.meta_key(), - shift_key: evt.shift_key(), - }) - } - "scroll" => Arc::new(()), - "wheel" => { - let evt: &web_sys::WheelEvent = event.dyn_ref().unwrap(); - Arc::new(WheelData { - delta_x: evt.delta_x(), - delta_y: evt.delta_y(), - delta_z: evt.delta_z(), - delta_mode: evt.delta_mode(), - }) - } - "animationstart" | "animationend" | "animationiteration" => { - let evt: &web_sys::AnimationEvent = event.dyn_ref().unwrap(); - Arc::new(AnimationData { - elapsed_time: evt.elapsed_time(), - animation_name: evt.animation_name(), - pseudo_element: evt.pseudo_element(), - }) - } - "transitionend" => { - let evt: &web_sys::TransitionEvent = event.dyn_ref().unwrap(); - Arc::new(TransitionData { - elapsed_time: evt.elapsed_time(), - property_name: evt.property_name(), - pseudo_element: evt.pseudo_element(), - }) - } - "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted" - | "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play" - | "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend" - | "timeupdate" | "volumechange" | "waiting" => Arc::new(MediaData {}), - "toggle" => Arc::new(ToggleData {}), - _ => Arc::new(()), - } -} - -/// This function decodes a websys event and produces an EventTrigger -/// With the websys implementation, we attach a unique key to the nodes -fn decode_trigger(event: &web_sys::Event) -> anyhow::Result { - use anyhow::Context; - - let target = event - .target() - .expect("missing target") - .dyn_into::() - .expect("not a valid element"); - - let typ = event.type_(); - - let element_id = target - .get_attribute("dioxus-id") - .context("Could not find element id on event target")? - .parse()?; - - Ok(UserEvent { - name: event_name_from_typ(&typ), - data: virtual_event_from_websys_event(event.clone()), - element: Some(ElementId(element_id)), - scope_id: None, - priority: dioxus_core::EventPriority::Medium, - }) -} - -pub(crate) fn load_document() -> Document { - web_sys::window() - .expect("should have access to the Window") - .document() - .expect("should have access to the Document") -} - -fn event_name_from_typ(typ: &str) -> &'static str { - match typ { - "copy" => "copy", - "cut" => "cut", - "paste" => "paste", - "compositionend" => "compositionend", - "compositionstart" => "compositionstart", - "compositionupdate" => "compositionupdate", - "keydown" => "keydown", - "keypress" => "keypress", - "keyup" => "keyup", - "focus" => "focus", - "blur" => "blur", - "change" => "change", - "input" => "input", - "invalid" => "invalid", - "reset" => "reset", - "submit" => "submit", - "click" => "click", - "contextmenu" => "contextmenu", - "doubleclick" => "doubleclick", - "drag" => "drag", - "dragend" => "dragend", - "dragenter" => "dragenter", - "dragexit" => "dragexit", - "dragleave" => "dragleave", - "dragover" => "dragover", - "dragstart" => "dragstart", - "drop" => "drop", - "mousedown" => "mousedown", - "mouseenter" => "mouseenter", - "mouseleave" => "mouseleave", - "mousemove" => "mousemove", - "mouseout" => "mouseout", - "mouseover" => "mouseover", - "mouseup" => "mouseup", - "pointerdown" => "pointerdown", - "pointermove" => "pointermove", - "pointerup" => "pointerup", - "pointercancel" => "pointercancel", - "gotpointercapture" => "gotpointercapture", - "lostpointercapture" => "lostpointercapture", - "pointerenter" => "pointerenter", - "pointerleave" => "pointerleave", - "pointerover" => "pointerover", - "pointerout" => "pointerout", - "select" => "select", - "touchcancel" => "touchcancel", - "touchend" => "touchend", - "touchmove" => "touchmove", - "touchstart" => "touchstart", - "scroll" => "scroll", - "wheel" => "wheel", - "animationstart" => "animationstart", - "animationend" => "animationend", - "animationiteration" => "animationiteration", - "transitionend" => "transitionend", - "abort" => "abort", - "canplay" => "canplay", - "canplaythrough" => "canplaythrough", - "durationchange" => "durationchange", - "emptied" => "emptied", - "encrypted" => "encrypted", - "ended" => "ended", - "error" => "error", - "loadeddata" => "loadeddata", - "loadedmetadata" => "loadedmetadata", - "loadstart" => "loadstart", - "pause" => "pause", - "play" => "play", - "playing" => "playing", - "progress" => "progress", - "ratechange" => "ratechange", - "seeked" => "seeked", - "seeking" => "seeking", - "stalled" => "stalled", - "suspend" => "suspend", - "timeupdate" => "timeupdate", - "volumechange" => "volumechange", - "waiting" => "waiting", - "toggle" => "toggle", - _ => { - panic!("unsupported event type") - } - } -} - - -//! This module provides a mirror of the VirtualDOM Element Slab using a Vector. - -use std::ops::{Index, IndexMut}; -use web_sys::Node; - -pub(crate) struct NodeSlab { - nodes: Vec>, -} - -impl NodeSlab { - pub fn new(capacity: usize) -> NodeSlab { - let nodes = Vec::with_capacity(capacity); - NodeSlab { nodes } - } -} -impl Index for NodeSlab { - type Output = Option; - fn index(&self, index: usize) -> &Self::Output { - &self.nodes[index] - } -} - -impl IndexMut for NodeSlab { - fn index_mut(&mut self, index: usize) -> &mut Self::Output { - if index >= self.nodes.capacity() * 3 { - panic!("Trying to mutate an element way too far out of bounds"); - } - - if index + 1 > self.nodes.len() { - self.nodes.resize_with(index + 1, || None); - } - &mut self.nodes[index] - } -} - - -#[derive(Debug, Default)] -struct Stack { - list: Vec, -} - -impl Stack { - #[inline] - fn with_capacity(cap: usize) -> Self { - Stack { - list: Vec::with_capacity(cap), - } - } - - #[inline] - fn push(&mut self, node: Node) { - self.list.push(node); - } - - #[inline] - fn pop(&mut self) -> Node { - self.list.pop().unwrap() - } - - fn top(&self) -> &Node { - match self.list.last() { - Some(a) => a, - None => panic!("Called 'top' of an empty stack, make sure to push the root first"), - } - } -} diff --git a/packages/web/src/rehydrate.rs b/packages/web/src/rehydrate.rs index cab43bacd..bf327176d 100644 --- a/packages/web/src/rehydrate.rs +++ b/packages/web/src/rehydrate.rs @@ -1,6 +1,5 @@ use crate::dom::WebsysDom; use dioxus_core::{VNode, VirtualDom}; -use dioxus_html::event_bubbles; use wasm_bindgen::JsCast; use web_sys::{Comment, Element, Node, Text}; @@ -30,14 +29,15 @@ impl WebsysDom { let mut last_node_was_text = false; - // Recursively rehydrate the dom from the VirtualDom - self.rehydrate_single( - &mut nodes, - &mut counter, - dom, - root_node, - &mut last_node_was_text, - ) + todo!() + // // Recursively rehydrate the dom from the VirtualDom + // self.rehydrate_single( + // &mut nodes, + // &mut counter, + // dom, + // root_node, + // &mut last_node_was_text, + // ) } fn rehydrate_single( @@ -164,7 +164,8 @@ impl WebsysDom { VNode::Component(el) => { let scope = dom.get_scope(el.scope.get().unwrap()).unwrap(); let node = scope.root_node(); - self.rehydrate_single(nodes, place, dom, node, last_node_was_text)?; + todo!() + // self.rehydrate_single(nodes, place, dom, node, last_node_was_text)?; } VNode::TemplateRef(_) => todo!(), } diff --git a/packages/web/tests/hydrate.rs b/packages/web/tests/hydrate.rs index af6e49cc2..d0b5acb9c 100644 --- a/packages/web/tests/hydrate.rs +++ b/packages/web/tests/hydrate.rs @@ -50,7 +50,7 @@ fn rehydrates() { let mut dom = VirtualDom::new(app); let _ = dom.rebuild(); - let out = dioxus_ssr::render_vdom_cfg(&dom, |c| c.pre_render(true)); + let out = dioxus_ssr::render_vdom_cfg(&dom, Default::default()); window() .unwrap()