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/examples/rsx_usage.rs b/examples/rsx_usage.rs index fb7bb623c..23ab19cf9 100644 --- a/examples/rsx_usage.rs +++ b/examples/rsx_usage.rs @@ -1,5 +1,4 @@ #![allow(non_snake_case)] - //! A tour of the rsx! macro //! ------------------------ //! @@ -39,7 +38,7 @@ //! - Accept a list of vnodes as children for a Fragment component //! - Allow keyed fragments in iterators //! - Allow top-level fragments -//! + fn main() { dioxus_desktop::launch(app); } diff --git a/examples/svg_basic.rs b/examples/svg_basic.rs index e950eb339..1264aa001 100644 --- a/examples/svg_basic.rs +++ b/examples/svg_basic.rs @@ -67,6 +67,12 @@ fn app(cx: Scope) -> Element { stroke: "blue", stroke_width: "5", } + path { + d: "M9.00001 9C9 62 103.5 124 103.5 178", + stroke: "#3CC4DC", + "stroke-linecap": "square", + "stroke-width": "square", + } })) } diff --git a/packages/core-macro/Cargo.toml b/packages/core-macro/Cargo.toml index f9b97a436..e8dd1e1ef 100644 --- a/packages/core-macro/Cargo.toml +++ b/packages/core-macro/Cargo.toml @@ -18,7 +18,7 @@ proc-macro = true proc-macro2 = { version = "1.0" } quote = "1.0" syn = { version = "1.0", features = ["full", "extra-traits"] } -dioxus-rsx = { path = "../rsx" } +dioxus-rsx = { path = "../rsx", version = "0.0.1" } # testing [dev-dependencies] diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index 475000016..4e4f5600a 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -26,11 +26,6 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token } /// The rsx! macro makes it easy for developers to write jsx-style markup in their components. -/// -/// ## Complete Reference Guide: -/// ```ignore -#[doc = include_str!("../../../examples/rsx_usage.rs")] -/// ``` #[proc_macro] pub fn rsx(s: TokenStream) -> TokenStream { match syn::parse::(s) { @@ -42,11 +37,6 @@ pub fn rsx(s: TokenStream) -> TokenStream { /// The render! macro makes it easy for developers to write jsx-style markup in their components. /// /// The render macro automatically renders rsx - making it unhygenic. -/// -/// ## Complete Reference Guide: -/// ```ignore -#[doc = include_str!("../../../examples/rsx_usage.rs")] -/// ``` #[proc_macro] pub fn render(s: TokenStream) -> TokenStream { match syn::parse::(s) { diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index f779e6ba2..37f78f04d 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -18,7 +18,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] bumpalo = { version = "3.6", features = ["collections", "boxed"] } # faster hashmaps -fxhash = "0.2" +rustc-hash = "1.1.0" # Used in diffing longest-increasing-subsequence = "0.1.0" @@ -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..e893cd2b5 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -11,7 +11,7 @@ use crate::{ AttributeValue, TemplateNode, }; -use fxhash::{FxHashMap, FxHashSet}; +use rustc_hash::{FxHashMap, FxHashSet}; use DynamicNode::*; impl<'b> VirtualDom { @@ -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); } @@ -432,7 +434,7 @@ impl<'b> VirtualDom { // The stack is empty upon entry. fn diff_keyed_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { if cfg!(debug_assertions) { - let mut keys = fxhash::FxHashSet::default(); + let mut keys = rustc_hash::FxHashSet::default(); let mut assert_unique_keys = |children: &'b [VNode<'b>]| { keys.clear(); for child in children { diff --git a/packages/core/src/lazynodes.rs b/packages/core/src/lazynodes.rs index 020b307d9..eca7829dc 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,7 @@ 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> { + (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/lib.rs b/packages/core/src/lib.rs index 613a4a115..7ce2b723f 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -81,9 +81,9 @@ pub use crate::innerlude::{ /// This includes types like [`Scope`], [`Element`], and [`Component`]. pub mod prelude { pub use crate::innerlude::{ - fc_to_builder, Element, Event, EventHandler, Fragment, LazyNodes, Properties, Scope, - ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, VNode, - VirtualDom, + fc_to_builder, Component, Element, Event, EventHandler, Fragment, LazyNodes, Properties, + Scope, ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, + VNode, VirtualDom, }; } diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index 2512ae9fa..ecca2a678 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -1,4 +1,4 @@ -use fxhash::FxHashSet; +use rustc_hash::FxHashSet; use crate::{arena::ElementId, ScopeId, Template}; diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 81ed5da06..68c12537f 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -441,6 +441,12 @@ impl<'a> IntoDynNode<'a> for VNode<'a> { } } +impl<'a> IntoDynNode<'a> for DynamicNode<'a> { + fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { + self + } +} + // An element that's an error is currently lost into the ether impl<'a> IntoDynNode<'a> for Element<'a> { fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { 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..241418fa7 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 rustc_hash::{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..42ba90437 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 rustc_hash::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/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index cc084b403..a544aa305 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -104,10 +104,10 @@ fn get_asset_root() -> Option { #[cfg(target_os = "macos")] { let bundle = core_foundation::bundle::CFBundle::main_bundle(); - let bundle_path = dbg!(bundle.path()?); - let resources_path = dbg!(bundle.resources_path()?); - let absolute_resources_root = dbg!(bundle_path.join(resources_path)); - let canonical_resources_root = dbg!(dunce::canonicalize(absolute_resources_root).ok()?); + let bundle_path = bundle.path()?; + let resources_path = bundle.resources_path()?; + let absolute_resources_root = bundle_path.join(resources_path); + let canonical_resources_root = dunce::canonicalize(absolute_resources_root).ok()?; return Some(canonical_resources_root); } diff --git a/packages/dioxus/src/lib.rs b/packages/dioxus/src/lib.rs index 86adae336..fed3d2bd5 100644 --- a/packages/dioxus/src/lib.rs +++ b/packages/dioxus/src/lib.rs @@ -1,5 +1,3 @@ -#![doc = include_str!("../../../notes/README.md")] - pub use dioxus_core as core; #[cfg(feature = "hooks")] diff --git a/packages/interpreter/Cargo.toml b/packages/interpreter/Cargo.toml index 4eb7410bd..6fcf19f27 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.3", 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..3ac52a8f1 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -90,7 +90,8 @@ export class Interpreter { this.stack.pop(); } AppendChildren(many) { - let root = this.stack[this.stack.length - (1 + many)]; + // let root = this.nodes[id]; + let root = this.stack[this.stack.length - 1 - many]; 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..a81e16235 --- /dev/null +++ b/packages/interpreter/src/sledgehammer_bindings.rs @@ -0,0 +1,231 @@ +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) { + r#"{ + node = LoadChild($ptr$, $len$); + if (node.nodeType == Node.TEXT_NODE) { + node.textContent = value; + } else { + let text = document.createTextNode(value); + node.replaceWith(text); + node = text; + } + 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/native-core-macro/src/lib.rs b/packages/native-core-macro/src/lib.rs index dda96b512..5bf50afcc 100644 --- a/packages/native-core-macro/src/lib.rs +++ b/packages/native-core-macro/src/lib.rs @@ -306,8 +306,7 @@ impl Member { + field.ty.to_token_stream().to_string().as_str()) .as_str(), Span::call_site(), - ) - .into(), + ), ident: field.ident.as_ref()?.clone(), }) } diff --git a/packages/native-core/src/real_dom.rs b/packages/native-core/src/real_dom.rs index 7d56b0dca..656c59b52 100644 --- a/packages/native-core/src/real_dom.rs +++ b/packages/native-core/src/real_dom.rs @@ -102,11 +102,7 @@ impl RealDom { self.tree.add_child(node_id, child_id); } - fn create_template_node( - &mut self, - node: &TemplateNode, - mutations_vec: &mut FxHashMap, - ) -> RealNodeId { + fn create_template_node(&mut self, node: &TemplateNode) -> RealNodeId { match node { TemplateNode::Element { tag, @@ -139,27 +135,18 @@ impl RealDom { }); let node_id = self.create_node(node); for child in *children { - let child_id = self.create_template_node(child, mutations_vec); + let child_id = self.create_template_node(child); self.add_child(node_id, child_id); } node_id } - TemplateNode::Text { text } => { - let node_id = self.create_node(Node::new(NodeType::Text { - text: text.to_string(), - })); - node_id - } - TemplateNode::Dynamic { .. } => { - let node_id = self.create_node(Node::new(NodeType::Placeholder)); - node_id - } - TemplateNode::DynamicText { .. } => { - let node_id = self.create_node(Node::new(NodeType::Text { - text: String::new(), - })); - node_id - } + TemplateNode::Text { text } => self.create_node(Node::new(NodeType::Text { + text: text.to_string(), + })), + TemplateNode::Dynamic { .. } => self.create_node(Node::new(NodeType::Placeholder)), + TemplateNode::DynamicText { .. } => self.create_node(Node::new(NodeType::Text { + text: String::new(), + })), } } @@ -172,7 +159,7 @@ impl RealDom { for template in mutations.templates { let mut template_root_ids = Vec::new(); for root in template.roots { - let id = self.create_template_node(root, &mut nodes_updated); + let id = self.create_template_node(root); template_root_ids.push(id); } self.templates diff --git a/packages/native-core/src/utils/cursor.rs b/packages/native-core/src/utils/cursor.rs new file mode 100644 index 000000000..86bcb71b1 --- /dev/null +++ b/packages/native-core/src/utils/cursor.rs @@ -0,0 +1,484 @@ +use std::cmp::Ordering; + +use dioxus_html::input_data::keyboard_types::{Code, Key, Modifiers}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Pos { + pub col: usize, + pub row: usize, +} + +impl Pos { + pub fn new(col: usize, row: usize) -> Self { + Self { row, col } + } + + pub fn up(&mut self, rope: &str) { + self.move_row(-1, rope); + } + + pub fn down(&mut self, rope: &str) { + self.move_row(1, rope); + } + + pub fn right(&mut self, rope: &str) { + self.move_col(1, rope); + } + + pub fn left(&mut self, rope: &str) { + self.move_col(-1, rope); + } + + pub fn move_row(&mut self, change: i32, rope: &str) { + let new = self.row as i32 + change; + if new >= 0 && new < rope.lines().count() as i32 { + self.row = new as usize; + } + } + + pub fn move_col(&mut self, change: i32, rope: &str) { + self.realize_col(rope); + let idx = self.idx(rope) as i32; + if idx + change >= 0 && idx + change <= rope.len() as i32 { + let len_line = self.len_line(rope) as i32; + let new_col = self.col as i32 + change; + let diff = new_col - len_line; + if diff > 0 { + self.down(rope); + self.col = 0; + self.move_col(diff - 1, rope); + } else if new_col < 0 { + self.up(rope); + self.col = self.len_line(rope); + self.move_col(new_col + 1, rope); + } else { + self.col = new_col as usize; + } + } + } + + pub fn col(&self, rope: &str) -> usize { + self.col.min(self.len_line(rope)) + } + + pub fn row(&self) -> usize { + self.row + } + + fn len_line(&self, rope: &str) -> usize { + let line = rope.lines().nth(self.row).unwrap_or_default(); + let len = line.len(); + if len > 0 && line.chars().nth(len - 1) == Some('\n') { + len - 1 + } else { + len + } + } + + pub fn idx(&self, rope: &str) -> usize { + rope.lines().take(self.row).map(|l| l.len()).sum::() + self.col(rope) + } + + // the column can be more than the line length, cap it + pub fn realize_col(&mut self, rope: &str) { + self.col = self.col(rope); + } +} + +impl Ord for Pos { + fn cmp(&self, other: &Self) -> Ordering { + self.row.cmp(&other.row).then(self.col.cmp(&other.col)) + } +} + +impl PartialOrd for Pos { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Cursor { + pub start: Pos, + pub end: Option, +} + +impl Cursor { + pub fn from_start(pos: Pos) -> Self { + Self { + start: pos, + end: None, + } + } + + pub fn new(start: Pos, end: Pos) -> Self { + Self { + start, + end: Some(end), + } + } + + fn move_cursor(&mut self, f: impl FnOnce(&mut Pos), shift: bool) { + if shift { + self.with_end(f); + } else { + f(&mut self.start); + self.end = None; + } + } + + fn delete_selection(&mut self, text: &mut String) -> [i32; 2] { + let first = self.first(); + let last = self.last(); + let dr = first.row as i32 - last.row as i32; + let dc = if dr != 0 { + -(last.col as i32) + } else { + first.col as i32 - last.col as i32 + }; + text.replace_range(first.idx(text)..last.idx(text), ""); + if let Some(end) = self.end.take() { + if self.start > end { + self.start = end; + } + } + [dc, dr] + } + + pub fn handle_input( + &mut self, + data: &dioxus_html::KeyboardData, + text: &mut String, + max_width: usize, + ) { + use Code::*; + match data.code() { + ArrowUp => { + self.move_cursor(|c| c.up(text), data.modifiers().contains(Modifiers::SHIFT)); + } + ArrowDown => { + self.move_cursor( + |c| c.down(text), + data.modifiers().contains(Modifiers::SHIFT), + ); + } + ArrowRight => { + if data.modifiers().contains(Modifiers::CONTROL) { + self.move_cursor( + |c| { + let mut change = 1; + let idx = c.idx(text); + let length = text.len(); + while idx + change < length { + let chr = text.chars().nth(idx + change).unwrap(); + if chr.is_whitespace() { + break; + } + change += 1; + } + c.move_col(change as i32, text); + }, + data.modifiers().contains(Modifiers::SHIFT), + ); + } else { + self.move_cursor( + |c| c.right(text), + data.modifiers().contains(Modifiers::SHIFT), + ); + } + } + ArrowLeft => { + if data.modifiers().contains(Modifiers::CONTROL) { + self.move_cursor( + |c| { + let mut change = -1; + let idx = c.idx(text) as i32; + while idx + change > 0 { + let chr = text.chars().nth((idx + change) as usize).unwrap(); + if chr == ' ' { + break; + } + change -= 1; + } + c.move_col(change as i32, text); + }, + data.modifiers().contains(Modifiers::SHIFT), + ); + } else { + self.move_cursor( + |c| c.left(text), + data.modifiers().contains(Modifiers::SHIFT), + ); + } + } + End => { + self.move_cursor( + |c| c.col = c.len_line(text), + data.modifiers().contains(Modifiers::SHIFT), + ); + } + Home => { + self.move_cursor(|c| c.col = 0, data.modifiers().contains(Modifiers::SHIFT)); + } + Backspace => { + self.start.realize_col(text); + let mut start_idx = self.start.idx(text); + if self.end.is_some() { + self.delete_selection(text); + } else if start_idx > 0 { + self.start.left(text); + text.replace_range(start_idx - 1..start_idx, ""); + if data.modifiers().contains(Modifiers::CONTROL) { + start_idx = self.start.idx(text); + while start_idx > 0 + && text + .chars() + .nth(start_idx - 1) + .filter(|c| *c != ' ') + .is_some() + { + self.start.left(text); + text.replace_range(start_idx - 1..start_idx, ""); + start_idx = self.start.idx(text); + } + } + } + } + Enter => { + if text.len() + 1 - self.selection_len(text) <= max_width { + text.insert(self.start.idx(text), '\n'); + self.start.col = 0; + self.start.down(text); + } + } + Tab => { + if text.len() + 1 - self.selection_len(text) <= max_width { + self.start.realize_col(text); + self.delete_selection(text); + text.insert(self.start.idx(text), '\t'); + self.start.right(text); + } + } + _ => { + self.start.realize_col(text); + if let Key::Character(character) = data.key() { + if text.len() + 1 - self.selection_len(text) <= max_width { + self.delete_selection(text); + let character = character.chars().next().unwrap(); + text.insert(self.start.idx(text), character); + self.start.right(text); + } + } + } + } + } + + pub fn with_end(&mut self, f: impl FnOnce(&mut Pos)) { + let mut new = self.end.take().unwrap_or_else(|| self.start.clone()); + f(&mut new); + self.end.replace(new); + } + + pub fn first(&self) -> &Pos { + if let Some(e) = &self.end { + e.min(&self.start) + } else { + &self.start + } + } + + pub fn last(&self) -> &Pos { + if let Some(e) = &self.end { + e.max(&self.start) + } else { + &self.start + } + } + + pub fn selection_len(&self, text: &str) -> usize { + self.last().idx(text) - self.first().idx(text) + } +} + +impl Default for Cursor { + fn default() -> Self { + Self { + start: Pos::new(0, 0), + end: None, + } + } +} + +#[test] +fn pos_direction_movement() { + let mut pos = Pos::new(100, 0); + let text = "hello world\nhi"; + + assert_eq!(pos.col(text), text.lines().next().unwrap_or_default().len()); + pos.down(text); + assert_eq!(pos.col(text), text.lines().nth(1).unwrap_or_default().len()); + pos.up(text); + assert_eq!(pos.col(text), text.lines().next().unwrap_or_default().len()); + pos.left(text); + assert_eq!( + pos.col(text), + text.lines().next().unwrap_or_default().len() - 1 + ); + pos.right(text); + assert_eq!(pos.col(text), text.lines().next().unwrap_or_default().len()); +} + +#[test] +fn pos_col_movement() { + let mut pos = Pos::new(100, 0); + let text = "hello world\nhi"; + + // move inside a row + pos.move_col(-5, text); + assert_eq!( + pos.col(text), + text.lines().next().unwrap_or_default().len() - 5 + ); + pos.move_col(5, text); + assert_eq!(pos.col(text), text.lines().next().unwrap_or_default().len()); + + // move between rows + pos.move_col(3, text); + assert_eq!(pos.col(text), 2); + pos.move_col(-3, text); + assert_eq!(pos.col(text), text.lines().next().unwrap_or_default().len()); + + // don't panic if moving out of range + pos.move_col(-100, text); + pos.move_col(1000, text); +} + +#[test] +fn cursor_row_movement() { + let mut pos = Pos::new(100, 0); + let text = "hello world\nhi"; + + pos.move_row(1, text); + assert_eq!(pos.row(), 1); + pos.move_row(-1, text); + assert_eq!(pos.row(), 0); + + // don't panic if moving out of range + pos.move_row(-100, text); + pos.move_row(1000, text); +} + +#[test] +fn cursor_input() { + let mut cursor = Cursor::from_start(Pos::new(0, 0)); + let mut text = "hello world\nhi".to_string(); + + for _ in 0..5 { + cursor.handle_input( + &dioxus_html::KeyboardData::new( + dioxus_html::input_data::keyboard_types::Key::ArrowRight, + dioxus_html::input_data::keyboard_types::Code::ArrowRight, + dioxus_html::input_data::keyboard_types::Location::Standard, + false, + Modifiers::empty(), + ), + &mut text, + 10, + ); + } + + for _ in 0..5 { + cursor.handle_input( + &dioxus_html::KeyboardData::new( + dioxus_html::input_data::keyboard_types::Key::Backspace, + dioxus_html::input_data::keyboard_types::Code::Backspace, + dioxus_html::input_data::keyboard_types::Location::Standard, + false, + Modifiers::empty(), + ), + &mut text, + 10, + ); + } + + assert_eq!(text, " world\nhi"); + + let goal_text = "hello world\nhi"; + let max_width = goal_text.len(); + cursor.handle_input( + &dioxus_html::KeyboardData::new( + dioxus_html::input_data::keyboard_types::Key::Character("h".to_string()), + dioxus_html::input_data::keyboard_types::Code::KeyH, + dioxus_html::input_data::keyboard_types::Location::Standard, + false, + Modifiers::empty(), + ), + &mut text, + max_width, + ); + + cursor.handle_input( + &dioxus_html::KeyboardData::new( + dioxus_html::input_data::keyboard_types::Key::Character("e".to_string()), + dioxus_html::input_data::keyboard_types::Code::KeyE, + dioxus_html::input_data::keyboard_types::Location::Standard, + false, + Modifiers::empty(), + ), + &mut text, + max_width, + ); + + cursor.handle_input( + &dioxus_html::KeyboardData::new( + dioxus_html::input_data::keyboard_types::Key::Character("l".to_string()), + dioxus_html::input_data::keyboard_types::Code::KeyL, + dioxus_html::input_data::keyboard_types::Location::Standard, + false, + Modifiers::empty(), + ), + &mut text, + max_width, + ); + + cursor.handle_input( + &dioxus_html::KeyboardData::new( + dioxus_html::input_data::keyboard_types::Key::Character("l".to_string()), + dioxus_html::input_data::keyboard_types::Code::KeyL, + dioxus_html::input_data::keyboard_types::Location::Standard, + false, + Modifiers::empty(), + ), + &mut text, + max_width, + ); + + cursor.handle_input( + &dioxus_html::KeyboardData::new( + dioxus_html::input_data::keyboard_types::Key::Character("o".to_string()), + dioxus_html::input_data::keyboard_types::Code::KeyO, + dioxus_html::input_data::keyboard_types::Location::Standard, + false, + Modifiers::empty(), + ), + &mut text, + max_width, + ); + + // these should be ignored + for _ in 0..10 { + cursor.handle_input( + &dioxus_html::KeyboardData::new( + dioxus_html::input_data::keyboard_types::Key::Character("o".to_string()), + dioxus_html::input_data::keyboard_types::Code::KeyO, + dioxus_html::input_data::keyboard_types::Location::Standard, + false, + Modifiers::empty(), + ), + &mut text, + max_width, + ); + } + + assert_eq!(text.to_string(), goal_text); +} diff --git a/packages/native-core/src/utils/mod.rs b/packages/native-core/src/utils/mod.rs new file mode 100644 index 000000000..58e98b0fa --- /dev/null +++ b/packages/native-core/src/utils/mod.rs @@ -0,0 +1,3 @@ +mod persistant_iterator; +pub use persistant_iterator::*; +pub mod cursor; diff --git a/packages/native-core/src/utils.rs b/packages/native-core/src/utils/persistant_iterator.rs similarity index 100% rename from packages/native-core/src/utils.rs rename to packages/native-core/src/utils/persistant_iterator.rs diff --git a/packages/rsx/Cargo.toml b/packages/rsx/Cargo.toml index 40b785b6e..40637eff8 100644 --- a/packages/rsx/Cargo.toml +++ b/packages/rsx/Cargo.toml @@ -1,8 +1,13 @@ [package] name = "dioxus-rsx" -version = "0.0.0" +version = "0.0.1" edition = "2018" license = "MIT/Apache-2.0" +description = "Core functionality for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences" +repository = "https://github.com/DioxusLabs/dioxus/" +homepage = "https://dioxuslabs.com" +documentation = "https://docs.rs/dioxus-rsx" +keywords = ["dom", "ui", "gui", "react", "wasm"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -10,5 +15,4 @@ license = "MIT/Apache-2.0" proc-macro2 = { version = "1.0", features = ["span-locations"] } syn = { version = "1.0", features = ["full", "extra-traits"] } quote = { version = "1.0" } -dioxus-core = { path = "../core", features = ["serialize"] } serde = { version = "1.0", features = ["derive"] } diff --git a/packages/rsx/src/element.rs b/packages/rsx/src/element.rs index 307059f9d..ae4bfabe9 100644 --- a/packages/rsx/src/element.rs +++ b/packages/rsx/src/element.rs @@ -45,7 +45,7 @@ impl Parse for Element { content.parse::()?; - if content.peek(LitStr) && content.peek2(Token![,]) { + if content.peek(LitStr) { let value = content.parse()?; attributes.push(ElementAttrNamed { el_name: el_name.clone(), @@ -53,7 +53,6 @@ impl Parse for Element { }); } else { let value = content.parse::()?; - attributes.push(ElementAttrNamed { el_name: el_name.clone(), attr: ElementAttr::CustomAttrExpression { name, value }, diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs index ccfcda8fa..913ff8660 100644 --- a/packages/rsx/src/lib.rs +++ b/packages/rsx/src/lib.rs @@ -182,7 +182,7 @@ impl<'a> DynamicContext<'a> { let static_attrs = el.attributes.iter().map(|attr| match &attr.attr { ElementAttr::AttrText { name, value } if value.is_static() => { - let value = value.source.as_ref().unwrap(); + let value = value.to_static().unwrap(); quote! { ::dioxus::core::TemplateAttribute::Static { name: dioxus_elements::#el_name::#name.0, @@ -196,7 +196,7 @@ impl<'a> DynamicContext<'a> { } ElementAttr::CustomAttrText { name, value } if value.is_static() => { - let value = value.source.as_ref().unwrap(); + let value = value.to_static().unwrap(); quote! { ::dioxus::core::TemplateAttribute::Static { name: #name, @@ -244,7 +244,7 @@ impl<'a> DynamicContext<'a> { } BodyNode::Text(text) if text.is_static() => { - let text = text.source.as_ref().unwrap(); + let text = text.to_static().unwrap(); quote! { ::dioxus::core::TemplateNode::Text{ text: #text } } } diff --git a/packages/tui/Cargo.toml b/packages/tui/Cargo.toml index 116784c7b..94aaf3296 100644 --- a/packages/tui/Cargo.toml +++ b/packages/tui/Cargo.toml @@ -13,6 +13,7 @@ license = "MIT/Apache-2.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +dioxus = { path = "../dioxus", version = "^0.2.1" } dioxus-core = { path = "../core", version = "^0.2.1" } dioxus-html = { path = "../html", version = "^0.2.1" } dioxus-native-core = { path = "../native-core", version = "^0.2.0" } diff --git a/packages/tui/examples/tui_widgets.rs b/packages/tui/examples/tui_widgets.rs new file mode 100644 index 000000000..793057bc5 --- /dev/null +++ b/packages/tui/examples/tui_widgets.rs @@ -0,0 +1,93 @@ +use dioxus::prelude::*; +use dioxus_html::FormData; +use dioxus_tui::prelude::*; +use dioxus_tui::Config; + +fn main() { + dioxus_tui::launch_cfg(app, Config::new()); +} + +fn app(cx: Scope) -> Element { + let bg_green = use_state(cx, || false); + + let color = if *bg_green.get() { "green" } else { "red" }; + cx.render(rsx! { + div{ + width: "100%", + background_color: "{color}", + flex_direction: "column", + align_items: "center", + justify_content: "center", + + Input{ + oninput: |data: FormData| if &data.value == "good"{ + bg_green.set(true); + } else{ + bg_green.set(false); + }, + r#type: "checkbox", + value: "good", + width: "50%", + height: "10%", + checked: "true", + } + Input{ + oninput: |data: FormData| if &data.value == "hello world"{ + bg_green.set(true); + } else{ + bg_green.set(false); + }, + width: "50%", + height: "10%", + maxlength: "11", + } + Input{ + oninput: |data: FormData| { + if (data.value.parse::().unwrap() - 40.0).abs() < 5.0 { + bg_green.set(true); + } else{ + bg_green.set(false); + } + }, + r#type: "range", + width: "50%", + height: "10%", + min: "20", + max: "80", + } + Input{ + oninput: |data: FormData| { + if data.value == "10"{ + bg_green.set(true); + } else{ + bg_green.set(false); + } + }, + r#type: "number", + width: "50%", + height: "10%", + maxlength: "4", + } + Input{ + oninput: |data: FormData| { + if data.value == "hello world"{ + bg_green.set(true); + } else{ + bg_green.set(false); + } + }, + r#type: "password", + width: "50%", + height: "10%", + maxlength: "11", + } + Input{ + onclick: |_: FormData| bg_green.set(true), + r#type: "button", + value: "green", + width: "50%", + height: "10%", + } + } + }) +} diff --git a/packages/tui/src/hooks.rs b/packages/tui/src/hooks.rs index b57b71e13..d627ac3e0 100644 --- a/packages/tui/src/hooks.rs +++ b/packages/tui/src/hooks.rs @@ -287,7 +287,10 @@ impl InnerInputState { fn prepare_mouse_data(mouse_data: &MouseData, layout: &Layout) -> MouseData { let Point { x, y } = layout.location; - let node_origin = ClientPoint::new(x.into(), y.into()); + let node_origin = ClientPoint::new( + layout_to_screen_space(x).into(), + layout_to_screen_space(y).into(), + ); let new_client_coordinates = (mouse_data.client_coordinates() - node_origin) .to_point() diff --git a/packages/tui/src/layout.rs b/packages/tui/src/layout.rs index d863f13d8..ba50b59a6 100644 --- a/packages/tui/src/layout.rs +++ b/packages/tui/src/layout.rs @@ -7,7 +7,7 @@ use dioxus_native_core::state::ChildDepState; use dioxus_native_core_macro::sorted_str_slice; use taffy::prelude::*; -use crate::screen_to_layout_space; +use crate::{screen_to_layout_space, unit_to_layout_space}; #[derive(Debug, Clone, Copy, PartialEq)] pub(crate) enum PossiblyUninitalized { @@ -105,6 +105,59 @@ impl ChildDepState for TaffyLayout { child_layout.push(l.node.unwrap()); } + fn scale_dimention(d: Dimension) -> Dimension { + match d { + Dimension::Points(p) => Dimension::Points(unit_to_layout_space(p)), + Dimension::Percent(p) => Dimension::Percent(p), + Dimension::Auto => Dimension::Auto, + Dimension::Undefined => Dimension::Undefined, + } + } + let style = Style { + position: Rect { + left: scale_dimention(style.position.left), + right: scale_dimention(style.position.right), + top: scale_dimention(style.position.top), + bottom: scale_dimention(style.position.bottom), + }, + margin: Rect { + left: scale_dimention(style.margin.left), + right: scale_dimention(style.margin.right), + top: scale_dimention(style.margin.top), + bottom: scale_dimention(style.margin.bottom), + }, + padding: Rect { + left: scale_dimention(style.padding.left), + right: scale_dimention(style.padding.right), + top: scale_dimention(style.padding.top), + bottom: scale_dimention(style.padding.bottom), + }, + border: Rect { + left: scale_dimention(style.border.left), + right: scale_dimention(style.border.right), + top: scale_dimention(style.border.top), + bottom: scale_dimention(style.border.bottom), + }, + gap: Size { + width: scale_dimention(style.gap.width), + height: scale_dimention(style.gap.height), + }, + flex_basis: scale_dimention(style.flex_basis), + size: Size { + width: scale_dimention(style.size.width), + height: scale_dimention(style.size.height), + }, + min_size: Size { + width: scale_dimention(style.min_size.width), + height: scale_dimention(style.min_size.height), + }, + max_size: Size { + width: scale_dimention(style.max_size.width), + height: scale_dimention(style.max_size.height), + }, + ..style + }; + if let PossiblyUninitalized::Initialized(n) = self.node { if self.style != style { taffy.set_style(n, style).unwrap(); diff --git a/packages/tui/src/lib.rs b/packages/tui/src/lib.rs index 38638f7c5..7b4f72ce3 100644 --- a/packages/tui/src/lib.rs +++ b/packages/tui/src/lib.rs @@ -1,5 +1,6 @@ use anyhow::Result; use crossterm::{ + cursor::{MoveTo, RestorePosition, SavePosition, Show}, event::{DisableMouseCapture, EnableMouseCapture, Event as TermEvent, KeyCode, KeyModifiers}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, @@ -28,11 +29,13 @@ mod focus; mod hooks; mod layout; mod node; +pub mod prelude; pub mod query; mod render; mod style; mod style_attributes; mod widget; +mod widgets; pub use config::*; pub use hooks::*; @@ -43,6 +46,10 @@ pub(crate) fn screen_to_layout_space(screen: u16) -> f32 { screen as f32 * 10.0 } +pub(crate) fn unit_to_layout_space(screen: f32) -> f32 { + screen * 10.0 +} + pub(crate) fn layout_to_screen_space(layout: f32) -> f32 { layout / 10.0 } @@ -136,7 +143,13 @@ fn render_vdom( let mut terminal = (!cfg.headless).then(|| { enable_raw_mode().unwrap(); let mut stdout = std::io::stdout(); - execute!(stdout, EnterAlternateScreen, EnableMouseCapture).unwrap(); + execute!( + stdout, + EnterAlternateScreen, + EnableMouseCapture, + MoveTo(0, 1000) + ) + .unwrap(); let backend = CrosstermBackend::new(io::stdout()); Terminal::new(backend).unwrap() }); @@ -181,14 +194,16 @@ fn render_vdom( taffy.compute_layout(root_node, size).unwrap(); } if let Some(terminal) = &mut terminal { + execute!(terminal.backend_mut(), SavePosition).unwrap(); terminal.draw(|frame| { let rdom = rdom.borrow(); let mut taffy = taffy.lock().expect("taffy lock poisoned"); // size is guaranteed to not change when rendering - resize(frame.size(), &mut *taffy, &rdom); + resize(frame.size(), &mut taffy, &rdom); let root = &rdom[NodeId(0)]; - render::render_vnode(frame, &*taffy, &rdom, root, cfg, Point::ZERO); + render::render_vnode(frame, &taffy, &rdom, root, cfg, Point::ZERO); })?; + execute!(terminal.backend_mut(), RestorePosition, Show).unwrap(); } else { let rdom = rdom.borrow(); resize( diff --git a/packages/tui/src/prelude/mod.rs b/packages/tui/src/prelude/mod.rs new file mode 100644 index 000000000..2155f7707 --- /dev/null +++ b/packages/tui/src/prelude/mod.rs @@ -0,0 +1 @@ +pub use crate::widgets::*; diff --git a/packages/tui/src/query.rs b/packages/tui/src/query.rs index 64da87650..c052ad9ea 100644 --- a/packages/tui/src/query.rs +++ b/packages/tui/src/query.rs @@ -89,7 +89,7 @@ impl<'a> ElementRef<'a> { .ok(); layout.map(|layout| Layout { order: layout.order, - size: layout.size.map(|v| layout_to_screen_space(v)), + size: layout.size.map(layout_to_screen_space), location: Point { x: layout_to_screen_space(layout.location.x), y: layout_to_screen_space(layout.location.y), diff --git a/packages/tui/src/render.rs b/packages/tui/src/render.rs index ef18f2989..e7de5eaac 100644 --- a/packages/tui/src/render.rs +++ b/packages/tui/src/render.rs @@ -41,7 +41,7 @@ pub(crate) fn render_vnode( let x = layout_to_screen_space(fx).round() as u16; let y = layout_to_screen_space(fy).round() as u16; let Size { width, height } = *size; - let width = layout_to_screen_space(fx + width).round() as u16 + x; + let width = layout_to_screen_space(fx + width).round() as u16 - x; let height = layout_to_screen_space(fy + height).round() as u16 - y; match &node.node_data.node_type { diff --git a/packages/tui/src/widgets/button.rs b/packages/tui/src/widgets/button.rs new file mode 100644 index 000000000..f3ebc7901 --- /dev/null +++ b/packages/tui/src/widgets/button.rs @@ -0,0 +1,59 @@ +use std::collections::HashMap; + +use dioxus::prelude::*; +use dioxus_elements::input_data::keyboard_types::Key; +use dioxus_html as dioxus_elements; +use dioxus_html::FormData; + +#[derive(Props)] +pub(crate) struct ButtonProps<'a> { + #[props(!optional)] + raw_onclick: Option<&'a EventHandler<'a, FormData>>, + #[props(!optional)] + value: Option<&'a str>, + #[props(!optional)] + width: Option<&'a str>, + #[props(!optional)] + height: Option<&'a str>, +} + +#[allow(non_snake_case)] +pub(crate) fn Button<'a>(cx: Scope<'a, ButtonProps>) -> Element<'a> { + let state = use_state(cx, || false); + let width = cx.props.width.unwrap_or("1px"); + let height = cx.props.height.unwrap_or("1px"); + + let single_char = width == "1px" || height == "1px"; + let text = if let Some(v) = cx.props.value { v } else { "" }; + let border_style = if single_char { "none" } else { "solid" }; + let update = || { + let new_state = !state.get(); + if let Some(callback) = cx.props.raw_onclick { + callback.call(FormData { + value: text.to_string(), + values: HashMap::new(), + files: None, + }); + } + state.set(new_state); + }; + cx.render(rsx! { + div{ + width: "{width}", + height: "{height}", + border_style: "{border_style}", + flex_direction: "row", + align_items: "center", + justify_content: "center", + onclick: move |_| { + update(); + }, + onkeydown: move |evt|{ + if !evt.is_auto_repeating() && match evt.key(){ Key::Character(c) if c == " " =>true, Key::Enter=>true, _=>false } { + update(); + } + }, + "{text}" + } + }) +} diff --git a/packages/tui/src/widgets/checkbox.rs b/packages/tui/src/widgets/checkbox.rs new file mode 100644 index 000000000..483117212 --- /dev/null +++ b/packages/tui/src/widgets/checkbox.rs @@ -0,0 +1,82 @@ +use std::collections::HashMap; + +use dioxus::prelude::*; +use dioxus_elements::input_data::keyboard_types::Key; +use dioxus_html as dioxus_elements; +use dioxus_html::FormData; + +#[derive(Props)] +pub(crate) struct CheckBoxProps<'a> { + #[props(!optional)] + raw_oninput: Option<&'a EventHandler<'a, FormData>>, + #[props(!optional)] + value: Option<&'a str>, + #[props(!optional)] + width: Option<&'a str>, + #[props(!optional)] + height: Option<&'a str>, + #[props(!optional)] + checked: Option<&'a str>, +} + +#[allow(non_snake_case)] +pub(crate) fn CheckBox<'a>(cx: Scope<'a, CheckBoxProps>) -> Element<'a> { + let state = use_state(cx, || cx.props.checked.filter(|&c| c == "true").is_some()); + let width = cx.props.width.unwrap_or("1px"); + let height = cx.props.height.unwrap_or("1px"); + + let single_char = width == "1px" && height == "1px"; + let text = if single_char { + if *state.get() { + "☑" + } else { + "☐" + } + } else if *state.get() { + "✓" + } else { + " " + }; + let border_style = if width == "1px" || height == "1px" { + "none" + } else { + "solid" + }; + let update = move || { + let new_state = !state.get(); + if let Some(callback) = cx.props.raw_oninput { + callback.call(FormData { + value: if let Some(value) = &cx.props.value { + if new_state { + value.to_string() + } else { + String::new() + } + } else { + "on".to_string() + }, + values: HashMap::new(), + files: None, + }); + } + state.set(new_state); + }; + cx.render(rsx! { + div { + width: "{width}", + height: "{height}", + border_style: "{border_style}", + align_items: "center", + justify_content: "center", + onclick: move |_| { + update(); + }, + onkeydown: move |evt| { + if !evt.is_auto_repeating() && match evt.key(){ Key::Character(c) if c == " " =>true, Key::Enter=>true, _=>false } { + update(); + } + }, + "{text}" + } + }) +} diff --git a/packages/tui/src/widgets/input.rs b/packages/tui/src/widgets/input.rs new file mode 100644 index 000000000..2294b0f10 --- /dev/null +++ b/packages/tui/src/widgets/input.rs @@ -0,0 +1,102 @@ +use dioxus::prelude::*; +use dioxus_core::prelude::fc_to_builder; +use dioxus_html::FormData; + +use crate::widgets::button::Button; +use crate::widgets::checkbox::CheckBox; +use crate::widgets::number::NumbericInput; +use crate::widgets::password::Password; +use crate::widgets::slider::Slider; +use crate::widgets::textbox::TextBox; + +#[derive(Props)] +pub struct InputProps<'a> { + r#type: Option<&'static str>, + oninput: Option>, + onclick: Option>, + value: Option<&'a str>, + size: Option<&'a str>, + maxlength: Option<&'a str>, + width: Option<&'a str>, + height: Option<&'a str>, + min: Option<&'a str>, + max: Option<&'a str>, + step: Option<&'a str>, + checked: Option<&'a str>, +} + +#[allow(non_snake_case)] +pub fn Input<'a>(cx: Scope<'a, InputProps<'a>>) -> Element<'a> { + cx.render(match cx.props.r#type { + Some("checkbox") => { + rsx! { + CheckBox{ + raw_oninput: cx.props.oninput.as_ref(), + value: cx.props.value, + width: cx.props.width, + height: cx.props.height, + checked: cx.props.checked, + } + } + } + Some("range") => { + rsx! { + Slider{ + raw_oninput: cx.props.oninput.as_ref(), + value: cx.props.value, + width: cx.props.width, + height: cx.props.height, + max: cx.props.max, + min: cx.props.min, + step: cx.props.step, + } + } + } + Some("button") => { + rsx! { + Button{ + raw_onclick: cx.props.onclick.as_ref(), + value: cx.props.value, + width: cx.props.width, + height: cx.props.height, + } + } + } + Some("number") => { + rsx! { + NumbericInput{ + raw_oninput: cx.props.oninput.as_ref(), + value: cx.props.value, + size: cx.props.size, + max_length: cx.props.maxlength, + width: cx.props.width, + height: cx.props.height, + } + } + } + Some("password") => { + rsx! { + Password{ + raw_oninput: cx.props.oninput.as_ref(), + value: cx.props.value, + size: cx.props.size, + max_length: cx.props.maxlength, + width: cx.props.width, + height: cx.props.height, + } + } + } + _ => { + rsx! { + TextBox{ + raw_oninput: cx.props.oninput.as_ref(), + value: cx.props.value, + size: cx.props.size, + max_length: cx.props.maxlength, + width: cx.props.width, + height: cx.props.height, + } + } + } + }) +} diff --git a/packages/tui/src/widgets/mod.rs b/packages/tui/src/widgets/mod.rs new file mode 100644 index 000000000..916295e50 --- /dev/null +++ b/packages/tui/src/widgets/mod.rs @@ -0,0 +1,18 @@ +mod button; +mod checkbox; +mod input; +mod number; +mod password; +mod slider; +mod textbox; + +use dioxus_core::{ElementId, RenderReturn, Scope}; +pub use input::*; + +pub(crate) fn get_root_id(cx: Scope) -> Option { + if let RenderReturn::Sync(Ok(sync)) = cx.root_node() { + sync.root_ids.get(0).map(|id| id.get()) + } else { + None + } +} diff --git a/packages/tui/src/widgets/number.rs b/packages/tui/src/widgets/number.rs new file mode 100644 index 000000000..05cb2d678 --- /dev/null +++ b/packages/tui/src/widgets/number.rs @@ -0,0 +1,209 @@ +use crate::widgets::get_root_id; +use crate::Query; +use crossterm::{cursor::MoveTo, execute}; +use dioxus::prelude::*; +use dioxus_elements::input_data::keyboard_types::Key; +use dioxus_html as dioxus_elements; +use dioxus_html::FormData; +use dioxus_native_core::utils::cursor::{Cursor, Pos}; +use std::{collections::HashMap, io::stdout}; +use taffy::geometry::Point; + +#[derive(Props)] +pub(crate) struct NumbericInputProps<'a> { + #[props(!optional)] + raw_oninput: Option<&'a EventHandler<'a, FormData>>, + #[props(!optional)] + value: Option<&'a str>, + #[props(!optional)] + size: Option<&'a str>, + #[props(!optional)] + max_length: Option<&'a str>, + #[props(!optional)] + width: Option<&'a str>, + #[props(!optional)] + height: Option<&'a str>, +} +#[allow(non_snake_case)] +pub(crate) fn NumbericInput<'a>(cx: Scope<'a, NumbericInputProps>) -> Element<'a> { + let tui_query: Query = cx.consume_context().unwrap(); + let tui_query_clone = tui_query.clone(); + + let text_ref = use_ref(cx, || { + if let Some(intial_text) = cx.props.value { + intial_text.to_string() + } else { + String::new() + } + }); + let cursor = use_ref(cx, Cursor::default); + let dragging = use_state(cx, || false); + + let text = text_ref.read().clone(); + let start_highlight = cursor.read().first().idx(&text); + let end_highlight = cursor.read().last().idx(&text); + let (text_before_first_cursor, text_after_first_cursor) = text.split_at(start_highlight); + let (text_highlighted, text_after_second_cursor) = + text_after_first_cursor.split_at(end_highlight - start_highlight); + + let max_len = cx + .props + .max_length + .as_ref() + .and_then(|s| s.parse().ok()) + .unwrap_or(usize::MAX); + + let width = cx + .props + .width + .map(|s| s.to_string()) + // px is the same as em in tui + .or_else(|| cx.props.size.map(|s| s.to_string() + "px")) + .unwrap_or_else(|| "10px".to_string()); + let height = cx.props.height.unwrap_or("3px"); + + // don't draw a border unless there is enough space + let border = if width + .strip_suffix("px") + .and_then(|w| w.parse::().ok()) + .filter(|w| *w < 3) + .is_some() + || height + .strip_suffix("px") + .and_then(|h| h.parse::().ok()) + .filter(|h| *h < 3) + .is_some() + { + "none" + } else { + "solid" + }; + + let update = |text: String| { + if let Some(input_handler) = &cx.props.raw_oninput { + input_handler.call(FormData { + value: text, + values: HashMap::new(), + files: None, + }); + } + }; + let increase = move || { + let mut text = text_ref.write(); + *text = (text.parse::().unwrap_or(0.0) + 1.0).to_string(); + update(text.clone()); + }; + let decrease = move || { + let mut text = text_ref.write(); + *text = (text.parse::().unwrap_or(0.0) - 1.0).to_string(); + update(text.clone()); + }; + + render! { + div{ + width: "{width}", + height: "{height}", + border_style: "{border}", + + onkeydown: move |k| { + let is_text = match k.key(){ + Key::ArrowLeft | Key::ArrowRight | Key::Backspace => true, + Key::Character(c) if c=="." || c== "-" || c.chars().all(|c|c.is_numeric())=> true, + _ => false, + }; + if is_text{ + let mut text = text_ref.write(); + cursor.write().handle_input(&k, &mut text, max_len); + update(text.clone()); + + let node = tui_query.get(get_root_id(cx).unwrap()); + let Point{ x, y } = node.pos().unwrap(); + + let Pos { col, row } = cursor.read().start; + let (x, y) = (col as u16 + x as u16 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1}); + if let Ok(pos) = crossterm::cursor::position() { + if pos != (x, y){ + execute!(stdout(), MoveTo(x, y)).unwrap(); + } + } + else{ + execute!(stdout(), MoveTo(x, y)).unwrap(); + } + } + else{ + match k.key() { + Key::ArrowUp =>{ + increase(); + } + Key::ArrowDown =>{ + decrease(); + } + _ => () + } + } + }, + onmousemove: move |evt| { + if *dragging.get() { + let offset = evt.data.element_coordinates(); + let mut new = Pos::new(offset.x as usize, offset.y as usize); + if border != "none" { + new.col = new.col.saturating_sub(1); + } + // textboxs are only one line tall + new.row = 0; + + if new != cursor.read().start { + cursor.write().end = Some(new); + } + } + }, + onmousedown: move |evt| { + let offset = evt.data.element_coordinates(); + let mut new = Pos::new(offset.x as usize, offset.y as usize); + if border != "none" { + new.col = new.col.saturating_sub(1); + } + new.row = 0; + + new.realize_col(&text_ref.read()); + cursor.set(Cursor::from_start(new)); + dragging.set(true); + let node = tui_query_clone.get(get_root_id(cx).unwrap()); + let Point{ x, y } = node.pos().unwrap(); + + let Pos { col, row } = cursor.read().start; + let (x, y) = (col as u16 + x as u16 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1}); + if let Ok(pos) = crossterm::cursor::position() { + if pos != (x, y){ + execute!(stdout(), MoveTo(x, y)).unwrap(); + } + } + else{ + execute!(stdout(), MoveTo(x, y)).unwrap(); + } + }, + onmouseup: move |_| { + dragging.set(false); + }, + onmouseleave: move |_| { + dragging.set(false); + }, + onmouseenter: move |_| { + dragging.set(false); + }, + onfocusout: |_| { + execute!(stdout(), MoveTo(0, 1000)).unwrap(); + }, + + "{text_before_first_cursor}" + + span{ + background_color: "rgba(255, 255, 255, 50%)", + + "{text_highlighted}" + } + + "{text_after_second_cursor}" + } + } +} diff --git a/packages/tui/src/widgets/password.rs b/packages/tui/src/widgets/password.rs new file mode 100644 index 000000000..7f8455d71 --- /dev/null +++ b/packages/tui/src/widgets/password.rs @@ -0,0 +1,186 @@ +use crate::widgets::get_root_id; +use crate::Query; +use crossterm::{cursor::*, execute}; +use dioxus::prelude::*; +use dioxus_elements::input_data::keyboard_types::Key; +use dioxus_html as dioxus_elements; +use dioxus_html::FormData; +use dioxus_native_core::utils::cursor::{Cursor, Pos}; +use std::{collections::HashMap, io::stdout}; +use taffy::geometry::Point; + +#[derive(Props)] +pub(crate) struct PasswordProps<'a> { + #[props(!optional)] + raw_oninput: Option<&'a EventHandler<'a, FormData>>, + #[props(!optional)] + value: Option<&'a str>, + #[props(!optional)] + size: Option<&'a str>, + #[props(!optional)] + max_length: Option<&'a str>, + #[props(!optional)] + width: Option<&'a str>, + #[props(!optional)] + height: Option<&'a str>, +} +#[allow(non_snake_case)] +pub(crate) fn Password<'a>(cx: Scope<'a, PasswordProps>) -> Element<'a> { + let tui_query: Query = cx.consume_context().unwrap(); + let tui_query_clone = tui_query.clone(); + + let text_ref = use_ref(cx, || { + if let Some(intial_text) = cx.props.value { + intial_text.to_string() + } else { + String::new() + } + }); + let cursor = use_ref(cx, Cursor::default); + let dragging = use_state(cx, || false); + + let text = text_ref.read().clone(); + let start_highlight = cursor.read().first().idx(&text); + let end_highlight = cursor.read().last().idx(&text); + let (text_before_first_cursor, text_after_first_cursor) = text.split_at(start_highlight); + let (text_highlighted, text_after_second_cursor) = + text_after_first_cursor.split_at(end_highlight - start_highlight); + + let text_before_first_cursor = ".".repeat(text_before_first_cursor.len()); + let text_highlighted = ".".repeat(text_highlighted.len()); + let text_after_second_cursor = ".".repeat(text_after_second_cursor.len()); + + let max_len = cx + .props + .max_length + .as_ref() + .and_then(|s| s.parse().ok()) + .unwrap_or(usize::MAX); + + let width = cx + .props + .width + .map(|s| s.to_string()) + // px is the same as em in tui + .or_else(|| cx.props.size.map(|s| s.to_string() + "px")) + .unwrap_or_else(|| "10px".to_string()); + let height = cx.props.height.unwrap_or("3px"); + + // don't draw a border unless there is enough space + let border = if width + .strip_suffix("px") + .and_then(|w| w.parse::().ok()) + .filter(|w| *w < 3) + .is_some() + || height + .strip_suffix("px") + .and_then(|h| h.parse::().ok()) + .filter(|h| *h < 3) + .is_some() + { + "none" + } else { + "solid" + }; + + render! { + div{ + width: "{width}", + height: "{height}", + border_style: "{border}", + + onkeydown: move |k| { + if k.key()== Key::Enter { + return; + } + let mut text = text_ref.write(); + cursor.write().handle_input(&k, &mut text, max_len); + if let Some(input_handler) = &cx.props.raw_oninput{ + input_handler.call(FormData{ + value: text.clone(), + values: HashMap::new(), + files: None + }); + } + + let node = tui_query.get(get_root_id(cx).unwrap()); + let Point{ x, y } = node.pos().unwrap(); + + let Pos { col, row } = cursor.read().start; + let (x, y) = (col as u16 + x as u16 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1}); + if let Ok(pos) = crossterm::cursor::position() { + if pos != (x, y){ + execute!(stdout(), MoveTo(x, y)).unwrap(); + } + } + else{ + execute!(stdout(), MoveTo(x, y)).unwrap(); + } + }, + + onmousemove: move |evt| { + if *dragging.get() { + let offset = evt.data.element_coordinates(); + let mut new = Pos::new(offset.x as usize, offset.y as usize); + if border != "none" { + new.col = new.col.saturating_sub(1); + } + // textboxs are only one line tall + new.row = 0; + + if new != cursor.read().start { + cursor.write().end = Some(new); + } + } + }, + onmousedown: move |evt| { + let offset = evt.data.element_coordinates(); + let mut new = Pos::new(offset.x as usize, offset.y as usize); + if border != "none" { + new.col = new.col.saturating_sub(1); + } + // textboxs are only one line tall + new.row = 0; + + new.realize_col(&text_ref.read()); + cursor.set(Cursor::from_start(new)); + dragging.set(true); + let node = tui_query_clone.get(get_root_id(cx).unwrap()); + let Point{ x, y } = node.pos().unwrap(); + + let Pos { col, row } = cursor.read().start; + let (x, y) = (col as u16 + x as u16 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1}); + if let Ok(pos) = crossterm::cursor::position() { + if pos != (x, y){ + execute!(stdout(), MoveTo(x, y)).unwrap(); + } + } + else{ + execute!(stdout(), MoveTo(x, y)).unwrap(); + } + }, + onmouseup: move |_| { + dragging.set(false); + }, + onmouseleave: move |_| { + dragging.set(false); + }, + onmouseenter: move |_| { + dragging.set(false); + }, + onfocusout: |_| { + execute!(stdout(), MoveTo(0, 1000)).unwrap(); + }, + + "{text_before_first_cursor}" + + span{ + background_color: "rgba(255, 255, 255, 50%)", + + "{text_highlighted}" + } + + "{text_after_second_cursor}" + } + } +} diff --git a/packages/tui/src/widgets/slider.rs b/packages/tui/src/widgets/slider.rs new file mode 100644 index 000000000..43f0ac77e --- /dev/null +++ b/packages/tui/src/widgets/slider.rs @@ -0,0 +1,108 @@ +use std::collections::HashMap; + +use crate::widgets::get_root_id; +use crate::Query; +use dioxus::prelude::*; +use dioxus_elements::input_data::keyboard_types::Key; +use dioxus_html as dioxus_elements; +use dioxus_html::FormData; + +#[derive(Props)] +pub(crate) struct SliderProps<'a> { + #[props(!optional)] + raw_oninput: Option<&'a EventHandler<'a, FormData>>, + #[props(!optional)] + value: Option<&'a str>, + #[props(!optional)] + width: Option<&'a str>, + #[props(!optional)] + height: Option<&'a str>, + #[props(!optional)] + min: Option<&'a str>, + #[props(!optional)] + max: Option<&'a str>, + #[props(!optional)] + step: Option<&'a str>, +} + +#[allow(non_snake_case)] +pub(crate) fn Slider<'a>(cx: Scope<'a, SliderProps>) -> Element<'a> { + let tui_query: Query = cx.consume_context().unwrap(); + + let value_state = use_state(cx, || 0.0); + let value: Option = cx.props.value.and_then(|v| v.parse().ok()); + let width = cx.props.width.unwrap_or("20px"); + let height = cx.props.height.unwrap_or("1px"); + let min = cx.props.min.and_then(|v| v.parse().ok()).unwrap_or(0.0); + let max = cx.props.max.and_then(|v| v.parse().ok()).unwrap_or(100.0); + let size = max - min; + let step = cx + .props + .step + .and_then(|v| v.parse().ok()) + .unwrap_or(size / 10.0); + + let current_value = if let Some(value) = value { + value + } else { + *value_state.get() + } + .max(min) + .min(max); + let fst_width = 100.0 * (current_value - min) / size; + let snd_width = 100.0 * (max - current_value) / size; + assert!(fst_width + snd_width > 99.0 && fst_width + snd_width < 101.0); + + let update = |value: String| { + if let Some(oninput) = cx.props.raw_oninput { + oninput.call(FormData { + value, + values: HashMap::new(), + files: None, + }); + } + }; + + render! { + div{ + width: "{width}", + height: "{height}", + display: "flex", + flex_direction: "row", + onkeydown: move |event| { + match event.key() { + Key::ArrowLeft => { + value_state.set((current_value - step).max(min).min(max)); + update(value_state.current().to_string()); + } + Key::ArrowRight => { + value_state.set((current_value + step).max(min).min(max)); + update(value_state.current().to_string()); + } + _ => () + } + }, + onmousemove: move |evt| { + let mouse = evt.data; + if !mouse.held_buttons().is_empty(){ + let node = tui_query.get(get_root_id(cx).unwrap()); + let width = node.size().unwrap().width; + let offset = mouse.element_coordinates(); + value_state.set(min + size*(offset.x as f32) / width as f32); + update(value_state.current().to_string()); + } + }, + div{ + width: "{fst_width}%", + background_color: "rgba(10,10,10,0.5)", + } + div{ + "|" + } + div{ + width: "{snd_width}%", + background_color: "rgba(10,10,10,0.5)", + } + } + } +} diff --git a/packages/tui/src/widgets/textbox.rs b/packages/tui/src/widgets/textbox.rs new file mode 100644 index 000000000..8628fca29 --- /dev/null +++ b/packages/tui/src/widgets/textbox.rs @@ -0,0 +1,182 @@ +use crate::widgets::get_root_id; +use crate::Query; +use crossterm::{cursor::*, execute}; +use dioxus::prelude::*; +use dioxus_elements::input_data::keyboard_types::Key; +use dioxus_html as dioxus_elements; +use dioxus_html::FormData; +use dioxus_native_core::utils::cursor::{Cursor, Pos}; +use std::{collections::HashMap, io::stdout}; +use taffy::geometry::Point; + +#[derive(Props)] +pub(crate) struct TextBoxProps<'a> { + #[props(!optional)] + raw_oninput: Option<&'a EventHandler<'a, FormData>>, + #[props(!optional)] + value: Option<&'a str>, + #[props(!optional)] + size: Option<&'a str>, + #[props(!optional)] + max_length: Option<&'a str>, + #[props(!optional)] + width: Option<&'a str>, + #[props(!optional)] + height: Option<&'a str>, +} +#[allow(non_snake_case)] +pub(crate) fn TextBox<'a>(cx: Scope<'a, TextBoxProps>) -> Element<'a> { + let tui_query: Query = cx.consume_context().unwrap(); + let tui_query_clone = tui_query.clone(); + + let text_ref = use_ref(cx, || { + if let Some(intial_text) = cx.props.value { + intial_text.to_string() + } else { + String::new() + } + }); + let cursor = use_ref(cx, Cursor::default); + let dragging = use_state(cx, || false); + + let text = text_ref.read().clone(); + let start_highlight = cursor.read().first().idx(&text); + let end_highlight = cursor.read().last().idx(&text); + let (text_before_first_cursor, text_after_first_cursor) = text.split_at(start_highlight); + let (text_highlighted, text_after_second_cursor) = + text_after_first_cursor.split_at(end_highlight - start_highlight); + + let max_len = cx + .props + .max_length + .as_ref() + .and_then(|s| s.parse().ok()) + .unwrap_or(usize::MAX); + + let width = cx + .props + .width + .map(|s| s.to_string()) + // px is the same as em in tui + .or_else(|| cx.props.size.map(|s| s.to_string() + "px")) + .unwrap_or_else(|| "10px".to_string()); + let height = cx.props.height.unwrap_or("3px"); + + // don't draw a border unless there is enough space + let border = if width + .strip_suffix("px") + .and_then(|w| w.parse::().ok()) + .filter(|w| *w < 3) + .is_some() + || height + .strip_suffix("px") + .and_then(|h| h.parse::().ok()) + .filter(|h| *h < 3) + .is_some() + { + "none" + } else { + "solid" + }; + + render! { + div{ + width: "{width}", + height: "{height}", + border_style: "{border}", + + onkeydown: move |k| { + if k.key() == Key::Enter { + return; + } + let mut text = text_ref.write(); + cursor.write().handle_input(&k, &mut text, max_len); + if let Some(input_handler) = &cx.props.raw_oninput{ + input_handler.call(FormData{ + value: text.clone(), + values: HashMap::new(), + files: None + }); + } + + let node = tui_query.get(get_root_id(cx).unwrap()); + let Point{ x, y } = node.pos().unwrap(); + + let Pos { col, row } = cursor.read().start; + let (x, y) = (col as u16 + x as u16 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1}); + if let Ok(pos) = crossterm::cursor::position() { + if pos != (x, y){ + execute!(stdout(), MoveTo(x, y)).unwrap(); + } + } + else{ + execute!(stdout(), MoveTo(x, y)).unwrap(); + } + }, + + onmousemove: move |evt| { + if *dragging.get() { + let offset = evt.data.element_coordinates(); + let mut new = Pos::new(offset.x as usize, offset.y as usize); + if border != "none" { + new.col = new.col.saturating_sub(1); + } + // textboxs are only one line tall + new.row = 0; + + if new != cursor.read().start { + cursor.write().end = Some(new); + } + } + }, + onmousedown: move |evt| { + let offset = evt.data.element_coordinates(); + let mut new = Pos::new(offset.x as usize, offset.y as usize); + if border != "none" { + new.col = new.col.saturating_sub(1); + } + // textboxs are only one line tall + new.row = 0; + + new.realize_col(&text_ref.read()); + cursor.set(Cursor::from_start(new)); + dragging.set(true); + let node = tui_query_clone.get(get_root_id(cx).unwrap()); + let Point{ x, y } = node.pos().unwrap(); + + let Pos { col, row } = cursor.read().start; + let (x, y) = (col as u16 + x as u16 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1}); + if let Ok(pos) = crossterm::cursor::position() { + if pos != (x, y){ + execute!(stdout(), MoveTo(x, y)).unwrap(); + } + } + else{ + execute!(stdout(), MoveTo(x, y)).unwrap(); + } + }, + onmouseup: move |_| { + dragging.set(false); + }, + onmouseleave: move |_| { + dragging.set(false); + }, + onmouseenter: move |_| { + dragging.set(false); + }, + onfocusout: |_| { + execute!(stdout(), MoveTo(0, 1000)).unwrap(); + }, + + "{text_before_first_cursor}" + + span{ + background_color: "rgba(255, 255, 255, 50%)", + + "{text_highlighted}" + } + + "{text_after_second_cursor}" + } + } +} 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) + } +}