diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 8bf67b3d0..a738faeeb 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -105,11 +105,11 @@ use DomEdit::*; /// /// Funnily enough, this stack machine's entire job is to create instructions for another stack machine to execute. It's /// stack machines all the way down! -pub struct DiffState<'bump> { - scopes: &'bump ScopeArena, - pub mutations: Mutations<'bump>, +pub(crate) struct DiffState<'bump> { + pub(crate) scopes: &'bump ScopeArena, + pub(crate) mutations: Mutations<'bump>, pub(crate) stack: DiffStack<'bump>, - pub force_diff: bool, + pub(crate) force_diff: bool, } impl<'bump> DiffState<'bump> { @@ -149,7 +149,7 @@ pub(crate) enum DiffInstruction<'a> { } #[derive(Debug, Clone, Copy)] -pub enum MountType<'a> { +pub(crate) enum MountType<'a> { Absorb, Append, Replace { old: &'a VNode<'a> }, @@ -159,9 +159,9 @@ pub enum MountType<'a> { pub(crate) struct DiffStack<'bump> { pub(crate) instructions: Vec>, - nodes_created_stack: SmallVec<[usize; 10]>, - pub scope_stack: SmallVec<[ScopeId; 5]>, - pub element_stack: SmallVec<[ElementId; 10]>, + pub(crate) nodes_created_stack: SmallVec<[usize; 10]>, + pub(crate) scope_stack: SmallVec<[ScopeId; 5]>, + pub(crate) element_stack: SmallVec<[ElementId; 10]>, } impl<'bump> DiffStack<'bump> { diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 20f62e89f..ce9ad0b2b 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -1,17 +1,6 @@ #![allow(non_snake_case)] #![doc = include_str!("../README.md")] -/* -Navigating this crate: -- virtual_dom: the primary entrypoint for the crate -- scheduler: the core interior logic called by the [`VirtualDom`] -- nodes: the definition of VNodes, listeners, etc. -- diff: the stackmachine-based diffing algorithm -- hooks: foundational hooks that require crate-private APIs -- mutations: DomEdits/NodeRefs and internal API to create them - -Some utilities -*/ pub(crate) mod component; pub(crate) mod diff; pub(crate) mod lazynodes; @@ -23,7 +12,7 @@ pub(crate) mod virtual_dom; pub(crate) mod innerlude { pub use crate::component::*; - pub use crate::diff::*; + pub(crate) use crate::diff::*; pub use crate::lazynodes::*; pub use crate::mutations::*; pub use crate::nodes::*; @@ -37,7 +26,7 @@ pub(crate) mod innerlude { pub use crate::innerlude::{ Attribute, Component, Context, DioxusElement, DomEdit, Element, ElementId, EventHandler, - EventPriority, IntoVNode, LazyNodes, Listener, MountType, Mutations, NodeFactory, Properties, + EventPriority, IntoVNode, LazyNodes, Listener, Mutations, NodeFactory, Properties, SchedulerMsg, ScopeId, UserEvent, VElement, VFragment, VNode, VirtualDom, }; diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index e7c54cdca..862a42df4 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -16,7 +16,6 @@ pub struct Mutations<'a> { pub edits: Vec>, pub dirty_scopes: FxHashSet, pub refs: Vec>, - pub effects: Vec<&'a dyn FnMut()>, } impl Debug for Mutations<'_> { @@ -113,7 +112,6 @@ impl<'a> Mutations<'a> { edits: Vec::new(), refs: Vec::new(), dirty_scopes: Default::default(), - effects: Vec::new(), } } diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index cd7dd22b1..9f30019d1 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -7,6 +7,7 @@ use std::{ cell::{Cell, RefCell}, collections::HashMap, future::Future, + pin::Pin, rc::Rc, }; @@ -32,6 +33,14 @@ use bumpalo::{boxed::Box as BumpBox, Bump}; /// ``` pub type Context<'a> = &'a Scope; +/// A component's unique identifier. +/// +/// `ScopeId` is a `usize` that is unique across the entire VirtualDOM and across time. ScopeIDs will never be reused +/// once a component has been unmounted. +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] +pub struct ScopeId(pub usize); + /// Every component in Dioxus is represented by a `Scope`. /// /// Scopes contain the state for hooks, the component's props, and other lifecycle information. @@ -44,7 +53,6 @@ pub type Context<'a> = &'a Scope; pub struct Scope { pub(crate) parent_scope: Option<*mut Scope>, - // parent element I think? pub(crate) container: ElementId, pub(crate) our_arena_idx: ScopeId, @@ -75,17 +83,9 @@ pub struct Scope { pub struct SelfReferentialItems<'a> { pub(crate) listeners: Vec<&'a Listener<'a>>, pub(crate) borrowed_props: Vec<&'a VComponent<'a>>, - pub(crate) tasks: Vec>>, + pub(crate) tasks: Vec>>>, } -/// A component's unique identifier. -/// -/// `ScopeId` is a `usize` that is unique across the entire VirtualDOM and across time. ScopeIDs will never be reused -/// once a component has been unmounted. -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] -pub struct ScopeId(pub usize); - // Public methods exposed to libraries and components impl Scope { /// Get the subtree ID that this scope belongs to. @@ -227,11 +227,11 @@ impl Scope { let _ = self.sender.unbounded_send(SchedulerMsg::Immediate(id)); } - /// Get the [`ScopeId`] of a mounted component. - /// - /// `ScopeId` is not unique for the lifetime of the VirtualDom - a ScopeId will be reused if a component is unmounted. - pub fn bump(&self) -> &Bump { - &self.wip_frame().bump + /// Get the Root Node of this scope + pub fn root_node(&self) -> &VNode { + todo!("Portals have changed how we address nodes. Still fixing this, sorry."); + // let node = *self.wip_frame().nodes.borrow().get(0).unwrap(); + // unsafe { std::mem::transmute(&*node) } } /// This method enables the ability to expose state to children further down the VirtualDOM Tree. @@ -287,15 +287,10 @@ impl Scope { /// Pushes the future onto the poll queue to be polled after the component renders. /// - /// - /// - /// /// The future is forcibly dropped if the component is not ready by the next render - pub fn push_task<'src, F: Future>( - &'src self, - fut: impl FnOnce() -> F + 'src, - ) -> usize + pub fn push_task<'src, F>(&'src self, fut: impl FnOnce() -> F + 'src) -> usize where + F: Future, F::Output: 'src, F: 'src, { @@ -303,18 +298,20 @@ impl Scope { .unbounded_send(SchedulerMsg::NewTask(self.our_arena_idx)) .unwrap(); - // allocate the future - let fut = fut(); - let fut: &mut dyn Future = self.bump().alloc(fut); - // wrap it in a type that will actually drop the contents + // + // Safety: we just made the pointer above and will promise not to alias it! + // The main reason we do this through from_raw is because Bumpalo's box does + // not support unsized coercion + let fut: &mut dyn Future = self.bump().alloc(fut()); let boxed_fut: BumpBox> = unsafe { BumpBox::from_raw(fut) }; + let pinned_fut: Pin> = boxed_fut.into(); // erase the 'src lifetime for self-referential storage - let self_ref_fut = unsafe { std::mem::transmute(boxed_fut) }; + let self_ref_fut = unsafe { std::mem::transmute(pinned_fut) }; + // Push the future into the tasks let mut items = self.items.borrow_mut(); - items.tasks.push(self_ref_fut); items.tasks.len() - 1 } @@ -335,22 +332,19 @@ impl Scope { /// } ///``` pub fn render<'src>(&'src self, rsx: Option>) -> Option { - let frame = self.wip_frame(); - let bump = &frame.bump; - let factory = NodeFactory { bump }; - let node = rsx.map(|f| f.call(factory))?; - let node = bump.alloc(node); + let bump = &self.wip_frame().bump; - let node_ptr = node as *mut _; - let node_ptr = unsafe { std::mem::transmute(node_ptr) }; + let owned_node: VNode<'src> = rsx.map(|f| f.call(NodeFactory { bump }))?; + let alloced_vnode: &'src mut VNode<'src> = bump.alloc(owned_node); + let node_ptr: *mut VNode<'src> = alloced_vnode as *mut _; - let link = VPortal { + let node: *mut VNode<'static> = unsafe { std::mem::transmute(node_ptr) }; + + Some(VPortal { scope_id: Cell::new(Some(self.our_arena_idx)), link_idx: Cell::new(0), - node: node_ptr, - }; - - Some(link) + node, + }) } /// Store a value between renders @@ -380,12 +374,12 @@ impl Scope { runner: impl FnOnce(&'src mut State) -> Output, ) -> Output { let mut vals = self.hook_vals.borrow_mut(); + let hook_len = vals.len(); let cur_idx = self.hook_idx.get(); if cur_idx >= hook_len { - let val = self.hook_arena.alloc(initializer(hook_len)); - vals.push(val); + vals.push(self.hook_arena.alloc(initializer(hook_len))); } let state = vals @@ -416,6 +410,7 @@ impl Scope { } } + /// Mutable access to the "work in progress frame" - used to clear it pub(crate) fn wip_frame_mut(&mut self) -> &mut BumpFrame { match self.generation.get() & 1 == 0 { true => &mut self.frames[0], @@ -423,6 +418,7 @@ impl Scope { } } + /// Access to the frame where finalized nodes existed pub(crate) fn fin_frame(&self) -> &BumpFrame { match self.generation.get() & 1 == 1 { true => &self.frames[0], @@ -433,20 +429,23 @@ impl Scope { /// Reset this component's frame /// /// # Safety: + /// /// This method breaks every reference of VNodes in the current frame. + /// + /// Calling reset itself is not usually a big deal, but we consider it important + /// due to the complex safety guarantees we need to uphold. pub(crate) unsafe fn reset_wip_frame(&mut self) { - // todo: unsafecell or something - let bump = self.wip_frame_mut(); - bump.bump.reset(); + self.wip_frame_mut().bump.reset(); } + /// Cycle to the next generation pub(crate) fn cycle_frame(&self) { self.generation.set(self.generation.get() + 1); } - pub fn root_node(&self) -> &VNode { - let node = *self.wip_frame().nodes.borrow().get(0).unwrap(); - unsafe { std::mem::transmute(&*node) } + /// Get the [`Bump`] of the WIP frame. + pub(crate) fn bump(&self) -> &Bump { + &self.wip_frame().bump } } @@ -455,7 +454,7 @@ pub(crate) struct BumpFrame { pub nodes: RefCell>>, } impl BumpFrame { - pub fn new(capacity: usize) -> Self { + pub(crate) fn new(capacity: usize) -> Self { let bump = Bump::with_capacity(capacity); let node = &*bump.alloc(VText { @@ -468,7 +467,7 @@ impl BumpFrame { Self { bump, nodes } } - pub fn assign_nodelink(&self, node: &VPortal) { + pub(crate) fn assign_nodelink(&self, node: &VPortal) { let mut nodes = self.nodes.borrow_mut(); let len = nodes.len(); diff --git a/packages/core/src/scopearena.rs b/packages/core/src/scopearena.rs index bf10846a8..ae4b1b67a 100644 --- a/packages/core/src/scopearena.rs +++ b/packages/core/src/scopearena.rs @@ -92,15 +92,8 @@ impl ScopeArena { let new_scope_id = ScopeId(self.scope_counter.get()); self.scope_counter.set(self.scope_counter.get() + 1); - // log::debug!("new scope {:?} with parent {:?}", new_scope_id, container); - if let Some(old_scope) = self.free_scopes.borrow_mut().pop() { let scope = unsafe { &mut *old_scope }; - // log::debug!( - // "reusing scope {:?} as {:?}", - // scope.our_arena_idx, - // new_scope_id - // ); scope.caller = caller; scope.parent_scope = parent_scope; diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 71511e4e1..4980f15c5 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -183,6 +183,11 @@ impl VirtualDom { /// /// This is useful when the VirtualDom must be driven from outside a thread and it doesn't make sense to wait for the /// VirtualDom to be created just to retrieve its channel receiver. + /// + /// ```rust + /// let (sender, receiver) = futures_channel::mpsc::unbounded(); + /// let dom = VirtualDom::new_with_scheduler(Example, (), sender, receiver); + /// ``` pub fn new_with_props_and_scheduler( root: Component

