diff --git a/Cargo.toml b/Cargo.toml index 235754743..ae9d2c464 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,16 +4,16 @@ members = [ "packages/core-macro", "packages/core", "packages/web", - "packages/liveview", - "packages/3d", "packages/docsite", + "packages/ssr", ] +# "packages/liveview", +# "packages/3d", # Built-in # "packages/webview/client", # "packages/webview", # "packages/router", -# "packages/ssr", # "packages/webview", # "packages/livehost", # "packages/vscode-ext", diff --git a/README.md b/README.md index e90523c8a..cbbcd6e64 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Dioxus can be used to deliver webapps, desktop apps, static pages, liveview apps ## Get Started with... - + diff --git a/notes/CHANGELOG.md b/notes/CHANGELOG.md index 242208ab0..3ab2f409a 100644 --- a/notes/CHANGELOG.md +++ b/notes/CHANGELOG.md @@ -1,55 +1,14 @@ - - - -# Project: Live-View 🤲 🍨 -> Combine the server and client into a single file :) - - -# Project: Sanitization (TBD) -> Improve code health -- [ ] (Macro) Clippy sanity for html macro -- [ ] (Macro) Error sanitization - -# Project: Examples -> Get *all* the examples -- [ ] (Examples) Tide example with templating - -# Project: State management -> Get some global state management installed with the hooks API - - -# Project: Concurrency (TBD) -> Ensure the concurrency model works well, play with lifetimes to check if it can be multithreaded + halted - - -# Project: Web-View 🤲 🍨 -> Proof of concept: stream render edits from server to client -- [x] Prove that the diffing and patching framework can support patch streaming - -# Project: Web_sys renderer (TBD) -- [x] WebSys edit interpreter -- [x] Event system using async channels -- [ ] Implement conversion of all event types into synthetic events -# Project: String Render (TBD) -> Implement a light-weight string renderer with basic caching -- [x] (Macro) Make VText nodes automatically capture and format IE allow "Text is {blah}" -- [ ] (SSR) Implement stateful 3rd party string renderer - -# Project: Hooks + Context + Subscriptions (TBD) -> Implement the foundations for state management -- [x] Implement context object -- [x] Implement use_state (rewrite to use the use_reducer api like rei) -- [x] Implement use_ref -- [x] Implement use_context (only the API, not the state management solution) -- [ ] Implement use_reducer (WIP) - -# Project: QOL -> Make it easier to write components -- [x] (Macro) Tweak event syntax to not be dependent on wasm32 target (just return regular closures which get boxed/alloced) -- [x] (Macro) Tweak component syntax to accept a new custom element -- [ ] (Macro) Allow components to specify their props as function args - -# Project: Initial VDOM support (TBD) +# Dioxus v0.1.0 +Welcome to the first iteration of the Dioxus Virtual DOM! This release brings support for: +- Web via WASM +- Desktop via webview integration +- Server-rendering with custom Display Impl +- Liveview (experimental) +- Mobile (experimental) +- State management +- Build CLI +---- +## Project: Initial VDOM support (TBD) > Get the initial VDom + Event System + Patching + Diffing + Component framework up and running > Get a demo working using just the web - [x] (Core) Migrate virtual node into new VNode type @@ -61,3 +20,73 @@ - [x] (Core) Implement child nodes, scope creation - [ ] (Core) Implement dirty tagging and compression +## Project: QOL +> Make it easier to write components +- [x] (Macro) Tweak event syntax to not be dependent on wasm32 target (just return regular closures which get boxed/alloced) +- [x] (Macro) Tweak component syntax to accept a new custom element +- [ ] (Macro) Allow components to specify their props as function args (not going to do) + +## Project: Hooks + Context + Subscriptions (TBD) +> Implement the foundations for state management +- [x] Implement context object +- [x] Implement use_state (rewrite to use the use_reducer api like rei) +- [x] Implement use_ref +- [x] Implement use_context (only the API, not the state management solution) +- [ ] Implement use_reducer (WIP) + +## Project: String Render (TBD) +> Implement a light-weight string renderer with basic caching +- [x] (Macro) Make VText nodes automatically capture and format IE allow "Text is {blah}" +- [x] (SSR) Implement stateful 3rd party string renderer + +## Project: Web_sys renderer (TBD) +- [x] WebSys edit interpreter +- [x] Event system using async channels +- [ ] Implement conversion of all event types into synthetic events + +## Project: Web-View 🤲 🍨 +> Proof of concept: stream render edits from server to client +- [x] Prove that the diffing and patching framework can support patch streaming + +## Project: Examples +> Get *all* the examples +- [ ] (Examples) Tide example with templating + +## Project: State management +> Get some global state management installed with the hooks + context API + + +## Project: Concurrency (TBD) +> Ensure the concurrency model works well, play with lifetimes to check if it can be multithreaded + halted +? + + +## Project: Mobile exploration + + +## Project: Live-View 🤲 🍨 +> Combine the server and client into a single file :) + + +## Project: Sanitization (TBD) +> Improve code health +- [ ] (Macro) Clippy sanity for html macro +- [ ] (Macro) Error sanitization + + +## Outstanding todos: +> anything missed so far +- dirty tagging, compression +- fragments +- make ssr follow HTML spec +- code health +- miri tests +- todo mvc +- fix +- node refs (postpone for future release?) +- styling built-in (future release?) +- key handler? +- FC macro +- Documentation overhaul +- Website +- keys on components diff --git a/packages/core-macro/src/rsxt.rs b/packages/core-macro/src/rsxt.rs index 0cb16f5f8..19e123416 100644 --- a/packages/core-macro/src/rsxt.rs +++ b/packages/core-macro/src/rsxt.rs @@ -16,7 +16,24 @@ use { // ============================================== pub struct RsxRender { custom_context: Option, - el: Element, + root: RootOption, +} + +enum RootOption { + // for lists of components + Fragment(), + Element(Element), + Component(Component), +} + +impl ToTokens for RootOption { + fn to_tokens(&self, tokens: &mut TokenStream2) { + match self { + RootOption::Fragment() => todo!(), + RootOption::Element(el) => el.to_tokens(tokens), + RootOption::Component(comp) => comp.to_tokens(tokens), + } + } } impl Parse for RsxRender { @@ -33,14 +50,24 @@ impl Parse for RsxRender { }) .ok(); - let el: Element = input.parse()?; - Ok(Self { el, custom_context }) + let forked = input.fork(); + let name = forked.parse::()?; + + let root = match crate::util::is_valid_tag(&name.to_string()) { + true => input.parse::().map(|el| RootOption::Element(el)), + false => input.parse::().map(|c| RootOption::Component(c)), + }?; + + Ok(Self { + root, + custom_context, + }) } } impl ToTokens for RsxRender { fn to_tokens(&self, out_tokens: &mut TokenStream2) { - let new_toks = (&self.el).to_token_stream(); + let new_toks = (&self.root).to_token_stream(); // create a lazy tree that accepts a bump allocator let final_tokens = match &self.custom_context { diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index e92f5d2fc..39787b78f 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -17,10 +17,7 @@ dioxus-core-macro = { path = "../core-macro", version = "0.1.1" } generational-arena = { version = "0.2.8", features = ["serde"] } # Bumpalo backs the VNode creation -bumpalo = { version = "3.6.0", features = ["collections"] } - -# Backs hook data - might move away from this arena -typed-arena = "2.0.1" +bumpalo = { version = "3.6.0", features = ["collections", "boxed"] } # custom error type thiserror = "1.0.23" @@ -34,7 +31,7 @@ longest-increasing-subsequence = "0.1.0" # internall used log = "0.4.14" -serde = { version = "1.0.123", features = ["derive"], optional=true } +serde = { version = "1.0.123", features = ["derive"], optional = true } [features] default = [] diff --git a/packages/core/examples/borrowed.rs b/packages/core/examples/borrowed.rs index d1fd7728d..97b8ea801 100644 --- a/packages/core/examples/borrowed.rs +++ b/packages/core/examples/borrowed.rs @@ -22,7 +22,7 @@ struct ListItem { } fn app<'a>(ctx: Context<'a>, props: &Props) -> DomTree { - let val = use_state(&ctx, || 0); + let (val, set_val) = use_state(&ctx, || 0); ctx.render(dioxus::prelude::LazyNodes::new(move |c| { let mut root = builder::ElementBuilder::new(c, "div"); @@ -36,7 +36,7 @@ fn app<'a>(ctx: Context<'a>, props: &Props) -> DomTree { // create the props with nothing but the fc fc_to_builder(ChildItem) .item(child) - .item_handler(val.setter()) + .item_handler(set_val) .build(), )); } diff --git a/packages/core/examples/listener.rs b/packages/core/examples/listener.rs index 93f7416ec..fe87513a7 100644 --- a/packages/core/examples/listener.rs +++ b/packages/core/examples/listener.rs @@ -11,13 +11,13 @@ fn main() { } static Example: FC<()> = |ctx, props| { - let name = use_state(&ctx, || "...?"); + let (name, set_name) = use_state(&ctx, || "...?"); ctx.render(html! {

