diff --git a/examples/eval.rs b/examples/eval.rs index c0dda3e09..22e036b9c 100644 --- a/examples/eval.rs +++ b/examples/eval.rs @@ -1,4 +1,5 @@ use dioxus::prelude::*; +use dioxus_desktop::EvalResult; fn main() { dioxus_desktop::launch(app); @@ -7,6 +8,15 @@ fn main() { fn app(cx: Scope) -> Element { let script = use_state(cx, String::new); let eval = dioxus_desktop::use_eval(cx); + let future: &UseRef> = use_ref(cx, || None); + if future.read().is_some() { + let future_clone = future.clone(); + cx.spawn(async move { + if let Some(fut) = future_clone.with_mut(|o| o.take()) { + println!("{:?}", fut.await) + } + }); + } cx.render(rsx! { div { @@ -16,7 +26,10 @@ fn app(cx: Scope) -> Element { oninput: move |e| script.set(e.value.clone()), } button { - onclick: move |_| eval(script.to_string()), + onclick: move |_| { + let fut = eval(script); + future.set(Some(fut)); + }, "Execute" } } diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index f779e6ba2..827968655 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -35,6 +35,8 @@ indexmap = "1.7" serde = { version = "1", features = ["derive"], optional = true } anyhow = "1.0.66" +smallbox = "0.8.1" + [dev-dependencies] tokio = { version = "*", features = ["full"] } dioxus = { path = "../dioxus" } diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index cc63a0e8b..35c2db411 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -36,6 +36,9 @@ impl<'b> VirtualDom { let cur_scope = self.scope_stack.last().copied().unwrap(); + // we know that this will generate at least one mutation per node + self.mutations.edits.reserve(template.template.roots.len()); + let mut on_stack = 0; for (root_idx, root) in template.template.roots.iter().enumerate() { // We might need to generate an ID for the root node @@ -208,19 +211,12 @@ impl<'b> VirtualDom { // If it's all dynamic nodes, then we don't need to register it // Quickly run through and see if it's all just dynamic nodes - let dynamic_roots = template - .template - .roots - .iter() - .filter(|root| { - matches!( - root, - TemplateNode::Dynamic { .. } | TemplateNode::DynamicText { .. } - ) - }) - .count(); - - if dynamic_roots == template.template.roots.len() { + if template.template.roots.iter().all(|root| { + matches!( + root, + TemplateNode::Dynamic { .. } | TemplateNode::DynamicText { .. } + ) + }) { return; } @@ -291,7 +287,7 @@ impl<'b> VirtualDom { } pub(crate) fn create_fragment(&mut self, nodes: &'b [VNode<'b>]) -> usize { - nodes.iter().fold(0, |acc, child| acc + self.create(child)) + nodes.iter().map(|child| self.create(child)).sum() } pub(super) fn create_component_node( @@ -302,7 +298,7 @@ impl<'b> VirtualDom { ) -> usize { let props = component .props - .replace(None) + .take() .expect("Props to always exist when a component is being created"); let unbounded_props = unsafe { std::mem::transmute(props) }; diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 0384192e2..8abf8b4ee 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -56,7 +56,9 @@ impl<'b> VirtualDom { fn diff_err_to_ok(&mut self, _e: &anyhow::Error, _l: &'b VNode<'b>) {} fn diff_node(&mut self, left_template: &'b VNode<'b>, right_template: &'b VNode<'b>) { - if left_template.template.name != right_template.template.name { + if !std::ptr::eq(left_template.template.name, right_template.template.name) + && left_template.template.name != right_template.template.name + { return self.light_diff_templates(left_template, right_template); } diff --git a/packages/core/src/lazynodes.rs b/packages/core/src/lazynodes.rs index 020b307d9..594672707 100644 --- a/packages/core/src/lazynodes.rs +++ b/packages/core/src/lazynodes.rs @@ -11,8 +11,9 @@ //! The logic for this was borrowed from . Unfortunately, this crate does not //! support non-static closures, so we've implemented the core logic of `ValueA` in this module. +use smallbox::{smallbox, space::S16, SmallBox}; + use crate::{innerlude::VNode, ScopeState}; -use std::mem; /// A concrete type provider for closures that build [`VNode`] structures. /// @@ -24,14 +25,7 @@ use std::mem; /// LazyNodes::new(|f| f.element("div", [], [], [] None)) /// ``` pub struct LazyNodes<'a, 'b> { - inner: StackNodeStorage<'a, 'b>, -} - -type StackHeapSize = [usize; 16]; - -enum StackNodeStorage<'a, 'b> { - Stack(LazyStack), - Heap(Box) -> Option> + 'b>), + inner: SmallBox VNode<'a> + 'b, S16>, } impl<'a, 'b> LazyNodes<'a, 'b> { @@ -44,114 +38,11 @@ impl<'a, 'b> LazyNodes<'a, 'b> { // there's no way to call FnOnce without a box, so we need to store it in a slot and use static dispatch let mut slot = Some(val); - let val = move |fac: Option<&'a ScopeState>| { - fac.map( - slot.take() - .expect("LazyNodes closure to be called only once"), - ) - }; - - // miri does not know how to work with mucking directly into bytes - // just use a heap allocated type when miri is running - if cfg!(miri) { - Self { - inner: StackNodeStorage::Heap(Box::new(val)), - } - } else { - unsafe { LazyNodes::new_inner(val) } - } - } - - /// Create a new [`LazyNodes`] closure, but force it onto the heap. - pub fn new_boxed(inner: F) -> Self - where - F: FnOnce(&'a ScopeState) -> VNode<'a> + 'b, - { - // there's no way to call FnOnce without a box, so we need to store it in a slot and use static dispatch - let mut slot = Some(inner); - Self { - inner: StackNodeStorage::Heap(Box::new(move |fac: Option<&'a ScopeState>| { - fac.map( - slot.take() - .expect("LazyNodes closure to be called only once"), - ) - })), - } - } - - unsafe fn new_inner(val: F) -> Self - where - F: FnMut(Option<&'a ScopeState>) -> Option> + 'b, - { - let mut ptr: *const _ = &val as &dyn FnMut(Option<&'a ScopeState>) -> Option>; - - assert_eq!( - ptr as *const u8, &val as *const _ as *const u8, - "MISUSE: Closure returned different pointer" - ); - assert_eq!( - std::mem::size_of_val(&*ptr), - std::mem::size_of::(), - "MISUSE: Closure returned a subset pointer" - ); - - let words = ptr_as_slice(&mut ptr); - assert!( - words[0] == &val as *const _ as usize, - "BUG: Pointer layout is not (data_ptr, info...)" - ); - - // - Ensure that Self is aligned same as data requires - assert!( - std::mem::align_of::() <= std::mem::align_of::(), - "TODO: Enforce alignment >{} (requires {})", - std::mem::align_of::(), - std::mem::align_of::() - ); - - let info = &words[1..]; - let data = words[0] as *mut (); - let size = mem::size_of::(); - - let stored_size = info.len() * mem::size_of::() + size; - let max_size = mem::size_of::(); - - if stored_size > max_size { - Self { - inner: StackNodeStorage::Heap(Box::new(val)), - } - } else { - let mut buf: StackHeapSize = StackHeapSize::default(); - - assert!(info.len() + round_to_words(size) <= buf.as_ref().len()); - - // Place pointer information at the end of the region - // - Allows the data to be at the start for alignment purposes - { - let info_ofs = buf.as_ref().len() - info.len(); - let info_dst = &mut buf.as_mut()[info_ofs..]; - for (d, v) in Iterator::zip(info_dst.iter_mut(), info.iter()) { - *d = *v; - } - } - - let src_ptr = data as *const u8; - let dataptr = buf.as_mut_ptr().cast::(); - - for i in 0..size { - *dataptr.add(i) = *src_ptr.add(i); - } - - std::mem::forget(val); - - Self { - inner: StackNodeStorage::Stack(LazyStack { - _align: [], - buf, - dropped: false, - }), - } + inner: smallbox!(move |f| { + let val = slot.take().expect("cannot call LazyNodes twice"); + val(f) + }), } } @@ -163,88 +54,10 @@ impl<'a, 'b> LazyNodes<'a, 'b> { /// let node = f.call(cac); /// ``` #[must_use] - pub fn call(self, f: &'a ScopeState) -> VNode<'a> { - match self.inner { - StackNodeStorage::Heap(mut lazy) => { - lazy(Some(f)).expect("Closure should not be called twice") - } - StackNodeStorage::Stack(mut stack) => stack.call(f), + pub fn call(mut self, f: &'a ScopeState) -> VNode<'a> { + if self.inner.is_heap() { + panic!(); } + (self.inner)(f) } } - -struct LazyStack { - _align: [u64; 0], - buf: StackHeapSize, - dropped: bool, -} - -impl LazyStack { - fn call<'a>(&mut self, f: &'a ScopeState) -> VNode<'a> { - let LazyStack { buf, .. } = self; - let data = buf.as_ref(); - - let info_size = - mem::size_of::<*mut dyn FnMut(Option<&'a ScopeState>) -> Option>>() - / mem::size_of::() - - 1; - - let info_ofs = data.len() - info_size; - - let g: *mut dyn FnMut(Option<&'a ScopeState>) -> Option> = - unsafe { make_fat_ptr(data[..].as_ptr() as usize, &data[info_ofs..]) }; - - self.dropped = true; - - let clos = unsafe { &mut *g }; - clos(Some(f)).unwrap() - } -} -impl Drop for LazyStack { - fn drop(&mut self) { - if !self.dropped { - let LazyStack { buf, .. } = self; - let data = buf.as_ref(); - - let info_size = - mem::size_of::<*mut dyn FnMut(Option<&ScopeState>) -> Option>>() - / mem::size_of::() - - 1; - - let info_ofs = data.len() - info_size; - - let g: *mut dyn FnMut(Option<&ScopeState>) -> Option> = - unsafe { make_fat_ptr(data[..].as_ptr() as usize, &data[info_ofs..]) }; - - self.dropped = true; - - let clos = unsafe { &mut *g }; - clos(None); - } - } -} - -/// Obtain mutable access to a pointer's words -fn ptr_as_slice(ptr: &mut T) -> &mut [usize] { - assert!(mem::size_of::() % mem::size_of::() == 0); - let words = mem::size_of::() / mem::size_of::(); - // SAFE: Points to valid memory (a raw pointer) - unsafe { core::slice::from_raw_parts_mut(ptr as *mut _ as *mut usize, words) } -} - -/// Re-construct a fat pointer -unsafe fn make_fat_ptr(data_ptr: usize, meta_vals: &[usize]) -> *mut T { - let mut rv = mem::MaybeUninit::<*mut T>::uninit(); - { - let s = ptr_as_slice(&mut rv); - s[0] = data_ptr; - s[1..].copy_from_slice(meta_vals); - } - let rv = rv.assume_init(); - assert_eq!(rv as *const (), data_ptr as *const ()); - rv -} - -fn round_to_words(len: usize) -> usize { - (len + mem::size_of::() - 1) / mem::size_of::() -} diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index af36f1aa6..db328fd93 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -8,6 +8,7 @@ use crate::{ scopes::{ScopeId, ScopeState}, virtual_dom::VirtualDom, }; +use bumpalo::Bump; use futures_util::FutureExt; use std::{ mem, @@ -34,8 +35,8 @@ impl VirtualDom { props: Some(props), name, placeholder: Default::default(), - node_arena_1: BumpFrame::new(50), - node_arena_2: BumpFrame::new(50), + node_arena_1: BumpFrame::new(0), + node_arena_2: BumpFrame::new(0), spawned_tasks: Default::default(), render_cnt: Default::default(), hook_arena: Default::default(), @@ -62,7 +63,13 @@ impl VirtualDom { let mut new_nodes = unsafe { let scope = self.scopes[scope_id.0].as_mut(); - scope.previous_frame_mut().bump.reset(); + // if this frame hasn't been intialized yet, we can guess the size of the next frame to be more efficient + if scope.previous_frame().bump.allocated_bytes() == 0 { + scope.previous_frame_mut().bump = + Bump::with_capacity(scope.current_frame().bump.allocated_bytes()); + } else { + scope.previous_frame_mut().bump.reset(); + } // Make sure to reset the hook counter so we give out hooks in the right order scope.hook_idx.set(0); @@ -128,8 +135,8 @@ impl VirtualDom { let frame = scope.previous_frame(); // set the new head of the bump frame - let alloced = &*frame.bump.alloc(new_nodes); - frame.node.set(alloced); + let allocated = &*frame.bump.alloc(new_nodes); + frame.node.set(allocated); // And move the render generation forward by one scope.render_cnt.set(scope.render_cnt.get() + 1); @@ -141,6 +148,6 @@ impl VirtualDom { }); // rebind the lifetime now that its stored internally - unsafe { mem::transmute(alloced) } + unsafe { mem::transmute(allocated) } } } diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index b1bbb42fa..c46bbd4f0 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -10,10 +10,10 @@ use crate::{ Attribute, AttributeValue, Element, Event, Properties, TaskId, }; use bumpalo::{boxed::Box as BumpBox, Bump}; +use fxhash::{FxHashMap, FxHashSet}; use std::{ any::{Any, TypeId}, cell::{Cell, RefCell}, - collections::{HashMap, HashSet}, fmt::Arguments, future::Future, rc::Rc, @@ -82,10 +82,10 @@ pub struct ScopeState { pub(crate) hook_list: RefCell>, pub(crate) hook_idx: Cell, - pub(crate) shared_contexts: RefCell>>, + pub(crate) shared_contexts: RefCell>>, pub(crate) tasks: Rc, - pub(crate) spawned_tasks: HashSet, + pub(crate) spawned_tasks: FxHashSet, 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 ced92acb0..7a7c516f6 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -14,15 +14,9 @@ use crate::{ AttributeValue, Element, Event, Scope, SuspenseContext, }; use futures_util::{pin_mut, StreamExt}; +use fxhash::FxHashMap; use slab::Slab; -use std::{ - any::Any, - borrow::BorrowMut, - cell::Cell, - collections::{BTreeSet, HashMap}, - future::Future, - rc::Rc, -}; +use std::{any::Any, borrow::BorrowMut, cell::Cell, collections::BTreeSet, future::Future, rc::Rc}; /// A virtual node system that progresses user events and diffs UI trees. /// @@ -148,7 +142,7 @@ use std::{ /// } /// ``` pub struct VirtualDom { - pub(crate) templates: HashMap>, + pub(crate) templates: FxHashMap>, pub(crate) scopes: Slab>, pub(crate) dirty_scopes: BTreeSet, pub(crate) scheduler: Rc, diff --git a/packages/desktop/src/controller.rs b/packages/desktop/src/controller.rs index 7139c083f..9037189b1 100644 --- a/packages/desktop/src/controller.rs +++ b/packages/desktop/src/controller.rs @@ -5,6 +5,7 @@ use futures_channel::mpsc::{unbounded, UnboundedSender}; use futures_util::StreamExt; #[cfg(target_os = "ios")] use objc::runtime::Object; +use serde_json::Value; use std::{ collections::HashMap, sync::Arc, @@ -19,6 +20,7 @@ use wry::{ pub(super) struct DesktopController { pub(super) webviews: HashMap, + pub(super) eval_sender: tokio::sync::mpsc::UnboundedSender, pub(super) pending_edits: Arc>>, pub(super) quit_app_on_close: bool, pub(super) is_ready: Arc, @@ -43,6 +45,7 @@ impl DesktopController { let pending_edits = edit_queue.clone(); let desktop_context_proxy = proxy.clone(); + let (eval_sender, eval_reciever) = tokio::sync::mpsc::unbounded_channel::(); std::thread::spawn(move || { // We create the runtime as multithreaded, so you can still "tokio::spawn" onto multiple threads @@ -54,7 +57,7 @@ impl DesktopController { runtime.block_on(async move { let mut dom = VirtualDom::new_with_props(root, props) - .with_root_context(DesktopContext::new(desktop_context_proxy)); + .with_root_context(DesktopContext::new(desktop_context_proxy, eval_reciever)); { let edits = dom.rebuild(); let mut queue = edit_queue.lock().unwrap(); @@ -88,6 +91,7 @@ impl DesktopController { Self { pending_edits, + eval_sender, webviews: HashMap::new(), is_ready: Arc::new(AtomicBool::new(false)), quit_app_on_close: true, diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 0c1751363..756814de0 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -2,6 +2,11 @@ use std::rc::Rc; use crate::controller::DesktopController; use dioxus_core::ScopeState; +use serde::de::Error; +use serde_json::Value; +use std::future::Future; +use std::future::IntoFuture; +use std::pin::Pin; use wry::application::event_loop::ControlFlow; use wry::application::event_loop::EventLoopProxy; #[cfg(target_os = "ios")] @@ -19,16 +24,6 @@ pub fn use_window(cx: &ScopeState) -> &DesktopContext { .unwrap() } -/// Get a closure that executes any JavaScript in the WebView context. -pub fn use_eval(cx: &ScopeState) -> &Rc { - let desktop = use_window(cx); - - &*cx.use_hook(|| { - let desktop = desktop.clone(); - Rc::new(move |script| desktop.eval(script)) - } as Rc) -} - /// An imperative interface to the current window. /// /// To get a handle to the current window, use the [`use_window`] hook. @@ -45,11 +40,18 @@ pub fn use_eval(cx: &ScopeState) -> &Rc { pub struct DesktopContext { /// The wry/tao proxy to the current window pub proxy: ProxyType, + pub(super) eval_reciever: Rc>>, } impl DesktopContext { - pub(crate) fn new(proxy: ProxyType) -> Self { - Self { proxy } + pub(crate) fn new( + proxy: ProxyType, + eval_reciever: tokio::sync::mpsc::UnboundedReceiver, + ) -> Self { + Self { + proxy, + eval_reciever: Rc::new(tokio::sync::Mutex::new(eval_reciever)), + } } /// trigger the drag-window event @@ -242,6 +244,18 @@ impl DesktopController { Resizable(state) => window.set_resizable(state), AlwaysOnTop(state) => window.set_always_on_top(state), + Eval(code) => { + let script = format!( + r#"window.ipc.postMessage(JSON.stringify({{"method":"eval_result", params: (function(){{ + {} + }})()}}));"#, + code + ); + if let Err(e) = webview.evaluate_script(&script) { + // we can't panic this error. + log::warn!("Eval script error: {e}"); + } + } CursorVisible(state) => window.set_cursor_visible(state), CursorGrab(state) => { let _ = window.set_cursor_grab(state); @@ -265,13 +279,6 @@ impl DesktopController { log::warn!("Devtools are disabled in release builds"); } - Eval(code) => { - if let Err(e) = webview.evaluate_script(code.as_str()) { - // we can't panic this error. - log::warn!("Eval script error: {e}"); - } - } - #[cfg(target_os = "ios")] PushView(view) => unsafe { use objc::runtime::Object; @@ -301,6 +308,39 @@ impl DesktopController { } } +/// Get a closure that executes any JavaScript in the WebView context. +pub fn use_eval(cx: &ScopeState) -> &dyn Fn(S) -> EvalResult { + let desktop = use_window(cx).clone(); + cx.use_hook(|| { + move |script| { + desktop.eval(script); + let recv = desktop.eval_reciever.clone(); + EvalResult { reciever: recv } + } + }) +} + +/// A future that resolves to the result of a JavaScript evaluation. +pub struct EvalResult { + reciever: Rc>>, +} + +impl IntoFuture for EvalResult { + type Output = Result; + + type IntoFuture = Pin>>>; + + fn into_future(self) -> Self::IntoFuture { + Box::pin(async move { + let mut reciever = self.reciever.lock().await; + match reciever.recv().await { + Some(result) => Ok(result), + None => Err(serde_json::Error::custom("No result returned")), + } + }) as Pin>>> + } +} + #[cfg(target_os = "ios")] fn is_main_thread() -> bool { use objc::runtime::{Class, BOOL, NO}; diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 585a7ff51..a635e3f4e 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -17,7 +17,7 @@ use std::sync::atomic::AtomicBool; use std::sync::Arc; use desktop_context::UserWindowEvent; -pub use desktop_context::{use_eval, use_window, DesktopContext}; +pub use desktop_context::{use_eval, use_window, DesktopContext, EvalResult}; use futures_channel::mpsc::UnboundedSender; pub use wry; pub use wry::application as tao; @@ -142,6 +142,7 @@ impl DesktopController { event_loop, self.is_ready.clone(), self.proxy.clone(), + self.eval_sender.clone(), self.event_tx.clone(), ); @@ -154,6 +155,7 @@ fn build_webview( event_loop: &tao::event_loop::EventLoopWindowTarget, is_ready: Arc, proxy: tao::event_loop::EventLoopProxy, + eval_sender: tokio::sync::mpsc::UnboundedSender, event_tx: UnboundedSender, ) -> wry::webview::WebView { let builder = cfg.window.clone(); @@ -183,6 +185,10 @@ fn build_webview( .with_ipc_handler(move |_window: &Window, payload: String| { parse_ipc_message(&payload) .map(|message| match message.method() { + "eval_result" => { + let result = message.params(); + eval_sender.send(result).unwrap(); + } "user_event" => { _ = event_tx.unbounded_send(message.params()); } diff --git a/packages/interpreter/Cargo.toml b/packages/interpreter/Cargo.toml index 4eb7410bd..07797897a 100644 --- a/packages/interpreter/Cargo.toml +++ b/packages/interpreter/Cargo.toml @@ -17,8 +17,10 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] wasm-bindgen = { version = "0.2.79", optional = true } js-sys = { version = "0.3.56", optional = true } web-sys = { version = "0.3.56", optional = true, features = ["Element", "Node"] } - +sledgehammer_bindgen = { version = "0.1.2", optional = true } +sledgehammer_utils = { version = "0.1.0", optional = true } [features] default = [] web = ["wasm-bindgen", "js-sys", "web-sys"] +sledgehammer = ["wasm-bindgen", "js-sys", "web-sys", "sledgehammer_bindgen", "sledgehammer_utils"] diff --git a/packages/interpreter/src/bindings.rs b/packages/interpreter/src/bindings.rs index d028b8d30..478b8ca5c 100644 --- a/packages/interpreter/src/bindings.rs +++ b/packages/interpreter/src/bindings.rs @@ -17,9 +17,6 @@ extern "C" { #[wasm_bindgen(method)] pub fn MountToRoot(this: &Interpreter); - #[wasm_bindgen(method)] - pub fn AppendChildren(this: &Interpreter, m: u32, id: u32); - #[wasm_bindgen(method)] pub fn AssignId(this: &Interpreter, path: &[u8], id: u32); @@ -76,4 +73,7 @@ extern "C" { #[wasm_bindgen(method)] pub fn PushRoot(this: &Interpreter, id: u32); + + #[wasm_bindgen(method)] + pub fn AppendChildren(this: &Interpreter, id: u32, m: u32); } diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index 93da98b0c..7b29c1683 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -89,8 +89,8 @@ export class Interpreter { PopRoot() { this.stack.pop(); } - AppendChildren(many) { - let root = this.stack[this.stack.length - (1 + many)]; + AppendChildren(id, many) { + let root = this.nodes[id]; let to_add = this.stack.splice(this.stack.length - many); for (let i = 0; i < many; i++) { root.appendChild(to_add[i]); diff --git a/packages/interpreter/src/lib.rs b/packages/interpreter/src/lib.rs index 076a89197..962bd6bbb 100644 --- a/packages/interpreter/src/lib.rs +++ b/packages/interpreter/src/lib.rs @@ -1,5 +1,10 @@ pub static INTERPRETER_JS: &str = include_str!("./interpreter.js"); +#[cfg(feature = "sledgehammer")] +mod sledgehammer_bindings; +#[cfg(feature = "sledgehammer")] +pub use sledgehammer_bindings::*; + #[cfg(feature = "web")] mod bindings; diff --git a/packages/interpreter/src/sledgehammer_bindings.rs b/packages/interpreter/src/sledgehammer_bindings.rs new file mode 100644 index 000000000..9be487c58 --- /dev/null +++ b/packages/interpreter/src/sledgehammer_bindings.rs @@ -0,0 +1,221 @@ +use js_sys::Function; +use sledgehammer_bindgen::bindgen; +use web_sys::Node; + +#[bindgen] +mod js { + const JS: &str = r#" + class ListenerMap { + constructor(root) { + // bubbling events can listen at the root element + this.global = {}; + // non bubbling events listen at the element the listener was created at + this.local = {}; + this.root = null; + this.handler = null; + } + + create(event_name, element, bubbles) { + if (bubbles) { + if (this.global[event_name] === undefined) { + this.global[event_name] = {}; + this.global[event_name].active = 1; + this.root.addEventListener(event_name, this.handler); + } else { + this.global[event_name].active++; + } + } + else { + const id = element.getAttribute("data-dioxus-id"); + if (!this.local[id]) { + this.local[id] = {}; + } + element.addEventListener(event_name, this.handler); + } + } + + remove(element, event_name, bubbles) { + if (bubbles) { + this.global[event_name].active--; + if (this.global[event_name].active === 0) { + this.root.removeEventListener(event_name, this.global[event_name].callback); + delete this.global[event_name]; + } + } + else { + const id = element.getAttribute("data-dioxus-id"); + delete this.local[id][event_name]; + if (this.local[id].length === 0) { + delete this.local[id]; + } + element.removeEventListener(event_name, this.handler); + } + } + + removeAllNonBubbling(element) { + const id = element.getAttribute("data-dioxus-id"); + delete this.local[id]; + } + } + function SetAttributeInner(node, field, value, ns) { + const name = field; + if (ns === "style") { + // ????? why do we need to do this + if (node.style === undefined) { + node.style = {}; + } + node.style[name] = value; + } else if (ns !== null && ns !== undefined && ns !== "") { + node.setAttributeNS(ns, name, value); + } else { + switch (name) { + case "value": + if (value !== node.value) { + node.value = value; + } + break; + case "checked": + node.checked = value === "true"; + break; + case "selected": + node.selected = value === "true"; + break; + case "dangerous_inner_html": + node.innerHTML = value; + break; + default: + // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 + if (value === "false" && bool_attrs.hasOwnProperty(name)) { + node.removeAttribute(name); + } else { + node.setAttribute(name, value); + } + } + } + } + function LoadChild(ptr, len) { + // iterate through each number and get that child + node = stack[stack.length - 1]; + ptr_end = ptr + len; + for (; ptr < ptr_end; ptr++) { + end = m.getUint8(ptr); + for (node = node.firstChild; end > 0; end--) { + node = node.nextSibling; + } + } + return node; + } + const listeners = new ListenerMap(); + let nodes = []; + let stack = []; + const templates = {}; + let node, els, end, ptr_end, k; + export function save_template(nodes, tmpl_id) { + templates[tmpl_id] = nodes; + } + export function set_node(id, node) { + nodes[id] = node; + } + export function initilize(root, handler) { + listeners.handler = handler; + nodes = [root]; + stack = [root]; + listeners.root = root; + } + function AppendChildren(id, many){ + root = nodes[id]; + els = stack.splice(stack.length-many); + for (k = 0; k < many; k++) { + root.appendChild(els[k]); + } + } + "#; + + extern "C" { + #[wasm_bindgen] + pub fn save_template(nodes: Vec, tmpl_id: u32); + + #[wasm_bindgen] + pub fn set_node(id: u32, node: Node); + + #[wasm_bindgen] + pub fn initilize(root: Node, handler: &Function); + } + + fn mount_to_root() { + "{AppendChildren(root, stack.length-1);}" + } + fn push_root(root: u32) { + "{stack.push(nodes[$root$]);}" + } + fn append_children(id: u32, many: u32) { + "{AppendChildren($id$, $many$);}" + } + fn pop_root() { + "{stack.pop();}" + } + fn replace_with(id: u32, n: u32) { + "{root = nodes[$id$]; els = stack.splice(stack.length-$n$); if (root.listening) { listeners.removeAllNonBubbling(root); } root.replaceWith(...els);}" + } + fn insert_after(id: u32, n: u32) { + "{nodes[$id$].after(...stack.splice(stack.length-$n$));}" + } + fn insert_before(id: u32, n: u32) { + "{nodes[$id$].before(...stack.splice(stack.length-$n$));}" + } + fn remove(id: u32) { + "{node = nodes[$id$]; if (node !== undefined) { if (node.listening) { listeners.removeAllNonBubbling(node); } node.remove(); }}" + } + fn create_raw_text(text: &str) { + "{stack.push(document.createTextNode($text$));}" + } + fn create_text_node(text: &str, id: u32) { + "{node = document.createTextNode($text$); nodes[$id$] = node; stack.push(node);}" + } + fn create_placeholder(id: u32) { + "{node = document.createElement('pre'); node.hidden = true; stack.push(node); nodes[$id$] = node;}" + } + fn new_event_listener(event_name: &str, id: u32, bubbles: u8) { + r#"node = nodes[id]; if(node.listening){node.listening += 1;}else{node.listening = 1;} node.setAttribute('data-dioxus-id', `\${id}`); listeners.create($event_name$, node, $bubbles$);"# + } + fn remove_event_listener(event_name: &str, id: u32, bubbles: u8) { + "{node = nodes[$id$]; node.listening -= 1; node.removeAttribute('data-dioxus-id'); listeners.remove(node, $event_name$, $bubbles$);}" + } + fn set_text(id: u32, text: &str) { + "{nodes[$id$].textContent = $text$;}" + } + fn set_attribute(id: u32, field: &str, value: &str, ns: &str) { + "{node = nodes[$id$]; SetAttributeInner(node, $field$, $value$, $ns$);}" + } + fn remove_attribute(id: u32, field: &str, ns: &str) { + r#"{name = $field$; + node = this.nodes[$id$]; + if (ns == "style") { + node.style.removeProperty(name); + } else if (ns !== null && ns !== undefined && ns !== "") { + node.removeAttributeNS(ns, name); + } else if (name === "value") { + node.value = ""; + } else if (name === "checked") { + node.checked = false; + } else if (name === "selected") { + node.selected = false; + } else if (name === "dangerous_inner_html") { + node.innerHTML = ""; + } else { + node.removeAttribute(name); + }}"# + } + fn assign_id(ptr: u32, len: u8, id: u32) { + "{nodes[$id$] = LoadChild($ptr$, $len$);}" + } + fn hydrate_text(ptr: u32, len: u8, value: &str, id: u32) { + "{node = LoadChild($ptr$, $len$); node.textContent = $value$; nodes[$id$] = node;}" + } + fn replace_placeholder(ptr: u32, len: u8, n: u32) { + "{els = stack.splice(stack.length - $n$); node = LoadChild($ptr$, $len$); node.replaceWith(...els);}" + } + fn load_template(tmpl_id: u32, index: u32, id: u32) { + "{node = templates[$tmpl_id$][$index$].cloneNode(true); nodes[$id$] = node; stack.push(node);}" + } +} diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 94e1e8613..40056eaaa 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -14,7 +14,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] dioxus-core = { path = "../core", version = "^0.2.1", features = ["serialize"] } dioxus-html = { path = "../html", version = "^0.2.1", features = ["wasm-bind"] } dioxus-interpreter-js = { path = "../interpreter", version = "^0.2.1", features = [ - "web" + "sledgehammer" ] } js-sys = "0.3.56" @@ -30,6 +30,7 @@ futures-util = "0.3.19" smallstr = "0.2.0" futures-channel = "0.3.21" serde_json = { version = "1.0" } +serde = { version = "1.0" } serde-wasm-bindgen = "0.4.5" [dependencies.web-sys] diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 0cdf46b3e..89a829823 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -7,20 +7,22 @@ //! - tests to ensure dyn_into works for various event types. //! - Partial delegation?> -use dioxus_core::{Mutation, Template}; +use dioxus_core::{Mutation, Template, TemplateAttribute, TemplateNode}; use dioxus_html::{event_bubbles, CompositionData, FormData}; -use dioxus_interpreter_js::Interpreter; +use dioxus_interpreter_js::{save_template, Channel}; use futures_channel::mpsc; +use rustc_hash::FxHashMap; use std::{any::Any, rc::Rc}; use wasm_bindgen::{closure::Closure, JsCast}; -use web_sys::{Document, Element, Event}; +use web_sys::{Document, Element, Event, HtmlElement}; use crate::Config; pub struct WebsysDom { - interpreter: Interpreter, - handler: Closure, - _root: Element, + document: Document, + templates: FxHashMap, + max_template_id: u32, + interpreter: Channel, } impl WebsysDom { @@ -32,67 +34,139 @@ impl WebsysDom { Some(root) => root, None => document.create_element("body").ok().unwrap(), }; + let interpreter = Channel::default(); - Self { - interpreter: Interpreter::new(root.clone()), - _root: root, - handler: Closure::wrap(Box::new(move |event: &web_sys::Event| { + let handler: Closure = + Closure::wrap(Box::new(move |event: &web_sys::Event| { let _ = event_channel.unbounded_send(event.clone()); - })), + })); + + dioxus_interpreter_js::initilize(root.unchecked_into(), handler.as_ref().unchecked_ref()); + handler.forget(); + Self { + document, + interpreter, + templates: FxHashMap::default(), + max_template_id: 0, } } pub fn mount(&mut self) { - self.interpreter.MountToRoot(); + self.interpreter.mount_to_root(); } pub fn load_templates(&mut self, templates: &[Template]) { log::debug!("Loading templates {:?}", templates); for template in templates { - self.interpreter - .SaveTemplate(serde_wasm_bindgen::to_value(&template).unwrap()); + let mut roots = vec![]; + + for root in template.roots { + roots.push(self.create_template_node(root)) + } + + self.templates + .insert(template.name.to_owned(), self.max_template_id); + save_template(roots, self.max_template_id); + self.max_template_id += 1 + } + } + + fn create_template_node(&self, v: &TemplateNode) -> web_sys::Node { + use TemplateNode::*; + match v { + Element { + tag, + namespace, + attrs, + children, + .. + } => { + let el = match namespace { + Some(ns) => self.document.create_element_ns(Some(ns), tag).unwrap(), + None => self.document.create_element(tag).unwrap(), + }; + for attr in *attrs { + if let TemplateAttribute::Static { + name, + value, + namespace, + } = attr + { + match namespace { + Some(ns) if *ns == "style" => el + .dyn_ref::() + .unwrap() + .style() + .set_property(name, value) + .unwrap(), + Some(ns) => el.set_attribute_ns(Some(ns), name, value).unwrap(), + None => el.set_attribute(name, value).unwrap(), + } + } + } + for child in *children { + let _ = el.append_child(&self.create_template_node(child)); + } + el.dyn_into().unwrap() + } + Text { text } => self.document.create_text_node(text).dyn_into().unwrap(), + DynamicText { .. } => self.document.create_text_node("p").dyn_into().unwrap(), + Dynamic { .. } => { + let el = self.document.create_element("pre").unwrap(); + let _ = el.toggle_attribute("hidden"); + el.dyn_into().unwrap() + } } } pub fn apply_edits(&mut self, mut edits: Vec) { use Mutation::*; - let i = &self.interpreter; - for edit in edits.drain(..) { + let i = &mut self.interpreter; + for edit in &edits { match edit { - AppendChildren { id, m } => i.AppendChildren(m as u32, id.0 as u32), - AssignId { path, id } => i.AssignId(path, id.0 as u32), - CreatePlaceholder { id } => i.CreatePlaceholder(id.0 as u32), - 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), + AppendChildren { id, m } => i.append_children(id.0 as u32, *m as u32), + AssignId { path, id } => { + i.assign_id(path.as_ptr() as u32, path.len() as u8, id.0 as u32) + } + CreatePlaceholder { id } => i.create_placeholder(id.0 as u32), + CreateTextNode { value, id } => i.create_text_node(value, id.0 as u32), + HydrateText { path, value, id } => { + i.hydrate_text(path.as_ptr() as u32, path.len() as u8, value, id.0 as u32) + } + LoadTemplate { name, index, id } => { + if let Some(tmpl_id) = self.templates.get(*name) { + i.load_template(*tmpl_id, *index as u32, id.0 as u32) + } + } + ReplaceWith { id, m } => i.replace_with(id.0 as u32, *m as u32), + ReplacePlaceholder { path, m } => { + i.replace_placeholder(path.as_ptr() as u32, path.len() as u8, *m as u32) + } + InsertAfter { id, m } => i.insert_after(id.0 as u32, *m as u32), + InsertBefore { id, m } => i.insert_before(id.0 as u32, *m as u32), SetAttribute { name, value, id, ns, - } => i.SetAttribute(id.0 as u32, name, value.into(), ns), + } => i.set_attribute(id.0 as u32, name, value, ns.unwrap_or_default()), SetBoolAttribute { name, value, id } => { - i.SetBoolAttribute(id.0 as u32, name, value) + i.set_attribute(id.0 as u32, name, if *value { "true" } else { "false" }, "") } - SetText { value, id } => i.SetText(id.0 as u32, value.into()), + SetText { value, id } => i.set_text(id.0 as u32, value), NewEventListener { name, id, .. } => { - self.interpreter.NewEventListener( - name, - id.0 as u32, - event_bubbles(&name[2..]), - self.handler.as_ref().unchecked_ref(), - ); + i.new_event_listener(name, id.0 as u32, event_bubbles(&name[2..]) as u8); } - 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), + RemoveEventListener { name, id } => { + i.remove_event_listener(name, id.0 as u32, event_bubbles(&name[2..]) as u8) + } + Remove { id } => i.remove(id.0 as u32), + PushRoot { id } => i.push_root(id.0 as u32), } } + edits.clear(); + i.flush(); } } diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 62a2bf4dd..f430a7555 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -55,7 +55,7 @@ pub use crate::cfg::Config; use crate::dom::virtual_event_from_websys_event; -pub use crate::util::use_eval; +pub use crate::util::{use_eval, EvalResult}; use dioxus_core::{Element, ElementId, Scope, VirtualDom}; use futures_util::{pin_mut, FutureExt, StreamExt}; @@ -195,7 +195,7 @@ pub async fn run_with_props(root: fn(Scope) -> Element, root_prop // the mutations come back with nothing - we need to actually mount them websys_dom.mount(); - let mut work_loop = ric_raf::RafLoop::new(); + let _work_loop = ric_raf::RafLoop::new(); loop { log::debug!("waiting for work"); @@ -228,6 +228,7 @@ pub async fn run_with_props(root: fn(Scope) -> Element, root_prop res = rx.try_next().transpose().unwrap().ok(); } + // Todo: This is currently disabled because it has a negative impact on responce times for events but it could be re-enabled for tasks // Jank free rendering // // 1. wait for the browser to give us "idle" time @@ -236,13 +237,13 @@ pub async fn run_with_props(root: fn(Scope) -> Element, root_prop // 4. Wait for the animation frame to patch the dom // wait for the mainthread to schedule us in - let deadline = work_loop.wait_for_idle_time().await; + // let deadline = work_loop.wait_for_idle_time().await; // run the virtualdom work phase until the frame deadline is reached - let edits = dom.render_with_deadline(deadline).await; + let edits = dom.render_immediate(); // wait for the animation frame to fire so we can apply our changes - work_loop.wait_for_raf().await; + // work_loop.wait_for_raf().await; websys_dom.load_templates(&edits.templates); websys_dom.apply_edits(edits.edits); diff --git a/packages/web/src/util.rs b/packages/web/src/util.rs index 772f53608..3ed8bd479 100644 --- a/packages/web/src/util.rs +++ b/packages/web/src/util.rs @@ -1,6 +1,13 @@ //! Utilities specific to websys +use std::{ + future::{IntoFuture, Ready}, + str::FromStr, +}; + use dioxus_core::*; +use serde::de::Error; +use serde_json::Value; /// Get a closure that executes any JavaScript in the webpage. /// @@ -15,12 +22,51 @@ use dioxus_core::*; /// /// The closure will panic if the provided script is not valid JavaScript code /// or if it returns an uncaught error. -pub fn use_eval(cx: &ScopeState) -> &dyn Fn(S) { +pub fn use_eval(cx: &ScopeState) -> &dyn Fn(S) -> EvalResult { cx.use_hook(|| { |script: S| { - js_sys::Function::new_no_args(&script.to_string()) - .call0(&wasm_bindgen::JsValue::NULL) - .expect("failed to eval script"); + let body = script.to_string(); + EvalResult { + value: if let Ok(value) = + js_sys::Function::new_no_args(&body).call0(&wasm_bindgen::JsValue::NULL) + { + if let Ok(stringified) = js_sys::JSON::stringify(&value) { + if !stringified.is_undefined() && stringified.is_valid_utf16() { + let string: String = stringified.into(); + Value::from_str(&string) + } else { + Err(serde_json::Error::custom("Failed to stringify result")) + } + } else { + Err(serde_json::Error::custom("Failed to stringify result")) + } + } else { + Err(serde_json::Error::custom("Failed to execute script")) + }, + } } }) } + +/// A wrapper around the result of a JavaScript evaluation. +/// This implements IntoFuture to be compatible with the desktop renderer's EvalResult. +pub struct EvalResult { + value: Result, +} + +impl EvalResult { + /// Get the result of the Javascript execution. + pub fn get(self) -> Result { + self.value + } +} + +impl IntoFuture for EvalResult { + type Output = Result; + + type IntoFuture = Ready>; + + fn into_future(self) -> Self::IntoFuture { + std::future::ready(self.value) + } +}