, root_props: P, @@ -235,27 +240,55 @@ impl VirtualDom { /// # Example /// /// ```rust, ignore - /// - /// + /// let dom = VirtualDom::new(App); + /// let sender = dom.get_scheduler_channel(); /// ``` pub fn get_scheduler_channel(&self) -> futures_channel::mpsc::UnboundedSender { self.sender.clone() } + /// Add a new message to the scheduler queue directly. + /// + /// + /// This method makes it possible to send messages to the scheduler from outside the VirtualDom without having to + /// call `get_schedule_channel` and then `send`. + /// + /// # Example + /// ```rust, ignore + /// let dom = VirtualDom::new(App); + /// dom.insert_scheduler_message(SchedulerMsg::Immediate(ScopeId(0))); + /// ``` + pub fn insert_scheduler_message(&self, msg: SchedulerMsg) { + self.sender.unbounded_send(msg).unwrap() + } + /// Check if the [`VirtualDom`] has any pending updates or work to be done. /// /// # Example /// /// ```rust, ignore + /// let dom = VirtualDom::new(App); /// - /// + /// // the dom is "dirty" when it is started and must be rebuilt to get the first render + /// assert!(dom.has_any_work()); /// ``` - pub fn has_any_work(&self) -> bool { + pub fn has_work(&self) -> bool { !(self.dirty_scopes.is_empty() && self.pending_messages.is_empty()) } - /// Waits for the scheduler to have work + /// Wait for the scheduler to have any work. + /// + /// This method polls the internal future queue *and* the scheduler channel. + /// To add work to the VirtualDom, insert a message via the scheduler channel. + /// /// This lets us poll async tasks during idle periods without blocking the main thread. + /// + /// # Example + /// + /// ```rust, ignore + /// let dom = VirtualDom::new(App); + /// let sender = dom.get_scheduler_channel(); + /// ``` pub async fn wait_for_work(&mut self) { loop { if !self.dirty_scopes.is_empty() && self.pending_messages.is_empty() { @@ -390,9 +423,10 @@ impl VirtualDom { committed_mutations.push(mutations); } else { - log::debug!("Could not finish work in time"); - // leave the work in an incomplete state + // + // todo: we should store the edits and re-apply them later + // for now, we just dump the work completely (threadsafe) return committed_mutations; } } @@ -400,7 +434,7 @@ impl VirtualDom { committed_mutations } - /// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom from scratch + /// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom from scratch. /// /// The diff machine expects the RealDom's stack to be the root of the application. /// @@ -475,6 +509,15 @@ impl VirtualDom { /// Renders an `rsx` call into the Base Scope's allocator. /// /// Useful when needing to render nodes from outside the VirtualDom, such as in a test. + /// + /// ```rust + /// fn Base(cx: Context, props: &()) -> Element { + /// rsx!(cx, div {}) + /// } + /// + /// let dom = VirtualDom::new(Base); + /// let nodes = dom.render_nodes(rsx!("div")); + /// ``` pub fn render_vnodes<'a>(&'a self, lazy_nodes: Option>) -> &'a VNode<'a> { let scope = self.scopes.get_scope(&self.base_scope).unwrap(); let frame = scope.wip_frame(); @@ -485,7 +528,16 @@ impl VirtualDom { /// Renders an `rsx` call into the Base Scope's allocator. /// - /// Useful when needing to render nodes from outside the VirtualDom, such as in a test. + /// Useful when needing to render nodes from outside the VirtualDom, such as in a test. + /// + /// ```rust + /// fn Base(cx: Context, props: &()) -> Element { + /// rsx!(cx, div {}) + /// } + /// + /// let dom = VirtualDom::new(Base); + /// let nodes = dom.render_nodes(rsx!("div")); + /// ``` pub fn diff_vnodes<'a>(&'a self, old: &'a VNode<'a>, new: &'a VNode<'a>) -> Mutations<'a> { let mut machine = DiffState::new(&self.scopes); machine.stack.push(DiffInstruction::Diff { new, old }); @@ -498,6 +550,16 @@ impl VirtualDom { /// Renders an `rsx` call into the Base Scope's allocator. /// /// Useful when needing to render nodes from outside the VirtualDom, such as in a test. + /// + /// + /// ```rust + /// fn Base(cx: Context, props: &()) -> Element { + /// rsx!(cx, div {}) + /// } + /// + /// let dom = VirtualDom::new(Base); + /// let nodes = dom.render_nodes(rsx!("div")); + /// ``` pub fn create_vnodes<'a>(&'a self, left: Option>) -> Mutations<'a> { let nodes = self.render_vnodes(left); let mut machine = DiffState::new(&self.scopes); @@ -507,9 +569,19 @@ impl VirtualDom { machine.mutations } - /// Renders an `rsx` call into the Base Scope's allocator. + /// Renders an `rsx` call into the Base Scopes's arena. /// - /// Useful when needing to render nodes from outside the VirtualDom, such as in a test. + /// Useful when needing to diff two rsx! calls from outside the VirtualDom, such as in a test. + /// + /// + /// ```rust + /// fn Base(cx: Context, props: &()) -> Element { + /// rsx!(cx, div {}) + /// } + /// + /// let dom = VirtualDom::new(Base); + /// let nodes = dom.render_nodes(rsx!("div")); + /// ``` pub fn diff_lazynodes<'a>( &'a self, left: Option>, @@ -666,14 +738,7 @@ impl<'a> Future for PollTasks<'a> { // really this should just be retain_mut but that doesn't exist yet while let Some(mut task) = items.tasks.pop() { - // todo: does this make sense? - // I don't usually write futures by hand - // I think the futures neeed to be pinned using bumpbox or something - // right now, they're bump allocated so this shouldn't matter anyway - they're not going to move - let task_mut = task.as_mut(); - let pinned = unsafe { Pin::new_unchecked(task_mut) }; - - if pinned.poll(cx).is_ready() { + if task.as_mut().poll(cx).is_ready() { all_pending = false } else { unfinished_tasks.push(task); diff --git a/packages/core/tests/lifecycle.rs b/packages/core/tests/lifecycle.rs index 1ba91a1bb..1a7c4075e 100644 --- a/packages/core/tests/lifecycle.rs +++ b/packages/core/tests/lifecycle.rs @@ -69,7 +69,7 @@ fn events_generate() { let mut dom = VirtualDom::new(App); let mut channel = dom.get_scheduler_channel(); - assert!(dom.has_any_work()); + assert!(dom.has_work()); let edits = dom.rebuild(); assert_eq!( diff --git a/packages/hooks/src/lib.rs b/packages/hooks/src/lib.rs index ef5b699b6..f6a5227bc 100644 --- a/packages/hooks/src/lib.rs +++ b/packages/hooks/src/lib.rs @@ -1,6 +1,9 @@ mod usestate; pub use usestate::{use_state, AsyncUseState, UseState}; +mod usestate2; +// pub use usestate2::use_state2; + mod useref; pub use useref::*; diff --git a/packages/hooks/src/usecoroutine.rs b/packages/hooks/src/usecoroutine.rs index ecbf8b3bd..380d06284 100644 --- a/packages/hooks/src/usecoroutine.rs +++ b/packages/hooks/src/usecoroutine.rs @@ -59,6 +59,15 @@ pub struct CoroutineHandle<'a> { cx: Context<'a>, inner: &'a State, } +impl Clone for CoroutineHandle<'_> { + fn clone(&self) -> Self { + CoroutineHandle { + cx: self.cx, + inner: self.inner, + } + } +} +impl Copy for CoroutineHandle<'_> {} impl<'a> CoroutineHandle<'a> { pub fn start(&self) { diff --git a/packages/hooks/src/usestate.rs b/packages/hooks/src/usestate.rs index 853225df4..c1156f4a3 100644 --- a/packages/hooks/src/usestate.rs +++ b/packages/hooks/src/usestate.rs @@ -54,18 +54,26 @@ pub fn use_state<'a, T: 'static>( initial_state_fn: impl FnOnce() -> T, ) -> UseState<'a, T> { cx.use_hook( - move |_| UseStateInner { - current_val: initial_state_fn(), - update_callback: cx.schedule_update(), - wip: Rc::new(RefCell::new(None)), - update_scheuled: Cell::new(false), + move |_| { + let first_val = initial_state_fn(); + UseStateInner { + current_val: Rc::new(first_val), + update_callback: cx.schedule_update(), + wip: Rc::new(RefCell::new(None)), + update_scheuled: Cell::new(false), + } }, move |hook| { hook.update_scheuled.set(false); let mut new_val = hook.wip.borrow_mut(); if new_val.is_some() { - hook.current_val = new_val.take().unwrap(); + // if there's only one reference (weak or otherwise), we can just swap the values + if let Some(val) = Rc::get_mut(&mut hook.current_val) { + *val = new_val.take().unwrap(); + } else { + hook.current_val = Rc::new(new_val.take().unwrap()); + } } UseState { inner: &*hook } @@ -73,7 +81,7 @@ pub fn use_state<'a, T: 'static>( ) } struct UseStateInner { - current_val: T, + current_val: Rc, update_scheuled: Cell, update_callback: Rc, wip: Rc>>, @@ -115,6 +123,10 @@ impl<'a, T: 'static> UseState<'a, T> { &self.inner.current_val } + pub fn get_rc(&self) -> &'a Rc { + &self.inner.current_val + } + /// Get the current status of the work-in-progress data pub fn get_wip(&self) -> Ref> { self.inner.wip.borrow() @@ -140,6 +152,7 @@ impl<'a, T: 'static> UseState<'a, T> { AsyncUseState { re_render: self.inner.update_callback.clone(), wip: self.inner.wip.clone(), + inner: self.inner.current_val.clone(), } } @@ -162,14 +175,14 @@ impl<'a, T: 'static + ToOwned> UseState<'a, T> { // "get_mut" is locked behind "ToOwned" to make it explicit that cloning occurs to use this RefMut::map(self.inner.wip.borrow_mut(), |slot| { if slot.is_none() { - *slot = Some(self.inner.current_val.to_owned()); + *slot = Some(self.inner.current_val.as_ref().to_owned()); } slot.as_mut().unwrap() }) } pub fn inner(self) -> T { - self.inner.current_val.to_owned() + self.inner.current_val.as_ref().to_owned() } } @@ -256,6 +269,7 @@ impl<'a, T: 'static + Display> std::fmt::Display for UseState<'a, T> { /// A less ergonmic but still capable form of use_state that's valid for `static lifetime pub struct AsyncUseState { + inner: Rc, re_render: Rc, wip: Rc>>, } @@ -278,4 +292,11 @@ impl AsyncUseState { (self.re_render)(); *self.wip.borrow_mut() = Some(val); } + pub fn get(&self) -> &T { + self.inner.as_ref() + } + + pub fn get_rc(&self) -> &Rc { + &self.inner + } } diff --git a/packages/hooks/src/usestate2.rs b/packages/hooks/src/usestate2.rs new file mode 100644 index 000000000..df778f0f1 --- /dev/null +++ b/packages/hooks/src/usestate2.rs @@ -0,0 +1,227 @@ +use dioxus_core::prelude::Context; +use std::{ + borrow::{Borrow, BorrowMut}, + cell::{Cell, Ref, RefCell, RefMut}, + fmt::{Debug, Display}, + ops::Not, + rc::Rc, +}; + +// /// Store state between component renders! +// /// +// /// ## Dioxus equivalent of UseStateInner2, designed for Rust +// /// +// /// The Dioxus version of `UseStateInner2` is the "king daddy" of state management. It allows you to ergonomically store and +// /// modify state between component renders. When the state is updated, the component will re-render. +// /// +// /// Dioxus' use_state basically wraps a RefCell with helper methods and integrates it with the VirtualDOM update system. +// /// +// /// [`use_state`] exposes a few helper methods to modify the underlying state: +// /// - `.set(new)` allows you to override the "work in progress" value with a new value +// /// - `.get_mut()` allows you to modify the WIP value +// /// - `.get_wip()` allows you to access the WIP value +// /// - `.deref()` provides the previous value (often done implicitly, though a manual dereference with `*` might be required) +// /// +// /// Additionally, a ton of std::ops traits are implemented for the `UseStateInner2` wrapper, meaning any mutative type operations +// /// will automatically be called on the WIP value. +// /// +// /// ## Combinators +// /// +// /// On top of the methods to set/get state, `use_state` also supports fancy combinators to extend its functionality: +// /// - `.classic()` and `.split()` convert the hook into the classic React-style hook +// /// ```rust +// /// let (state, set_state) = use_state(cx, || 10).split() +// /// ``` +// /// +// /// +// /// Usage: +// /// ```ignore +// /// const Example: FC<()> = |cx, props|{ +// /// let counter = use_state(cx, || 0); +// /// let increment = |_| counter += 1; +// /// let decrement = |_| counter += 1; +// /// +// /// html! { +// ///

+// ///

"Counter: {counter}"

+// /// +// /// +// ///
+// /// } +// /// } +// /// ``` +// pub fn use_state2<'a, T: 'static>( +// cx: Context<'a>, +// initial_state_fn: impl FnOnce() -> T, +// ) -> &'a UseState2 { +// cx.use_hook( +// move |_| { +// UseState2(Rc::new(UseStateInner2 { +// current_val: initial_state_fn(), +// update_callback: cx.schedule_update(), +// wip: None, +// update_scheuled: Cell::new(false), +// })) +// }, +// move |hook: &mut UseState2| { +// { +// let r = hook.0.as_ref(); +// let mut state = r.borrow_mut(); +// state.update_scheuled.set(false); +// if state.wip.is_some() { +// state.current_val = state.wip.take().unwrap(); +// } +// } +// &*hook +// }, +// ) +// } + +// pub struct UseState2(Rc>); + +// impl ToOwned for UseState2 { +// type Owned = UseState2; +// fn to_owned(&self) -> Self::Owned { +// UseState2(self.0.clone()) +// } +// } + +// pub struct UseStateInner2 { +// current_val: T, +// update_scheuled: Cell, +// update_callback: Rc, +// wip: Option, +// } + +// impl Debug for UseStateInner2 { +// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +// write!(f, "{:?}", self.current_val) +// } +// } + +// impl UseState2 { +// /// Tell the Dioxus Scheduler that we need to be processed +// pub fn needs_update(&self) { +// if !self.update_scheuled.get() { +// self.update_scheuled.set(true); +// (self.update_callback)(); +// } +// } + +// pub fn set(&mut self, new_val: T) -> Option { +// self.needs_update(); +// ip.wip.replace(new_val) +// } + +// pub fn get(&self) -> &T { +// &self.current_val +// } +// } + +// // impl> UseState2 { +// // /// Gain mutable access to the new value. This method is only available when the value is a `ToOwned` type. +// // /// +// // /// Mutable access is derived by calling "ToOwned" (IE cloning) on the current value. +// // /// +// // /// To get a reference to the current value, use `.get()` +// // pub fn modify(&self) -> RefMut { +// // // make sure we get processed +// // self.0.needs_update(); + +// // // Bring out the new value, cloning if it we need to +// // // "get_mut" is locked behind "ToOwned" to make it explicit that cloning occurs to use this +// // RefMut::map(self.wip.borrow_mut(), |slot| { +// // if slot.is_none() { +// // *slot = Some(self.current_val.to_owned()); +// // } +// // slot.as_mut().unwrap() +// // }) +// // } + +// // pub fn inner(self) -> T { +// // self.current_val.to_owned() +// // } +// // } + +// impl std::ops::Deref for UseStateInner2 { +// type Target = T; + +// fn deref(&self) -> &Self::Target { +// self.get() +// } +// } + +// use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign}; + +// use crate::UseState; + +// impl> Add for UseStateInner2 { +// type Output = T; + +// fn add(self, rhs: T) -> Self::Output { +// self.current_val.add(rhs) +// } +// } +// impl> AddAssign for UseStateInner2 { +// fn add_assign(&mut self, rhs: T) { +// self.set(self.current_val.add(rhs)); +// } +// } +// impl> Sub for UseStateInner2 { +// type Output = T; + +// fn sub(self, rhs: T) -> Self::Output { +// self.current_val.sub(rhs) +// } +// } +// impl> SubAssign for UseStateInner2 { +// fn sub_assign(&mut self, rhs: T) { +// self.set(self.current_val.sub(rhs)); +// } +// } + +// /// MUL +// impl> Mul for UseStateInner2 { +// type Output = T; + +// fn mul(self, rhs: T) -> Self::Output { +// self.current_val.mul(rhs) +// } +// } +// impl> MulAssign for UseStateInner2 { +// fn mul_assign(&mut self, rhs: T) { +// self.set(self.current_val.mul(rhs)); +// } +// } +// /// DIV +// impl> Div for UseStateInner2 { +// type Output = T; + +// fn div(self, rhs: T) -> Self::Output { +// self.current_val.div(rhs) +// } +// } +// impl> DivAssign for UseStateInner2 { +// fn div_assign(&mut self, rhs: T) { +// self.set(self.current_val.div(rhs)); +// } +// } +// impl> PartialEq for UseStateInner2 { +// fn eq(&self, other: &V) -> bool { +// self.get() == other +// } +// } +// impl + Copy> Not for UseStateInner2 { +// type Output = O; + +// fn not(self) -> Self::Output { +// !*self.get() +// } +// } + +// // enable displaty for the handle +// impl std::fmt::Display for UseStateInner2 { +// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +// write!(f, "{}", self.current_val) +// } +// } diff --git a/packages/liveview/README.md b/packages/liveview/README.md index e06a662bc..1e29ac75a 100644 --- a/packages/liveview/README.md +++ b/packages/liveview/README.md @@ -22,3 +22,28 @@ async fn main() -> tide::Result<()> { Dioxus LiveView runs your Dioxus apps on the server + + +```rust +use soyuz::prelude::*; + +#[tokio::main] +async fn main() { + let mut app = soyuz::new(); + app.at("/app").get(websocket(handler)); + app.listen("127.0.0.1:8080").await.unwrap(); +} + +async fn order_shoes(mut req: WebsocketRequest) -> Response { + let stream = req.upgrade(); + dioxus::liveview::launch(App, stream).await; +} + +fn App(cx: Context, props: &()) -> Element { + let mut count = use_state(cx, || 0); + cx.render(rsx!( + button { onclick: move |_| count += 1, "Incr" } + button { onclick: move |_| count -= 1, "Decr" } + )) +} +``` diff --git a/packages/web/src/cache.rs b/packages/web/src/cache.rs index 61339f918..8beb58f18 100644 --- a/packages/web/src/cache.rs +++ b/packages/web/src/cache.rs @@ -1,311 +1,298 @@ -/// Wasm-bindgen has a performance option to intern commonly used phrases -/// This saves the decoding cost, making the interaction of Rust<->JS more performant. -/// We intern all the HTML tags and attributes, making most operations much faster. -/// -/// Interning takes < 1ms at the start of the app, but saves a *ton* of time later on. -/// -/// Eventually we might want to procedurally generate these strings for common words, phrases, and values. -pub(crate) fn intern_cached_strings() { - let cached_words = [ - // Important tags to dioxus - "dioxus-id", - "dioxus", - "dioxus-event-click", // todo: more events - "click", - // All the HTML Tags - "a", - "abbr", - "address", - "area", - "article", - "aside", - "audio", - "b", - "base", - "bdi", - "bdo", - "big", - "blockquote", - "body", - "br", - "button", - "canvas", - "caption", - "cite", - "code", - "col", - "colgroup", - "command", - "data", - "datalist", - "dd", - "del", - "details", - "dfn", - "dialog", - "div", - "dl", - "dt", - "em", - "embed", - "fieldset", - "figcaption", - "figure", - "footer", - "form", - "h1", - "h2", - "h3", - "h4", - "h5", - "h6", - "head", - "header", - "hr", - "html", - "i", - "iframe", - "img", - "input", - "ins", - "kbd", - "keygen", - "label", - "legend", - "li", - "link", - "main", - "map", - "mark", - "menu", - "menuitem", - "meta", - "meter", - "nav", - "noscript", - "object", - "ol", - "optgroup", - "option", - "output", - "p", - "param", - "picture", - "pre", - "progress", - "q", - "rp", - "rt", - "ruby", - "s", - "samp", - "script", - "section", - "select", - "small", - "source", - "span", - "strong", - "style", - "sub", - "summary", - "sup", - "table", - "tbody", - "td", - "textarea", - "tfoot", - "th", - "thead", - "time", - "title", - "tr", - "track", - "u", - "ul", - "var", - "video", - "wbr", - // All the event handlers - "Attribute", - "accept", - "accept-charset", - "accesskey", - "action", - "alt", - "async", - "autocomplete", - "autofocus", - "autoplay", - "charset", - "checked", - "cite", - "class", - "cols", - "colspan", - "content", - "contenteditable", - "controls", - "coords", - "data", - "data-*", - "datetime", - "default", - "defer", - "dir", - "dirname", - "disabled", - "download", - "draggable", - "enctype", - "for", - "form", - "formaction", - "headers", - "height", - "hidden", - "high", - "href", - "hreflang", - "http-equiv", - "id", - "ismap", - "kind", - "label", - "lang", - "list", - "loop", - "low", - "max", - "maxlength", - "media", - "method", - "min", - "multiple", - "muted", - "name", - "novalidate", - "onabort", - "onafterprint", - "onbeforeprint", - "onbeforeunload", - "onblur", - "oncanplay", - "oncanplaythrough", - "onchange", - "onclick", - "oncontextmenu", - "oncopy", - "oncuechange", - "oncut", - "ondblclick", - "ondrag", - "ondragend", - "ondragenter", - "ondragleave", - "ondragover", - "ondragstart", - "ondrop", - "ondurationchange", - "onemptied", - "onended", - "onerror", - "onfocus", - "onhashchange", - "oninput", - "oninvalid", - "onkeydown", - "onkeypress", - "onkeyup", - "onload", - "onloadeddata", - "onloadedmetadata", - "onloadstart", - "onmousedown", - "onmousemove", - "onmouseout", - "onmouseover", - "onmouseup", - "onmousewheel", - "onoffline", - "ononline", - "onpageshow", - "onpaste", - "onpause", - "onplay", - "onplaying", - "onprogress", - "onratechange", - "onreset", - "onresize", - "onscroll", - "onsearch", - "onseeked", - "onseeking", - "onselect", - "onstalled", - "onsubmit", - "onsuspend", - "ontimeupdate", - "ontoggle", - "onunload", - "onvolumechange", - "onwaiting", - "onwheel", - "open", - "optimum", - "pattern", - "placeholder", - "poster", - "preload", - "readonly", - "rel", - "required", - "reversed", - "rows", - "rowspan", - "sandbox", - "scope", - "selected", - "shape", - "size", - "sizes", - "span", - "spellcheck", - "src", - "srcdoc", - "srclang", - "srcset", - "start", - "step", - "style", - "tabindex", - "target", - "title", - "translate", - "type", - "usemap", - "value", - "width", - "wrap", - "0", - "1", - "2", - "3", - "4", - "5", - "6", - "7", - "8", - "9", - "10", - "11", - "12", - "13", - "14", - ]; - - for s in cached_words { - wasm_bindgen::intern(s); - } -} +pub static BUILTIN_INTERNED_STRINGS: &[&'static str] = &[ + // Important tags to dioxus + "dioxus-id", + "dioxus", + "dioxus-event-click", // todo: more events + "click", + // All the HTML Tags + "a", + "abbr", + "address", + "area", + "article", + "aside", + "audio", + "b", + "base", + "bdi", + "bdo", + "big", + "blockquote", + "body", + "br", + "button", + "canvas", + "caption", + "cite", + "code", + "col", + "colgroup", + "command", + "data", + "datalist", + "dd", + "del", + "details", + "dfn", + "dialog", + "div", + "dl", + "dt", + "em", + "embed", + "fieldset", + "figcaption", + "figure", + "footer", + "form", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "head", + "header", + "hr", + "html", + "i", + "iframe", + "img", + "input", + "ins", + "kbd", + "keygen", + "label", + "legend", + "li", + "link", + "main", + "map", + "mark", + "menu", + "menuitem", + "meta", + "meter", + "nav", + "noscript", + "object", + "ol", + "optgroup", + "option", + "output", + "p", + "param", + "picture", + "pre", + "progress", + "q", + "rp", + "rt", + "ruby", + "s", + "samp", + "script", + "section", + "select", + "small", + "source", + "span", + "strong", + "style", + "sub", + "summary", + "sup", + "table", + "tbody", + "td", + "textarea", + "tfoot", + "th", + "thead", + "time", + "title", + "tr", + "track", + "u", + "ul", + "var", + "video", + "wbr", + // All the event handlers + "Attribute", + "accept", + "accept-charset", + "accesskey", + "action", + "alt", + "async", + "autocomplete", + "autofocus", + "autoplay", + "charset", + "checked", + "cite", + "class", + "cols", + "colspan", + "content", + "contenteditable", + "controls", + "coords", + "data", + "data-*", + "datetime", + "default", + "defer", + "dir", + "dirname", + "disabled", + "download", + "draggable", + "enctype", + "for", + "form", + "formaction", + "headers", + "height", + "hidden", + "high", + "href", + "hreflang", + "http-equiv", + "id", + "ismap", + "kind", + "label", + "lang", + "list", + "loop", + "low", + "max", + "maxlength", + "media", + "method", + "min", + "multiple", + "muted", + "name", + "novalidate", + "onabort", + "onafterprint", + "onbeforeprint", + "onbeforeunload", + "onblur", + "oncanplay", + "oncanplaythrough", + "onchange", + "onclick", + "oncontextmenu", + "oncopy", + "oncuechange", + "oncut", + "ondblclick", + "ondrag", + "ondragend", + "ondragenter", + "ondragleave", + "ondragover", + "ondragstart", + "ondrop", + "ondurationchange", + "onemptied", + "onended", + "onerror", + "onfocus", + "onhashchange", + "oninput", + "oninvalid", + "onkeydown", + "onkeypress", + "onkeyup", + "onload", + "onloadeddata", + "onloadedmetadata", + "onloadstart", + "onmousedown", + "onmousemove", + "onmouseout", + "onmouseover", + "onmouseup", + "onmousewheel", + "onoffline", + "ononline", + "onpageshow", + "onpaste", + "onpause", + "onplay", + "onplaying", + "onprogress", + "onratechange", + "onreset", + "onresize", + "onscroll", + "onsearch", + "onseeked", + "onseeking", + "onselect", + "onstalled", + "onsubmit", + "onsuspend", + "ontimeupdate", + "ontoggle", + "onunload", + "onvolumechange", + "onwaiting", + "onwheel", + "open", + "optimum", + "pattern", + "placeholder", + "poster", + "preload", + "readonly", + "rel", + "required", + "reversed", + "rows", + "rowspan", + "sandbox", + "scope", + "selected", + "shape", + "size", + "sizes", + "span", + "spellcheck", + "src", + "srcdoc", + "srclang", + "srcset", + "start", + "step", + "style", + "tabindex", + "target", + "title", + "translate", + "type", + "usemap", + "value", + "width", + "wrap", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "14", +]; diff --git a/packages/web/src/cfg.rs b/packages/web/src/cfg.rs index 09c488022..0fa0dc48c 100644 --- a/packages/web/src/cfg.rs +++ b/packages/web/src/cfg.rs @@ -11,6 +11,7 @@ pub struct WebConfig { pub(crate) hydrate: bool, pub(crate) rootname: String, + pub(crate) cached_strings: Vec, } impl Default for WebConfig { @@ -18,6 +19,7 @@ impl Default for WebConfig { Self { hydrate: false, rootname: "main".to_string(), + cached_strings: Vec::new(), } } } @@ -41,4 +43,12 @@ impl WebConfig { self.rootname = name.into(); self } + + /// Set the name of the element that Dioxus will use as the root. + /// + /// This is akint to calling React.render() on the element with the specified name. + pub fn with_string_cache(mut self, cache: Vec) -> Self { + self.cached_strings = cache; + self + } } diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index ba33f5a60..d792c3725 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -56,7 +56,6 @@ use std::rc::Rc; pub use crate::cfg::WebConfig; use crate::dom::load_document; -use cache::intern_cached_strings; use dioxus::SchedulerMsg; use dioxus::VirtualDom; pub use dioxus_core as dioxus; @@ -82,15 +81,15 @@ mod ric_raf; /// /// ```rust /// fn main() { -/// dioxus_web::launch(App, |c| c); +/// dioxus_web::launch(App); /// } /// -/// static App: FC<()> = |cx, props| { +/// static App: Component<()> = |cx, props| { /// rsx!(cx, div {"hello world"}) /// } /// ``` -pub fn launch(root_component: Component<()>, configuration: impl FnOnce(WebConfig) -> WebConfig) { - launch_with_props(root_component, (), configuration) +pub fn launch(root_component: Component<()>) { + launch_with_props(root_component, (), |c| c); } /// Launches the VirtualDOM from the specified component function and props. @@ -109,7 +108,7 @@ pub fn launch(root_component: Component<()>, configuration: impl FnOnce(WebConfi /// name: String /// } /// -/// static App: FC = |cx, props| { +/// static App: Component = |cx, props| { /// rsx!(cx, div {"hello {props.name}"}) /// } /// ``` @@ -140,7 +139,12 @@ pub fn launch_with_props( pub async fn run_with_props(root: Component, root_props: T, cfg: WebConfig) { let mut dom = VirtualDom::new_with_props(root, root_props); - intern_cached_strings(); + for s in crate::cache::BUILTIN_INTERNED_STRINGS { + wasm_bindgen::intern(s); + } + for s in &cfg.cached_strings { + wasm_bindgen::intern(s); + } let should_hydrate = cfg.hydrate; @@ -158,7 +162,6 @@ pub async fn run_with_props(root: Component, root_props: T // hydrating is simply running the dom for a single render. If the page is already written, then the corresponding // ElementIds should already line up because the web_sys dom has already loaded elements with the DioxusID into memory if !should_hydrate { - // log::info!("Applying rebuild edits..., {:?}", mutations); websys_dom.process_edits(&mut mutations.edits); } @@ -169,20 +172,17 @@ pub async fn run_with_props(root: Component, root_props: T // if there is work then this future resolves immediately. dom.wait_for_work().await; - // // wait for the mainthread to schedule us in - // let mut deadline = work_loop.wait_for_idle_time().await; + // wait for the mainthread to schedule us in + let mut deadline = work_loop.wait_for_idle_time().await; // run the virtualdom work phase until the frame deadline is reached - let mutations = dom.work_with_deadline(|| false); - // // run the virtualdom work phase until the frame deadline is reached - // let mutations = dom.work_with_deadline(|| (&mut deadline).now_or_never().is_some()); + let mutations = dom.work_with_deadline(|| (&mut deadline).now_or_never().is_some()); // wait for the animation frame to fire so we can apply our changes work_loop.wait_for_raf().await; for mut edit in mutations { // actually apply our changes during the animation frame - // log::info!("Applying change edits..., {:?}", edit); websys_dom.process_edits(&mut edit.edits); } }