"Hello, {name}"

- - + +
}) }; diff --git a/packages/core/src/context.rs b/packages/core/src/context.rs index 3a1d2d013..718d3b64d 100644 --- a/packages/core/src/context.rs +++ b/packages/core/src/context.rs @@ -2,7 +2,7 @@ use crate::{nodebuilder::IntoDomTree, prelude::*}; use crate::{nodebuilder::LazyNodes, nodes::VNode}; use bumpalo::Bump; use hooks::Hook; -use std::{cell::RefCell, future::Future, ops::Deref, rc::Rc, sync::atomic::AtomicUsize}; +use std::{cell::RefCell, future::Future, ops::Deref, pin::Pin, rc::Rc, sync::atomic::AtomicUsize}; /// Components in Dioxus use the "Context" object to interact with their lifecycle. /// This lets components schedule updates, integrate hooks, and expose their context via the context api. @@ -25,13 +25,14 @@ use std::{cell::RefCell, future::Future, ops::Deref, rc::Rc, sync::atomic::Atomi // todo: force lifetime of source into T as a valid lifetime too // it's definitely possible, just needs some more messing around pub struct Context<'src> { - pub idx: AtomicUsize, + pub idx: RefCell, pub scope: ScopeIdx, // Borrowed from scope - pub(crate) arena: &'src typed_arena::Arena, - pub(crate) hooks: &'src RefCell>, + // pub(crate) arena: &'src typed_arena::Arena, + pub(crate) hooks: &'src RefCell>, + // pub(crate) hooks: &'src RefCell>, pub(crate) bump: &'src Bump, pub listeners: &'src RefCell>, @@ -114,14 +115,16 @@ impl<'a> Context<'a> { /// This module provides internal state management functionality for Dioxus components pub mod hooks { + use std::any::Any; + use super::*; #[derive(Debug)] - pub struct Hook(pub Box); + pub struct Hook(pub Pin>); impl Hook { - pub fn new(state: Box) -> Self { - Self(state) + pub fn new(state: T) -> Self { + Self(Box::pin(state)) } } @@ -139,28 +142,27 @@ pub mod hooks { // TODO: add this to the "clean up" group for when the component is dropped _cleanup: impl FnOnce(InternalHookState), ) -> Output { - let raw_hook = { - let idx = self.idx.load(std::sync::atomic::Ordering::Relaxed); + let idx = *self.idx.borrow(); - // Mutate hook list if necessary - let mut hooks = self.hooks.borrow_mut(); + // Mutate hook list if necessary + let mut hooks = self.hooks.borrow_mut(); - // Initialize the hook by allocating it in the typed arena. - // We get a reference from the arena which is owned by the component scope - // This is valid because "Context" is only valid while the scope is borrowed - if idx >= hooks.len() { - let new_state = initializer(); - let boxed_state: Box = Box::new(new_state); - let hook = self.arena.alloc(Hook::new(boxed_state)); + // Initialize the hook by allocating it in the typed arena. + // We get a reference from the arena which is owned by the component scope + // This is valid because "Context" is only valid while the scope is borrowed + if idx >= hooks.len() { + let new_state = initializer(); + hooks.push(Hook::new(new_state)); + } + *self.idx.borrow_mut() = 1; - // Push the raw pointer instead of the &mut - // A "poor man's OwningRef" - hooks.push(hook); - } - self.idx.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let stable_ref = hooks.get_mut(idx).unwrap().0.as_mut(); + let v = unsafe { Pin::get_unchecked_mut(stable_ref) }; + let internal_state = v.downcast_mut::().unwrap(); - *hooks.get(idx).unwrap() - }; + // we extend the lifetime from borrowed in this scope to borrowed from self. + // This is okay because the hook is pinned + runner(unsafe { &mut *(internal_state as *mut _) }) /* ** UNSAFETY ALERT ** @@ -178,11 +180,17 @@ pub mod hooks { - We don't expose the raw hook pointer outside of the scope of use_hook - The reference is tied to context, meaning it can only be used while ctx is around to free it */ - let borrowed_hook: &'a mut _ = unsafe { raw_hook.as_mut().unwrap() }; + // let raw_hook: &'scope mut _ = unsafe { &mut *raw_hook }; + // let p = raw_hook.0.downcast_mut::(); + // let r = p.unwrap(); + // let v = unsafe { Pin::get_unchecked_mut(raw_hook) }; + // // let carreied_ref: &'scope mut dyn Any = unsafe { &mut *v }; + // let internal_state = v.downcast_mut::().unwrap(); - let internal_state = borrowed_hook.0.downcast_mut::().unwrap(); + // let real_internal = unsafe { internal_state as *mut _ }; - runner(internal_state) + // runner(unsafe { &mut *real_internal }) + // runner(internal_state) } } } @@ -247,6 +255,8 @@ Context should *never* be dangling!. If a Context is torn down, so should anythi pub fn use_context(&'a self, _narrow: impl Fn(&'_ I) -> &'_ O) -> RemoteState { todo!() } + + pub fn create_context(&self, creator: impl FnOnce() -> T) {} } /// # SAFETY ALERT diff --git a/packages/core/src/hooks.rs b/packages/core/src/hooks.rs index bc0f470f6..3ba5333e3 100644 --- a/packages/core/src/hooks.rs +++ b/packages/core/src/hooks.rs @@ -5,6 +5,7 @@ //! - [ ] use_reducer //! - [ ] use_effect +pub use new_use_state_def::use_state_new; pub use use_reducer_def::use_reducer; pub use use_ref_def::use_ref; pub use use_state_def::use_state; @@ -13,15 +14,94 @@ mod use_state_def { use crate::innerlude::*; use std::{ cell::RefCell, - fmt::Display, ops::{Deref, DerefMut}, rc::Rc, }; + struct UseState { + new_val: Rc>>, + current_val: T, + caller: Box, + } + + /// Store state between component renders! + /// When called, this hook retrives a stored value and provides a setter to update that value. + /// When the setter is called, the component is re-ran with the new value. + /// + /// This is behaves almost exactly the same way as React's "use_state". + /// + /// Usage: + /// ```ignore + /// static Example: FC<()> = |ctx| { + /// let (counter, set_counter) = use_state(ctx, || 0); + /// let increment = |_| set_couter(counter + 1); + /// let decrement = |_| set_couter(counter + 1); + /// + /// html! { + ///
+ ///

"Counter: {counter}"

+ /// + /// + ///
+ /// } + /// } + /// ``` + pub fn use_state<'a, 'c, T: 'static, F: FnOnce() -> T>( + ctx: &'c Context<'a>, + initial_state_fn: F, + ) -> (&'a T, &'a impl Fn(T)) { + ctx.use_hook( + move || UseState { + new_val: Rc::new(RefCell::new(None)), + current_val: initial_state_fn(), + caller: Box::new(|_| println!("setter called!")), + }, + move |hook| { + log::debug!("Use_state set called"); + let inner = hook.new_val.clone(); + let scheduled_update = ctx.schedule_update(); + + // get ownership of the new val and replace the current with the new + // -> as_ref -> borrow_mut -> deref_mut -> take + // -> rc -> &RefCell -> RefMut -> &Option -> T + if let Some(new_val) = hook.new_val.as_ref().borrow_mut().deref_mut().take() { + hook.current_val = new_val; + } + + // todo: swap out the caller with a subscription call and an internal update + hook.caller = Box::new(move |new_val| { + // update the setter with the new value + let mut new_inner = inner.as_ref().borrow_mut(); + *new_inner = Some(new_val); + + // Ensure the component gets updated + scheduled_update(); + }); + + // box gets derefed into a ref which is then taken as ref with the hook + (&hook.current_val, &hook.caller) + }, + |_| {}, + ) + } +} + +mod new_use_state_def { + use crate::innerlude::*; + use std::{ + cell::RefCell, + fmt::{Debug, Display}, + ops::{Deref, DerefMut}, + pin::Pin, + rc::Rc, + }; + pub struct UseState { modifier: Rc>>>, current_val: T, update: Box, + setter: Box, + // setter: Box, } // #[derive(Clone, Copy)] @@ -33,8 +113,11 @@ mod use_state_def { impl<'a, T: 'static> UseState { pub fn setter(&self) -> &dyn Fn(T) { - todo!() + &self.setter + // let r = self.setter.as_mut(); + // unsafe { Pin::get_unchecked_mut(r) } } + pub fn set(&self, new_val: T) { self.modify(|f| *f = new_val); } @@ -84,7 +167,7 @@ mod use_state_def { /// } /// } /// ``` - pub fn use_state<'a, 'c, T: 'static, F: FnOnce() -> T>( + pub fn use_state_new<'a, 'c, T: 'static, F: FnOnce() -> T>( ctx: &'c Context<'a>, initial_state_fn: F, ) -> &'a UseState { @@ -93,19 +176,32 @@ mod use_state_def { modifier: Rc::new(RefCell::new(None)), current_val: initial_state_fn(), update: Box::new(|| {}), + setter: Box::new(|_| {}), }, move |hook| { + log::debug!("addr of hook: {:#?}", hook as *const _); let scheduled_update = ctx.schedule_update(); + // log::debug!("Checking if new value {:#?}", &hook.current_val); // get ownership of the new val and replace the current with the new // -> as_ref -> borrow_mut -> deref_mut -> take // -> rc -> &RefCell -> RefMut -> &Option -> T if let Some(new_val) = hook.modifier.as_ref().borrow_mut().deref_mut().take() { + // log::debug!("setting prev {:#?}", &hook.current_val); (new_val)(&mut hook.current_val); + // log::debug!("setting new value {:#?}", &hook.current_val); } hook.update = Box::new(move || scheduled_update()); + let modifier = hook.modifier.clone(); + hook.setter = Box::new(move |new_val: T| { + let mut slot = modifier.as_ref().borrow_mut(); + + let slot2 = slot.deref_mut(); + *slot2 = Some(Box::new(move |old: &mut T| *old = new_val)); + }); + &*hook }, |_| {}, diff --git a/packages/core/src/nodebuilder.rs b/packages/core/src/nodebuilder.rs index b938e4fe2..3e0a04173 100644 --- a/packages/core/src/nodebuilder.rs +++ b/packages/core/src/nodebuilder.rs @@ -566,6 +566,11 @@ where std::iter::once(self) } } +impl<'a> IntoDomTree<'a> for () { + fn into_vnode(self, ctx: &NodeCtx<'a>) -> VNode<'a> { + VNode::Suspended + } +} #[test] fn test_iterator_of_nodes<'b>() { @@ -646,9 +651,6 @@ pub fn text2<'a>(contents: bumpalo::collections::String<'a>) -> VNode<'a> { let f: &'a str = contents.into_bump_str(); VNode::text(f) } -// pub fn text<'a>(contents: &'a str) -> VNode<'a> { -// VNode::text(contents) -// } /// Construct an attribute for an element. /// @@ -666,35 +668,9 @@ pub fn attr<'a>(name: &'static str, value: &'a str) -> Attribute<'a> { Attribute { name, value } } -// /// Create an event listener. -// /// -// /// `event` is the type of event to listen for, e.g. `"click"`. The `callback` -// /// is the function that will be invoked when the event occurs. -// /// -// /// # Example -// /// -// /// ```no_run -// /// use dioxus::{builder::*, bumpalo::Bump}; -// /// -// /// let b = Bump::new(); -// /// -// /// let listener = on(&b, "click", |root, vdom, event| { -// /// // do something when a click happens... -// /// }); -// /// ``` -// pub fn on<'a, 'b>( -// // pub fn on<'a, F: 'static>( -// bump: &'a Bump, -// event: &'static str, -// callback: impl Fn(VirtualEvent) + 'a, -// ) -> Listener<'a> { -// Listener { -// event, -// callback: bump.alloc(callback), -// } -// } - pub fn virtual_child<'a, T: Properties + 'a>(ctx: &NodeCtx<'a>, f: FC, p: T) -> VNode<'a> { + // currently concerned about if props have a custom drop implementation + // might override it with the props macro let propsd: &'a mut _ = ctx.bump.alloc(p); VNode::Component(crate::nodes::VComponent::new(f, propsd)) } diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index 040e080b8..c5e4e6c63 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -36,9 +36,9 @@ pub struct Scope { // These two could be combined with "OwningRef" to remove unsafe usage // or we could dedicate a tiny bump arena just for them // could also use ourborous - pub hooks: RefCell>, + pub hooks: RefCell>, - pub hook_arena: typed_arena::Arena, + pub hook_arena: Vec, // Unsafety: // - is self-refenrential and therefore needs to point into the bump @@ -63,7 +63,7 @@ impl Scope { Self { caller: broken_caller, - hook_arena: typed_arena::Arena::new(), + hook_arena: Vec::new(), hooks: RefCell::new(Vec::new()), frames: ActiveFrame::new(), children: HashSet::new(), @@ -92,7 +92,7 @@ impl Scope { }; let ctx: Context<'b> = Context { - arena: &self.hook_arena, + // arena: &self.hook_arena, hooks: &self.hooks, bump: &frame.bump, idx: 0.into(), @@ -145,7 +145,7 @@ impl Scope { // Run the callback with the user event log::debug!("Running listener"); listener(source); - log::debug!("Running listener"); + log::debug!("Listener finished"); // drain all the event listeners // if we don't, then they'll stick around and become invalid @@ -203,7 +203,7 @@ impl ActiveFrame { } } - fn current_head_node<'b>(&'b self) -> &'b VNode<'b> { + pub fn current_head_node<'b>(&'b self) -> &'b VNode<'b> { let raw_node = match *self.idx.borrow() & 1 == 0 { true => &self.frames[0], false => &self.frames[1], @@ -217,7 +217,7 @@ impl ActiveFrame { } } - fn prev_head_node<'b>(&'b self) -> &'b VNode<'b> { + pub fn prev_head_node<'b>(&'b self) -> &'b VNode<'b> { let raw_node = match *self.idx.borrow() & 1 != 0 { true => &self.frames[0], false => &self.frames[1], diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index edbea9b70..a328d3a41 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -6,6 +6,8 @@ use generational_arena::Arena; use std::{ any::TypeId, borrow::{Borrow, BorrowMut}, + cell::RefCell, + collections::{BTreeMap, BTreeSet}, rc::{Rc, Weak}, }; use thiserror::private::AsDynError; @@ -19,11 +21,13 @@ pub(crate) type OpaqueComponent<'a> = dyn for<'b> Fn(Context<'b>) -> DomTree + ' pub struct VirtualDom { /// All mounted components are arena allocated to make additions, removals, and references easy to work with /// A generational arena is used to re-use slots of deleted scopes without having to resize the underlying arena. - components: Arena, + pub components: Arena, /// The index of the root component. /// Will not be ready if the dom is fresh - base_scope: ScopeIdx, + pub base_scope: ScopeIdx, + + pub(crate) update_schedule: Rc>>, // a strong allocation to the "caller" for the original props #[doc(hidden)] @@ -70,6 +74,7 @@ impl VirtualDom { components, _root_caller: root_caller, base_scope, + update_schedule: Rc::new(RefCell::new(Vec::new())), _root_prop_type: TypeId::of::

(), } } @@ -85,13 +90,14 @@ impl VirtualDom { let mut diff_machine = DiffMachine::new(); let very_unsafe_components = &mut self.components as *mut generational_arena::Arena; - let mut component = self + let component = self .components .get_mut(self.base_scope) .ok_or_else(|| Error::FatalInternal("Acquring base component should never fail"))?; component.run_scope()?; diff_machine.diff_node(component.old_frame(), component.new_frame()); + // log::debug!("New events generated: {:#?}", diff_machine.lifecycle_events); // chew down the the lifecycle events until all dirty nodes are computed while let Some(event) = diff_machine.lifecycle_events.pop_front() { match event { @@ -119,6 +125,7 @@ impl VirtualDom { } } LifeCycleEvent::PropsChanged { caller, id, scope } => { + log::debug!("PROPS CHANGED"); let idx = scope.upgrade().unwrap().as_ref().borrow().unwrap(); unsafe { let p = &mut *(very_unsafe_components); @@ -131,6 +138,7 @@ impl VirtualDom { // break 'render; } LifeCycleEvent::SameProps { caller, id, scope } => { + log::debug!("SAME PROPS RECEIVED"); // // break 'render; } @@ -181,7 +189,15 @@ impl VirtualDom { .expect("Borrowing should not fail"); component.call_listener(event); - component.run_scope()?; + + /* + -> call listener + -> sort through accumulated queue by the top + -> run each component, running its children + -> add new updates to the tree of updates (these are dirty) + -> + */ + // component.run_scope()?; // let mut diff_machine = DiffMachine::new(); // let mut diff_machine = DiffMachine::new(&self.diff_bump); @@ -192,3 +208,8 @@ impl VirtualDom { self.rebuild() } } + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct HierarchyMarker { + height: u32, +} diff --git a/packages/ssr/index.html b/packages/ssr/index.html new file mode 100644 index 000000000..9021c236e --- /dev/null +++ b/packages/ssr/index.html @@ -0,0 +1,102 @@ +

+
+

+ Hello world!: 0 +

+
+
+

+ Hello world!: 1 +

+
+
+

+ Hello world!: 2 +

+
+
+

+ Hello world!: 3 +

+
+
+

+ Hello world!: 4 +

+
+
+

+ Hello world!: 5 +

+
+
+

+ Hello world!: 6 +

+
+
+

+ Hello world!: 7 +

+
+
+

+ Hello world!: 8 +

+
+
+

+ Hello world!: 9 +

+
+
+

+ Hello world!: 10 +

+
+
+

+ Hello world!: 11 +

+
+
+

+ Hello world!: 12 +

+
+
+

+ Hello world!: 13 +

+
+
+

+ Hello world!: 14 +

+
+
+

+ Hello world!: 15 +

+
+
+

+ Hello world!: 16 +

+
+
+

+ Hello world!: 17 +

+
+
+

+ Hello world!: 18 +

+
+
+

+ Hello world!: 19 +

+
+
diff --git a/packages/ssr/src/lib.rs b/packages/ssr/src/lib.rs index ac6997465..83394e057 100644 --- a/packages/ssr/src/lib.rs +++ b/packages/ssr/src/lib.rs @@ -20,6 +20,7 @@ //! use dioxus_core::prelude::{VNode, FC}; +pub mod tostring; pub mod prelude { pub use dioxus_core::prelude::*; diff --git a/packages/ssr/src/tostring.rs b/packages/ssr/src/tostring.rs new file mode 100644 index 000000000..cd6f21724 --- /dev/null +++ b/packages/ssr/src/tostring.rs @@ -0,0 +1,84 @@ +use std::fmt::Display; + +use dioxus_core::prelude::*; +use dioxus_core::{nodes::VNode, prelude::ScopeIdx, virtual_dom::VirtualDom}; + +struct SsrRenderer { + dom: VirtualDom, +} + +impl Display for SsrRenderer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let node = self + .dom + .components + .get(self.dom.base_scope) + .unwrap() + .frames + .current_head_node(); + + html_render(&self.dom, node, f) + } +} + +// recursively walk the tree +fn html_render( + dom: &VirtualDom, + node: &VNode, + f: &mut std::fmt::Formatter<'_>, +) -> std::fmt::Result { + match node { + VNode::Element(el) => { + write!(f, "<{}", el.tag_name)?; + for attr in el.attributes { + write!(f, " {}=\"{}\"", attr.name, attr.value)?; + } + write!(f, ">\n")?; + for child in el.children { + html_render(dom, child, f)?; + } + write!(f, "\n", el.tag_name)?; + Ok(()) + } + VNode::Text(t) => write!(f, "{}", t.text), + VNode::Suspended => todo!(), + VNode::Component(vcomp) => { + let id = vcomp.ass_scope.as_ref().borrow().unwrap(); + let new_node = dom.components.get(id).unwrap().frames.current_head_node(); + html_render(&dom, new_node, f) + } + } +} + +#[test] +fn test_serialize() { + let mut dom = VirtualDom::new(|ctx, props| { + ctx.render(rsx! { + div { + title: "About W3Schools" + {(0..20).map(|f| rsx!{ + div { + title: "About W3Schools" + style: "color:blue;text-align:center" + class: "About W3Schools" + p { + title: "About W3Schools" + "Hello world!: {f}" + } + } + })} + } + }) + }); + dom.rebuild(); + let renderer = SsrRenderer { dom }; + + use std::fs::File; + use std::io::prelude::*; + + let mut file = File::create("index.html").unwrap(); + let buf = renderer.to_string(); + // dbg!(buf); + file.write(buf.as_bytes()); + // dbg!(renderer.to_string()); +} diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 7323068c5..9862f8f7f 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -55,3 +55,7 @@ debug = true [lib] crate-type = ["cdylib", "rlib"] + + +[dev-dependencies] +uuid = { version = "0.8.2", features = ["v4"] } diff --git a/packages/web/examples/deep.rs b/packages/web/examples/deep.rs index b232cd30c..2ee024e8a 100644 --- a/packages/web/examples/deep.rs +++ b/packages/web/examples/deep.rs @@ -10,7 +10,7 @@ fn main() { use components::CustomB; fn CustomA<'a>(ctx: Context<'a>, props: &'a ()) -> DomTree { - let val = use_state(&ctx, || "abcdef".to_string()); + let (val, set_val) = use_state(&ctx, || "abcdef".to_string()); ctx.render(rsx! { div { @@ -18,11 +18,11 @@ fn CustomA<'a>(ctx: Context<'a>, props: &'a ()) -> DomTree { "CustomA {val}" button { "Upper" - onclick: move |_| val.set(val.to_ascii_uppercase()) + onclick: move |_| set_val(val.to_ascii_uppercase()) } button { "Lower" - onclick: move |_| val.set(val.to_ascii_lowercase()) + onclick: move |_| set_val(val.to_ascii_lowercase()) } CustomB { val: val.as_ref() diff --git a/packages/web/examples/infer.rs b/packages/web/examples/infer.rs index 1b2a5be16..f39ca44be 100644 --- a/packages/web/examples/infer.rs +++ b/packages/web/examples/infer.rs @@ -12,10 +12,10 @@ fn main() { // this is a component static Example: FC<()> = |ctx, _props| { - let event = use_state(&ctx, || None); + let (event, set_event) = use_state(&ctx, || None); let handler = move |evt: MouseEvent| { - event.set(Some(evt)); + set_event(Some(evt)); }; log::info!("hello world"); diff --git a/packages/web/examples/jackjill.rs b/packages/web/examples/jackjill.rs index 91b2da2ef..17c80fab9 100644 --- a/packages/web/examples/jackjill.rs +++ b/packages/web/examples/jackjill.rs @@ -10,7 +10,7 @@ fn main() { } static Example: FC<()> = |ctx, props| { - let name = use_state(&ctx, || "...?"); + let (name, set_name) = use_state(&ctx, || "...?"); log::debug!("Running component...."); @@ -32,14 +32,14 @@ static Example: FC<()> = |ctx, props| {
diff --git a/packages/web/examples/list.rs b/packages/web/examples/list.rs index 1b36d362d..92808b497 100644 --- a/packages/web/examples/list.rs +++ b/packages/web/examples/list.rs @@ -1,6 +1,6 @@ -use std::collections::HashMap; +use std::collections::{BTreeMap, BTreeSet, HashMap}; -use dioxus::prelude::*; +use dioxus::{events::on::MouseEvent, prelude::*}; use dioxus_core as dioxus; use dioxus_web::WebsysRenderer; @@ -17,37 +17,38 @@ fn main() { use lazy_static::lazy_static; lazy_static! { - static ref DummyData: HashMap = { + static ref DummyData: BTreeMap = { let vals = vec![ - ("0 ", "abc123"), - ("1 ", "abc124"), - ("2 ", "abc125"), - ("3 ", "abc126"), - ("4 ", "abc127"), - ("5 ", "abc128"), - ("6 ", "abc129"), - ("7 ", "abc1210"), - ("8 ", "abc1211"), - ("9 ", "abc1212"), - ("10 ", "abc1213"), - ("11 ", "abc1214"), - ("12 ", "abc1215"), - ("13 ", "abc1216"), - ("14 ", "abc1217"), - ("15 ", "abc1218"), - ("16 ", "abc1219"), - ("17 ", "abc1220"), - ("18 ", "abc1221"), - ("19 ", "abc1222"), + "abc123", // + "abc124", // + "abc125", // + "abc126", // + "abc127", // + "abc128", // + "abc129", // + "abc1210", // + "abc1211", // + "abc1212", // + "abc1213", // + "abc1214", // + "abc1215", // + "abc1216", // + "abc1217", // + "abc1218", // + "abc1219", // + "abc1220", // + "abc1221", // + "abc1222", // ]; vals.into_iter() - .map(|(a, b)| (a.to_string(), b.to_string())) + .map(ToString::to_string) + .enumerate() .collect() }; } static App: FC<()> = |ctx, _| { - let items = use_state(&ctx, || DummyData.clone()); + let items = use_state_new(&ctx, || DummyData.clone()); // handle new elements let add_new = move |_| { @@ -59,20 +60,18 @@ static App: FC<()> = |ctx, _| { (_, 0) => "Buzz".to_string(), _ => k.to_string(), }; - m.insert(k.to_string(), v); + m.insert(k, v); }) }; let elements = items.iter().map(|(k, v)| { rsx! { - li { - span {"{k}: {v}"} - button { - "Remove" - onclick: move |_| { - let key_to_remove = k.clone(); - items.modify(move |m| { m.remove(&key_to_remove); } ) - } + ListHelper { + name: k, + value: v + onclick: move |_| { + let key = k.clone(); + items.modify(move |m| { m.remove(&key); } ) } } } @@ -81,17 +80,47 @@ static App: FC<()> = |ctx, _| { ctx.render(rsx!( div { h1 {"Some list"} - - // button to add new item + button { + "Remove all" + onclick: move |_| items.set(BTreeMap::new()) + } button { "add new" onclick: {add_new} } - - // list elements ul { {elements} } } )) }; + +#[derive(Props)] +struct ListProps<'a, F: Fn(MouseEvent) + 'a> { + name: &'a usize, + value: &'a str, + onclick: F, +} + +impl PartialEq for ListProps<'_, F> { + fn eq(&self, other: &Self) -> bool { + // no references are ever the same + false + } +} + +fn ListHelper(ctx: Context, props: &ListProps) -> DomTree { + let k = props.name; + let v = props.value; + ctx.render(rsx! { + li { + class: "flex items-center text-xl" + key: "{k}" + span { "{k}: {v}" } + button { + "__ Remove" + onclick: {&props.onclick} + } + } + }) +} diff --git a/packages/web/examples/rsxt.rs b/packages/web/examples/rsxt.rs index 2996c50c8..48156e8b0 100644 --- a/packages/web/examples/rsxt.rs +++ b/packages/web/examples/rsxt.rs @@ -16,16 +16,13 @@ fn main() { }); } - - - #[derive(PartialEq, Props)] struct ExampleProps { initial_name: &'static str, } static Example: FC = |ctx, props| { - let name = use_state(&ctx, move || props.initial_name.to_string()); + let name = use_state_new(&ctx, move || props.initial_name.to_string()); ctx.render(rsx! { div { @@ -39,9 +36,9 @@ static Example: FC = |ctx, props| { "Hello, {name}" } - CustomButton { name: "Jack!", set_name: name.setter() } - CustomButton { name: "Jill!", set_name: name.setter() } - CustomButton { name: "Bob!", set_name: name.setter() } + CustomButton { name: "Jack!", handler: move |evt| name.set("Jack".to_string()) } + CustomButton { name: "Jill!", handler: move |evt| name.set("Jill".to_string()) } + CustomButton { name: "Bob!", handler: move |evt| name.set("Bob".to_string())} } }) }; @@ -49,23 +46,22 @@ static Example: FC = |ctx, props| { #[derive(Props)] -struct ButtonProps<'src> { +struct ButtonProps<'src, F: Fn(MouseEvent)> { name: &'src str, - set_name: &'src dyn Fn(String) + handler: F } -fn CustomButton<'a>(ctx: Context<'a>, props: &'a ButtonProps<'a>) -> DomTree { +fn CustomButton<'b, 'a, F: Fn(MouseEvent)>(ctx: Context<'a>, props: &'b ButtonProps<'b, F>) -> DomTree { ctx.render(rsx!{ button { class: "inline-block py-4 px-8 mr-6 leading-none text-white bg-indigo-600 hover:bg-indigo-900 font-semibold rounded shadow" - onmouseover: move |evt| (props.set_name)(props.name.to_string()) + onmouseover: {&props.handler} "{props.name}" } }) } - -impl PartialEq for ButtonProps<'_> { +impl PartialEq for ButtonProps<'_, F> { fn eq(&self, other: &Self) -> bool { false } diff --git a/packages/web/examples/todomvc.rs b/packages/web/examples/todomvc.rs new file mode 100644 index 000000000..9a8a4b761 --- /dev/null +++ b/packages/web/examples/todomvc.rs @@ -0,0 +1,178 @@ +use std::{ + collections::{BTreeMap, BTreeSet}, + sync::atomic::AtomicUsize, +}; + +use dioxus_core::prelude::*; +use dioxus_web::WebsysRenderer; +use uuid::Uuid; + +// Entry point +fn main() { + wasm_bindgen_futures::spawn_local(WebsysRenderer::start(|ctx, props| { + ctx.create_context(|| model::TodoManager::new()); + + ctx.render(rsx! { + div { + TodoList {} + Footer {} + } + }) + })) +} + +static TodoList: FC<()> = |ctx, props| { + let todos = use_state_new(&ctx, || BTreeMap::::new()); + + let items = todos.iter().map(|(order, item)| { + rsx!(TodoItem { + // key: "{}", + todo: item + }) + }); + + ctx.render(rsx! { + div { + {items} + } + }) +}; + +#[derive(Debug, PartialEq, Props)] +struct TodoItemsProp<'a> { + todo: &'a model::TodoItem, +} + +fn TodoItem(ctx: Context, props: &TodoItemsProp) -> DomTree { + let (editing, set_editing) = use_state(&ctx, || false); + + let id = props.todo.id; + ctx.render(rsx! ( + li { + div { + "{id}" + } + // {input} + } + )) +} + +static Footer: FC<()> = |ctx, props| { + ctx.render(html! { + + }) +}; + +// The data model that the todo mvc uses +mod model { + use std::{borrow::BorrowMut, future::Future}; + + use super::*; + + #[derive(Debug, PartialEq, Clone)] + pub struct TodoItem { + pub id: Uuid, + pub checked: bool, + pub contents: String, + } + + struct Dispatcher {} + + struct AppContext { + _t: std::rc::Rc, + } + + impl AppContext { + fn dispatch(&self, f: impl FnOnce(&mut T)) {} + fn async_dispatch(&self, f: impl Future) {} + fn get(&self, f: impl Fn(&T) -> &G) -> &G { + f(&self._t) + } + fn set(&self, orig: &mut std::borrow::Cow) { + let r = orig.to_mut(); + } + } + + // use im-rc if your contexts are too large to clone! + // or, dangerously mutate and update subscriptions manually + #[derive(Clone)] + pub struct TodoManager { + items: Vec, + } + + impl AppContext { + fn remove_todo(&self, id: Uuid) { + self.dispatch(|f| {}) + } + + async fn push_todo(&self, todo: TodoItem) { + self.dispatch(|f| { + // + f.items.push(10); + }); + } + + fn add_todo(&self) { + // self.dispatch(|f| {}); + // let items = self.get(|f| &f.items); + } + } + + impl TodoManager { + pub fn new() -> Self { + todo!() + } + + pub fn get_todo(&self) -> &TodoItem { + todo!() + } + } + + pub struct TodoHandle {} + impl TodoHandle { + fn get_todo(&self, id: Uuid) -> &TodoItem { + todo!() + } + + fn add_todo(&self, todo: TodoItem) {} + } + + // use_reducer, but exposes the reducer and context to children + fn use_reducer_context() {} + fn use_context_selector() {} + + fn use_context<'b, 'c, Root: 'static, Item: 'c>( + ctx: &'b Context<'c>, + f: impl Fn(Root) -> &'c Item, + ) -> &'c Item { + todo!() + } + + pub fn use_todo_item<'b, 'c>(ctx: &'b Context<'c>, item: Uuid) -> &'c TodoItem { + todo!() + // ctx.use_hook(|| TodoManager::new(), |hook| {}, cleanup) + } + fn use_todos(ctx: &Context) -> TodoHandle { + todo!() + } + + fn use_todo_context(ctx: &Context) -> AppContext { + todo!() + } + + fn test(ctx: Context) { + let todos = use_todos(&ctx); + let todo = todos.get_todo(Uuid::new_v4()); + + let c = use_todo_context(&ctx); + // todos.add_todo(); + } +}
WebAppsWeb Desktop Mobile State Management