diff --git a/Cargo.toml b/Cargo.toml index 48a11f1b..438bd652 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,6 @@ resolver = "2" members = [ "crates/concoct", "crates/concoct-web", - "crates/concoct-menu", + # "crates/concoct-menu", "web_examples/counter" ] diff --git a/crates/concoct-web/Cargo.toml b/crates/concoct-web/Cargo.toml index 13ac2b38..20f15c41 100644 --- a/crates/concoct-web/Cargo.toml +++ b/crates/concoct-web/Cargo.toml @@ -10,3 +10,4 @@ repository = "https://github.com/concoct-rs/concoct" # concoct = "0.17.0-alpha.2" concoct = { path = "../concoct" } web-sys = { version = "0.3.66", features = ["Document", "Event", "HtmlElement", "HtmlCollection", "Text", "Window"] } +rustc-hash = "1.1.0" diff --git a/crates/concoct-web/src/html.rs b/crates/concoct-web/src/html.rs index 06f76715..f2f096a8 100644 --- a/crates/concoct-web/src/html.rs +++ b/crates/concoct-web/src/html.rs @@ -1,19 +1,19 @@ -use super::WebContext; use concoct::{ - hook::{use_context, use_on_drop, use_provider, use_ref}, - view::ViewCell, - View, ViewBuilder, + hook::{use_context, use_provider, use_ref}, + View, }; -use std::{borrow::Cow, cell::RefCell, rc::Rc}; +use std::{borrow::Cow, cell::RefCell, marker::PhantomData, rc::Rc}; use web_sys::{ wasm_bindgen::{closure::Closure, JsCast}, Element, Event, }; +use crate::WebContext; + macro_rules! make_tag_fns { ($($name:tt),*) => { $( - pub fn $name(content: C) -> Html { + pub fn $name>(content: C) -> Html { Html::new(stringify!($name), content) } )* @@ -40,11 +40,12 @@ struct Data { )>, } -pub struct Html { +pub struct Html { tag: Cow<'static, str>, attrs: Vec<(Cow<'static, str>, Cow<'static, str>)>, handlers: Vec<(Cow<'static, str>, Rc>)>, - content: ViewCell, + content: C, + _marker: PhantomData<(T, A)>, } macro_rules! impl_attr_methods { @@ -67,16 +68,17 @@ macro_rules! impl_handler_methods { }; } -impl Html { - pub fn new(tag: impl Into>, content: C) -> Html +impl Html { + pub fn new(tag: impl Into>, content: C) -> Html where - C: View, + C: View, { Html { tag: tag.into(), attrs: Vec::new(), handlers: Vec::new(), - content: ViewCell::new(content), + content, + _marker: PhantomData, } } @@ -111,19 +113,24 @@ impl Html { ); } -impl ViewBuilder for Html { - fn build(&self) -> impl View { - let data = use_ref(|| RefCell::new(Data::default())); +impl View for Html +where + C: View, +{ + fn body(&mut self, cx: &concoct::Scope) -> impl View { + let data = use_ref(cx, || Rc::new(RefCell::new(Data::default()))); let mut data_ref = data.borrow_mut(); - let web_cx = use_context::().unwrap(); - let data_clone = data.clone(); + let web_cx: Rc = use_context(cx); + let _data_clone = data.clone(); - use_on_drop(move || { + /* + use_on_drop(move || { if let Some(element) = &data_clone.borrow_mut().element { element.remove(); } }); + */ if data_ref.element.is_none() { let elem = web_cx.document.create_element(&self.tag).unwrap(); @@ -155,12 +162,13 @@ impl ViewBuilder for Html { } } - use_provider(WebContext { + use_provider(cx, || WebContext { window: web_cx.window.clone(), document: web_cx.document.clone(), - parent: data_ref.element.as_ref().unwrap().clone().into(), + body: web_cx.body.clone(), + parent: data_ref.element.as_ref().unwrap().clone().unchecked_into(), }); - self.content.clone() + &mut self.content } } diff --git a/crates/concoct-web/src/lib.rs b/crates/concoct-web/src/lib.rs index f257f53a..23cec294 100644 --- a/crates/concoct-web/src/lib.rs +++ b/crates/concoct-web/src/lib.rs @@ -1,66 +1,74 @@ use concoct::{ - hook::{use_context, use_on_drop, use_provider, use_ref}, - TextViewContext, View, ViewBuilder, + hook::{use_context, use_provider, use_ref}, + Scope, TextViewContext, View, }; -use std::{cell::RefCell, rc::Rc}; -use web_sys::{Document, Node, Text, Window}; +use rustc_hash::FxHasher; +use std::{ + cell::Cell, + hash::{Hash, Hasher}, + rc::Rc, +}; +use web_sys::{Document, HtmlElement, Window}; pub mod html; struct WebContext { window: Window, document: Document, - parent: Node, + body: HtmlElement, + parent: HtmlElement, } -struct WebRoot { - pub body: Rc, +pub struct HtmlRoot { + content: C, } -impl ViewBuilder for WebRoot { - fn build(&self) -> impl View { - let window = web_sys::window().unwrap(); - let document = window.document().unwrap(); - let body = document.body().unwrap(); - - use_provider(WebContext { - window, - document, - parent: body.into(), +impl> View for HtmlRoot { + fn body(&mut self, cx: &Scope) -> impl View { + use_provider(cx, || { + let window = web_sys::window().unwrap(); + let document = window.document().unwrap(); + let body = document.body().unwrap(); + WebContext { + window, + document, + body: body.clone(), + parent: body, + } }); - use_provider(TextViewContext::new(|s| { - let web_cx = use_context::().unwrap(); + use_provider(cx, || { + TextViewContext::new(|cx: &Scope, s| { + let web_cx: Rc = use_context(cx); - let data = use_ref(|| RefCell::new((s.clone(), None::))); - let (last, node_cell) = &mut *data.borrow_mut(); + let mut is_init = false; + let (hash_cell, node) = use_ref(cx, || { + let elem = web_cx.document.create_text_node(s); + web_cx.parent.append_child(&elem).unwrap(); - let data_clone = data.clone(); - use_on_drop(move || { - if let Some(node) = &data_clone.borrow_mut().1 { - node.remove(); - } - }); + let mut hasher = FxHasher::default(); + s.hash(&mut hasher); + let hash = hasher.finish(); + + is_init = true; + (Cell::new(hash), elem) + }); + + if !is_init { + let mut hasher = FxHasher::default(); + s.hash(&mut hasher); + let hash = hasher.finish(); - if let Some(node) = node_cell { - if s != *last { - node.set_text_content(Some(&s)); - *last = s.clone(); + let last_hash = hash_cell.get(); + hash_cell.set(hash); + + if hash != last_hash { + node.set_text_content(Some(s)); + } } - } else { - let node = web_cx.document.create_text_node(&s); - web_cx.parent.append_child(&node).unwrap(); - *node_cell = Some(node); - } - })); + }) + }); - self.body.clone() + &mut self.content } } - -pub async fn run(view: impl ViewBuilder) { - concoct::run(WebRoot { - body: Rc::new(view), - }) - .await; -} diff --git a/crates/concoct/examples/app.rs b/crates/concoct/examples/app.rs new file mode 100644 index 00000000..bbebf30f --- /dev/null +++ b/crates/concoct/examples/app.rs @@ -0,0 +1,24 @@ +use concoct::{Scope, View, VirtualDom}; + +struct Child; + +impl View for Child { + fn body(&mut self, cx: &Scope) -> impl View { + dbg!(cx.key); + } +} + +struct Counter; + +impl View for Counter { + fn body(&mut self, _cx: &Scope) -> impl View { + dbg!("view"); + (Child, Child) + } +} + +fn main() { + let mut app = VirtualDom::new(Counter); + app.build(); + app.rebuild(); +} diff --git a/crates/concoct/src/hook/mod.rs b/crates/concoct/src/hook/mod.rs index f754d16d..ce131747 100644 --- a/crates/concoct/src/hook/mod.rs +++ b/crates/concoct/src/hook/mod.rs @@ -1,16 +1,8 @@ -//! Hooks to access render context. - mod use_context; -pub use use_context::{use_context, use_provider}; +pub use self::use_context::use_context; -mod use_effect; -pub use self::use_effect::use_effect; +mod use_provider; +pub use self::use_provider::use_provider; mod use_ref; -pub use use_ref::use_ref; - -mod use_state; -pub use use_state::use_state; - -mod use_on_drop; -pub use use_on_drop::use_on_drop; +pub use self::use_ref::use_ref; diff --git a/crates/concoct/src/hook/use_context.rs b/crates/concoct/src/hook/use_context.rs index c2a66cdd..fa9d703c 100644 --- a/crates/concoct/src/hook/use_context.rs +++ b/crates/concoct/src/hook/use_context.rs @@ -1,26 +1,8 @@ -use crate::Runtime; +use crate::Scope; use std::{any::TypeId, rc::Rc}; -/// Hook to provide a context. -pub fn use_provider(value: T) { - let cx = Runtime::current(); - let cx_ref = cx.inner.borrow(); - let mut scope = cx_ref.scope.as_ref().unwrap().inner.borrow_mut(); - - scope.contexts.insert(TypeId::of::(), Rc::new(value)); -} - -/// Hook to get a context from its type. -pub fn use_context() -> Option> { - Runtime::current() - .inner - .borrow() - .scope - .as_ref() - .unwrap() - .inner - .borrow() - .contexts - .get(&TypeId::of::()) - .map(|rc| Rc::downcast(rc.clone()).unwrap()) +pub fn use_context(cx: &Scope) -> Rc { + let contexts = cx.contexts.borrow(); + let rc = contexts.get(&TypeId::of::()).unwrap(); + Rc::downcast(rc.clone()).unwrap() } diff --git a/crates/concoct/src/hook/use_effect.rs b/crates/concoct/src/hook/use_effect.rs deleted file mode 100644 index 1905feb5..00000000 --- a/crates/concoct/src/hook/use_effect.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::macros::trace; - -use super::use_ref; -use rustc_hash::FxHasher; -use std::hash::{Hash, Hasher}; - -/// Hook to cache a value and run an effect when it's changed. -pub fn use_effect(input: impl Hash, effect: impl FnOnce()) { - let mut hasher = FxHasher::default(); - input.hash(&mut hasher); - let hash = hasher.finish(); - - let mut is_initial = false; - let last_hash = use_ref(|| { - is_initial = true; - hash - }); - - if is_initial || hash != *last_hash { - trace!("running effect"); - - effect() - } -} diff --git a/crates/concoct/src/hook/use_on_drop.rs b/crates/concoct/src/hook/use_on_drop.rs deleted file mode 100644 index 6c2033c1..00000000 --- a/crates/concoct/src/hook/use_on_drop.rs +++ /dev/null @@ -1,18 +0,0 @@ -use super::use_ref; -use crate::Runtime; - -/// Hook to store a function that's triggered on removal of the current `View`. -pub fn use_on_drop(on_drop: impl FnMut() + 'static) { - use_ref(|| { - Runtime::current() - .inner - .borrow() - .scope - .as_ref() - .unwrap() - .inner - .borrow_mut() - .droppers - .push(Box::new(on_drop)) - }); -} diff --git a/crates/concoct/src/hook/use_provider.rs b/crates/concoct/src/hook/use_provider.rs new file mode 100644 index 00000000..171168d6 --- /dev/null +++ b/crates/concoct/src/hook/use_provider.rs @@ -0,0 +1,12 @@ +use crate::Scope; +use std::{any::TypeId, rc::Rc}; + +use super::use_ref; + +pub fn use_provider(cx: &Scope, make_initial: impl FnOnce() -> R) { + use_ref(cx, || { + cx.contexts + .borrow_mut() + .insert(TypeId::of::(), Rc::new(make_initial())) + }); +} diff --git a/crates/concoct/src/hook/use_ref.rs b/crates/concoct/src/hook/use_ref.rs index 56aef98e..5e312e4e 100644 --- a/crates/concoct/src/hook/use_ref.rs +++ b/crates/concoct/src/hook/use_ref.rs @@ -1,29 +1,16 @@ -use crate::Runtime; -use std::rc::Rc; +use crate::Scope; +use std::cell::UnsafeCell; -/// Hook to store a stateless value. -/// -/// This function will only call `make_value` once, on the first render, -/// to create the initial value. -pub fn use_ref(make_value: impl FnOnce() -> T) -> Rc { - let cx = Runtime::current(); - let cx_ref = cx.inner.borrow(); - let mut scope = cx_ref.scope.as_ref().unwrap().inner.borrow_mut(); +pub fn use_ref(cx: &Scope, make_initial: impl FnOnce() -> R) -> &mut R { + let mut node = cx.node.inner.borrow_mut(); + let idx = node.hook_idx; + node.hook_idx += 1; - let idx = scope.hook_idx; - scope.hook_idx += 1; - - if let Some(any) = scope.hooks.get(idx) { - Rc::downcast(any.clone()).unwrap() + let cell = if let Some(cell) = node.hooks.get(idx) { + cell } else { - drop(scope); - drop(cx_ref); - let value = Rc::new(make_value()); - - let cx = Runtime::current(); - let cx_ref = cx.inner.borrow(); - let scope = &mut *cx_ref.scope.as_ref().unwrap().inner.borrow_mut(); - scope.hooks.push(value.clone()); - value - } + node.hooks.push(UnsafeCell::new(Box::new(make_initial()))); + node.hooks.last().unwrap() + }; + unsafe { &mut *cell.get() }.downcast_mut().unwrap() } diff --git a/crates/concoct/src/hook/use_state.rs b/crates/concoct/src/hook/use_state.rs deleted file mode 100644 index b45221a2..00000000 --- a/crates/concoct/src/hook/use_state.rs +++ /dev/null @@ -1,36 +0,0 @@ -use super::use_ref; -use crate::Runtime; -use std::cell::RefCell; - -/// Hook to create render state. -/// -/// This function will only call `make_value` once, on the first render, -/// to create the initial state. -/// The returned function can be called to set the state. -/// -/// ```no_run -/// use concoct::hook::use_state; -/// -/// let (count, set_count) = use_state(|| 0); -/// assert_eq!(count, 0); -/// ``` -pub fn use_state( - make_value: impl FnOnce() -> T, -) -> (T, impl Fn(T) + Clone + 'static) { - let cell = use_ref(|| RefCell::new(make_value())); - let getter = cell.borrow().clone(); - - let cx = Runtime::current(); - let key = cx.inner.borrow().node.unwrap(); - let setter = move |value| { - *cell.borrow_mut() = value; - - let mut cx_ref = cx.inner.borrow_mut(); - cx_ref.pending.push_back(key); - if let Some(waker) = cx_ref.waker.take() { - waker.wake(); - } - }; - - (getter, setter) -} diff --git a/crates/concoct/src/lib.rs b/crates/concoct/src/lib.rs index 55444bac..e8448b6f 100644 --- a/crates/concoct/src/lib.rs +++ b/crates/concoct/src/lib.rs @@ -1,65 +1,132 @@ -//! A UI framework for writing declarative apps on multiple platforms. -//! -//! Concoct uses static typing to describe your UI at compile-time -//! to create an efficient tree of components. Updates to state re-render -//! your application top-down, starting at the state's parent component. -//! -//! ```ignore -//! use concoct::{View, ViewBuilder}; -//! use concoct::hook::use_state; -//! use concoct_web::html; -//! -//! struct App; -//! -//! impl ViewBuilder for App { -//! fn build(&self) -> impl View { -//! let (count, set_high) = use_state(|| 0); -//! let set_low = set_high.clone(); -//! -//! ( -//! format!("High five count: {}", count), -//! html::button("Up high!").on_click(move |_| set_high(count + 1)), -//! html::button("Down low!").on_click(move |_| set_low(count - 1)), -//! ) -//! } -//! } -//! ``` -//! -//! ## Feature flags -//! - `full`: Enables all of the features below. -//! - `tracing`: Enables logging with the `tracing` crate. -//! - -use std::borrow::Cow; -use std::cell::RefCell; +use rustc_hash::FxHashMap; +use slotmap::{DefaultKey, SlotMap}; +use std::{ + any::{Any, TypeId}, + cell::{Cell, RefCell, UnsafeCell}, + rc::Rc, +}; pub mod hook; -mod macros; +pub mod view; +pub use self::view::View; -mod rt; -pub(crate) use self::rt::{Runtime, Scope}; +pub enum ActionResult { + Action(A), + Rebuild, +} -mod tree; -pub(crate) use tree::Node; -pub use tree::Tree; +pub struct Scope { + pub key: DefaultKey, + node: Node, + update: Rc Option>>)>, + is_empty: Cell, + nodes: Rc>>, + contexts: RefCell>>, +} -mod vdom; -pub use self::vdom::VirtualDom; +#[derive(Default)] +struct NodeInner { + hooks: Vec>>, + hook_idx: usize, + children: Vec, +} -mod view_builder; -pub use self::view_builder::ViewBuilder; +#[derive(Clone, Default)] +struct Node { + inner: Rc>, +} -pub mod view; -pub use self::view::View; +pub struct VirtualDom { + content: V, + nodes: Rc>>, + pending_updates: Vec>, + root_key: Option, +} -/// Run a view in a new virtual dom. -pub async fn run(view: impl ViewBuilder) { - let mut vdom = VirtualDom::new(view.into_tree()); - vdom.build(); +impl VirtualDom { + pub fn new(content: V) -> Self { + Self { + content, + nodes: Rc::default(), + pending_updates: Vec::new(), + root_key: None, + } + } + + pub fn build(&mut self) + where + V: View, + { + let node = Node::default(); + let root_key = self.nodes.borrow_mut().insert(node.clone()); + self.root_key = Some(root_key); - loop { - vdom.rebuild().await + let cx = Scope { + key: root_key, + node, + update: Rc::new(|_f| {}), + is_empty: Cell::new(false), + nodes: self.nodes.clone(), + contexts: Default::default(), + }; + build_inner(&mut self.content, &cx) + } + + pub fn rebuild(&mut self) + where + V: View, + { + let root_key = self.root_key.unwrap(); + let node = self.nodes.borrow()[root_key].clone(); + let cx = Scope { + key: root_key, + node, + update: Rc::new(|_| {}), + is_empty: Cell::new(false), + nodes: self.nodes.clone(), + contexts: Default::default(), + }; + rebuild_inner(&mut self.content, &cx) + } +} + +fn build_inner(view: &mut impl View, cx: &Scope) { + let node = Node::default(); + let key = cx.nodes.borrow_mut().insert(node.clone()); + cx.node.inner.borrow_mut().children.push(key); + + let child_cx = Scope { + key, + node, + update: Rc::new(|_f| {}), + is_empty: Cell::new(false), + nodes: cx.nodes.clone(), + contexts: cx.contexts.clone(), + }; + + let mut body = view.body(&child_cx); + if !child_cx.is_empty.get() { + build_inner(&mut body, &child_cx); + } +} + +fn rebuild_inner(view: &mut impl View, cx: &Scope) { + for child_key in &cx.node.inner.borrow().children { + let node = cx.nodes.borrow()[*child_key].clone(); + let child_cx = Scope { + key: *child_key, + node, + update: cx.update.clone(), + is_empty: Cell::new(false), + nodes: cx.nodes.clone(), + contexts: cx.contexts.clone(), + }; + + let mut body = view.body(&child_cx); + if !child_cx.is_empty.get() { + rebuild_inner(&mut body, &child_cx); + } } } @@ -69,18 +136,24 @@ pub async fn run(view: impl ViewBuilder) { /// the default implementation of `View` for string types (like `&str` and `String`). /// /// To expose it to child views, use [`use_provider`](`crate::hook::use_provider`). -pub struct TextViewContext { - view: RefCell)>>, +pub struct TextViewContext { + view: RefCell, &str)>>, } -impl TextViewContext { +impl TextViewContext { /// Create a text view context from a view function. /// /// Text-based views, such as `&str` or `String` will call /// this view function on when rendered. - pub fn new(view: impl FnMut(Cow<'static, str>) + 'static) -> Self { + pub fn new(view: impl FnMut(&Scope, &str) + 'static) -> Self { Self { view: RefCell::new(Box::new(view)), } } } + +pub fn run>(content: V) { + let mut vdom = VirtualDom::new(content); + vdom.build(); + vdom.rebuild(); +} diff --git a/crates/concoct/src/macros.rs b/crates/concoct/src/macros.rs deleted file mode 100644 index 6b4a550b..00000000 --- a/crates/concoct/src/macros.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Internal macros - -macro_rules! trace { - ($($tt:tt)*) => { - #[cfg(feature = "tracing")] - tracing::trace!($($tt)*) - } -} - -pub(crate) use trace; diff --git a/crates/concoct/src/rt.rs b/crates/concoct/src/rt.rs deleted file mode 100644 index 5dfa7e6c..00000000 --- a/crates/concoct/src/rt.rs +++ /dev/null @@ -1,58 +0,0 @@ -use crate::Tree; -use rustc_hash::FxHashMap; -use slotmap::{DefaultKey, SlotMap}; -use std::{ - any::{Any, TypeId}, - cell::RefCell, - collections::VecDeque, - rc::Rc, - task::Waker, - time::Instant, -}; - -#[derive(Default)] -pub(crate) struct ScopeInner { - pub(crate) contexts: FxHashMap>, - pub(crate) hooks: Vec>, - pub(crate) hook_idx: usize, - pub(crate) droppers: Vec>, -} - -#[derive(Clone, Default)] -pub(crate) struct Scope { - pub(crate) inner: Rc>, -} - -#[derive(Default)] -pub(crate) struct RuntimeInner { - pub(crate) node: Option, - pub(crate) pending: VecDeque, - pub(crate) scope: Option, - pub(crate) nodes: SlotMap, - pub(crate) waker: Option, - pub(crate) contexts: FxHashMap>, - pub(crate) limit: Option, -} - -#[derive(Clone, Default)] -pub struct Runtime { - pub(crate) inner: Rc>, -} - -impl Runtime { - pub fn enter(&self) { - CONTEXT - .try_with(|cell| *cell.borrow_mut() = Some(self.clone())) - .unwrap(); - } - - pub fn current() -> Self { - CONTEXT - .try_with(|cell| cell.borrow().as_ref().unwrap().clone()) - .unwrap() - } -} - -thread_local! { - static CONTEXT: RefCell> = RefCell::new(None); -} diff --git a/crates/concoct/src/tree/mod.rs b/crates/concoct/src/tree/mod.rs deleted file mode 100644 index 71e91975..00000000 --- a/crates/concoct/src/tree/mod.rs +++ /dev/null @@ -1,115 +0,0 @@ -use std::{any::Any, collections::HashSet, hash::Hash}; - -mod node; -pub use node::Node; - -/// Statically-typed view tree. -/// -/// This trait is unsafe and intended as the lower-level backend of [`View`](crate::View). -pub trait Tree: 'static { - unsafe fn build(&mut self); - - unsafe fn rebuild(&mut self, last: &mut dyn Any, is_changed: bool); - - unsafe fn remove(&mut self); -} - -impl Tree for Option { - unsafe fn build(&mut self) { - if let Some(tree) = self { - tree.build() - } - } - - unsafe fn rebuild(&mut self, last: &mut dyn Any, _is_changed: bool) { - if let Some(tree) = self { - if let Some(last_tree) = last.downcast_mut::().unwrap() { - tree.rebuild(last_tree, true) - } else { - tree.build(); - } - } else if let Some(last_tree) = last.downcast_mut::().unwrap() { - last_tree.remove(); - } - } - - unsafe fn remove(&mut self) { - if let Some(tree) = self { - tree.remove() - } - } -} - -impl Tree for Vec<(K, T)> -where - K: Hash + Eq + 'static, - T: Tree, -{ - unsafe fn build(&mut self) { - for (_, body) in self.iter_mut() { - body.build() - } - } - - unsafe fn rebuild(&mut self, last: &mut dyn Any, _is_changed: bool) { - let mut visited = HashSet::new(); - let last = last.downcast_mut::().unwrap(); - - for (key, body) in self.iter_mut() { - if let Some((_, last_body)) = last.iter_mut().find(|(last_key, _)| last_key == key) { - body.rebuild(last_body, true); - visited.insert(key); - } else { - body.build(); - } - } - - for (key, body) in last.iter_mut() { - if !visited.contains(key) { - body.remove(); - } - } - } - - unsafe fn remove(&mut self) { - for (_, body) in self.iter_mut() { - body.remove() - } - } -} - -macro_rules! impl_tree_for_tuple { - ($($t:tt : $idx:tt),*) => { - impl<$($t: Tree),*> Tree for ($($t),*) { - unsafe fn build(&mut self) { - $( - self.$idx.build(); - )* - } - - unsafe fn rebuild(&mut self, last: &mut dyn Any, is_changed: bool) { - if let Some(last) = last.downcast_mut::() { - $( - self.$idx.rebuild(&mut last.$idx, is_changed); - )* - } - } - - unsafe fn remove(&mut self) { - $( - self.$idx.remove(); - )* - } - } - }; -} - -impl_tree_for_tuple!(V1: 0, V2: 1); -impl_tree_for_tuple!(V1: 0, V2: 1, V3: 2); -impl_tree_for_tuple!(V1: 0, V2: 1, V3: 2, V4: 3); -impl_tree_for_tuple!(V1: 0, V2: 1, V3: 2, V4: 3, V5: 4); -impl_tree_for_tuple!(V1: 0, V2: 1, V3: 2, V4: 3, V5: 4, V6: 5); -impl_tree_for_tuple!(V1: 0, V2: 1, V3: 2, V4: 3, V5: 4, V6: 5, V7: 6); -impl_tree_for_tuple!(V1: 0, V2: 1, V3: 2, V4: 3, V5: 4, V6: 5, V7: 6, V8: 7); -impl_tree_for_tuple!(V1: 0, V2: 1, V3: 2, V4: 3, V5: 4, V6: 5, V7: 6, V8: 7, V9: 8); -impl_tree_for_tuple!(V1: 0, V2: 1, V3: 2, V4: 3, V5: 4, V6: 5, V7: 6, V8: 7, V9: 8, V10: 9); diff --git a/crates/concoct/src/tree/node.rs b/crates/concoct/src/tree/node.rs deleted file mode 100644 index e9fcb2fe..00000000 --- a/crates/concoct/src/tree/node.rs +++ /dev/null @@ -1,165 +0,0 @@ -use crate::{macros::trace, Runtime, Scope, Tree, ViewBuilder}; -use slotmap::{DefaultKey, Key}; -use std::{ - any::{self, Any}, - mem, -}; - -pub struct Node { - pub(crate) view: V, - pub(crate) body: Option, - pub(crate) builder: F, - pub(crate) scope: Option, - pub(crate) key: Option, -} - -impl Node { - fn name(&self) -> &'static str { - let name = any::type_name::(); - name.split('<') - .next() - .unwrap_or(name) - .split("::") - .last() - .unwrap_or(name) - } -} - -impl Tree for Node -where - V: ViewBuilder, - B: Tree + 'static, - F: FnMut(&'static V) -> B + 'static, -{ - unsafe fn build(&mut self) { - let cx = Runtime::current(); - let mut cx_ref = cx.inner.borrow_mut(); - - if let Some(key) = self.key { - let mut scope = self.scope.as_ref().unwrap().inner.borrow_mut(); - for (name, value) in cx_ref.contexts.iter() { - if !scope.contexts.contains_key(name) { - scope.contexts.insert(*name, value.clone()); - } - } - drop(scope); - - cx_ref.node = Some(key); - cx_ref.scope = Some(self.scope.clone().unwrap()); - drop(cx_ref); - - trace!("rebuilding from {:?}: {}", key.data(), self.name()); - - let view = unsafe { mem::transmute(&self.view) }; - let body = (self.builder)(view); - - let parent_contexts = { - let mut cx_ref = cx.inner.borrow_mut(); - let mut scope = self.scope.as_ref().unwrap().inner.borrow_mut(); - scope.hook_idx = 0; - mem::replace(&mut cx_ref.contexts, scope.contexts.clone()) - }; - - let mut last_body = mem::replace(&mut self.body, Some(body)).unwrap(); - self.body.as_mut().unwrap().rebuild(&mut last_body, true); - - let mut cx_ref = cx.inner.borrow_mut(); - cx_ref.contexts = parent_contexts; - } else { - let key = cx_ref.nodes.insert(self as _); - self.key = Some(key); - - let scope = Scope::default(); - scope.inner.borrow_mut().contexts = cx_ref.contexts.clone(); - self.scope = Some(scope); - - cx_ref.node = Some(key); - cx_ref.scope = Some(self.scope.clone().unwrap()); - drop(cx_ref); - - trace!("building {:?}: {}", key.data(), self.name()); - - let view = unsafe { mem::transmute(&self.view) }; - let body = (self.builder)(view); - - let parent_contexts = { - let mut cx_ref = cx.inner.borrow_mut(); - let mut scope = self.scope.as_ref().unwrap().inner.borrow_mut(); - scope.hook_idx = 0; - mem::replace(&mut cx_ref.contexts, scope.contexts.clone()) - }; - - self.body = Some(body); - self.body.as_mut().unwrap().build(); - - let mut cx_ref = cx.inner.borrow_mut(); - cx_ref.contexts = parent_contexts; - } - } - - unsafe fn rebuild(&mut self, last: &mut dyn Any, is_changed: bool) { - let last = (*last).downcast_mut::().unwrap(); - let cx = Runtime::current(); - let mut cx_ref = cx.inner.borrow_mut(); - - let key = last.key.unwrap(); - self.key = Some(key); - self.scope = last.scope.clone(); - - if is_changed { - let mut scope = self.scope.as_ref().unwrap().inner.borrow_mut(); - for (name, value) in cx_ref.contexts.iter() { - if !scope.contexts.contains_key(name) { - scope.contexts.insert(*name, value.clone()); - } - } - drop(scope); - - cx_ref.node = Some(key); - cx_ref.scope = Some(self.scope.clone().unwrap()); - drop(cx_ref); - - trace!("rebuilding {:?}: {}", key.data(), self.name()); - - let view = unsafe { mem::transmute(&self.view) }; - let body = (self.builder)(view); - - let parent_contexts = { - let mut cx_ref = cx.inner.borrow_mut(); - let mut scope = self.scope.as_ref().unwrap().inner.borrow_mut(); - scope.hook_idx = 0; - mem::replace(&mut cx_ref.contexts, scope.contexts.clone()) - }; - - self.body = Some(body); - - self.body - .as_mut() - .unwrap() - .rebuild(last.body.as_mut().unwrap(), is_changed); - - let mut cx_ref = cx.inner.borrow_mut(); - cx_ref.contexts = parent_contexts; - } else { - trace!("skipping {:?}: {}", self.key.unwrap().data(), self.name()); - - self.body = last.body.take(); - } - } - - unsafe fn remove(&mut self) { - let cx = Runtime::current(); - let mut cx_ref = cx.inner.borrow_mut(); - let key = self.key.unwrap(); - cx_ref.nodes.remove(key); - drop(cx_ref); - - trace!("removing {:?}: {}", key.data(), self.name()); - - for dropper in &mut self.scope.as_ref().unwrap().inner.borrow_mut().droppers { - dropper() - } - - self.body.as_mut().unwrap().remove(); - } -} diff --git a/crates/concoct/src/vdom.rs b/crates/concoct/src/vdom.rs deleted file mode 100644 index f4d1d0c8..00000000 --- a/crates/concoct/src/vdom.rs +++ /dev/null @@ -1,80 +0,0 @@ -use slotmap::Key; -use crate::{macros::trace, Runtime, Tree}; -use std::{ - task::{Poll, Waker}, - time::{Duration, Instant}, -}; - -/// A virtual dom that renders a view on any backend. -pub struct VirtualDom { - cx: Runtime, - tree: T, -} - -impl VirtualDom { - /// Create a new virtual dom from a tree. - pub fn new(tree: T) -> Self { - VirtualDom { - cx: Runtime::default(), - tree, - } - } - - /// Build the initial virtual dom. - pub fn build(&mut self) - where - T: Tree, - { - self.cx.enter(); - - // Safety: Context is dropped when the tree is - unsafe { self.tree.build() } - } - - /// Rebuild the virtual dom. - pub async fn rebuild(&mut self) { - futures::future::poll_fn(|cx| { - self.try_rebuild_with_limit_inner(None, Some(cx.waker().clone())); - - Poll::Pending - }) - .await - } - - pub async fn rebuild_with_limit(&mut self, limit: Duration) { - futures::future::poll_fn(|cx| { - let instant = Instant::now() + limit; - self.try_rebuild_with_limit_inner(Some(instant), Some(cx.waker().clone())); - Poll::Pending - }) - .await - } - - pub fn try_rebuild(&mut self) { - self.try_rebuild_with_limit_inner(None, None) - } - - pub fn try_rebuild_with_limit(&mut self, limit: Duration) { - let instant = Instant::now() + limit; - self.try_rebuild_with_limit_inner(Some(instant), None) - } - - fn try_rebuild_with_limit_inner(&mut self, limit: Option, waker: Option) { - let mut inner = self.cx.inner.borrow_mut(); - inner.limit = limit; - inner.waker = waker; - - if let Some(key) = inner.pending.pop_front() { - trace!("Received rebuild event for {:?}", key.data()); - - if let Some(raw) = inner.nodes.get(key).copied() { - drop(inner); - - self.cx.enter(); - - // Safety: `raw` is guaranteed to be an `&mut dyn Tree`. - unsafe { (&mut *raw).build() }; - } - } - } -} diff --git a/crates/concoct/src/view/cell.rs b/crates/concoct/src/view/cell.rs deleted file mode 100644 index 22b32d69..00000000 --- a/crates/concoct/src/view/cell.rs +++ /dev/null @@ -1,68 +0,0 @@ -use crate::{Tree, View}; -use std::{any::Any, cell::RefCell, marker::PhantomData, rc::Rc}; - -trait AnyTree { - fn as_any(&mut self) -> &mut dyn Any; - - fn as_tree(&mut self) -> &mut dyn Tree; -} - -impl AnyTree for T { - fn as_any(&mut self) -> &mut dyn Any { - self - } - - fn as_tree(&mut self) -> &mut dyn Tree { - self - } -} - -/// Cell that contains a subtree. -/// -/// This type can be cloned and returned from a parent view to wrap its content. -pub struct ViewCell { - tree: Rc>, - _marker: PhantomData, -} - -impl ViewCell { - pub fn new(view: V) -> Self { - Self { - tree: Rc::new(RefCell::new(view.into_tree())), - _marker: PhantomData, - } - } -} - -impl Clone for ViewCell { - fn clone(&self) -> Self { - Self { - tree: self.tree.clone(), - _marker: PhantomData, - } - } -} - -impl View for ViewCell { - fn into_tree(self) -> impl Tree { - self - } -} - -impl Tree for ViewCell { - unsafe fn build(&mut self) { - self.tree.borrow_mut().as_tree().build() - } - - unsafe fn rebuild(&mut self, last: &mut dyn Any, is_changed: bool) { - let last = last.downcast_mut::().unwrap(); - self.tree - .borrow_mut() - .as_tree() - .rebuild(&mut *last.tree.borrow_mut().as_any(), is_changed) - } - - unsafe fn remove(&mut self) { - self.tree.borrow_mut().as_tree().remove() - } -} diff --git a/crates/concoct/src/view/empty.rs b/crates/concoct/src/view/empty.rs deleted file mode 100644 index d02c708e..00000000 --- a/crates/concoct/src/view/empty.rs +++ /dev/null @@ -1,19 +0,0 @@ -use crate::{Tree, View}; -use std::any::Any; - -/// Empty view. -pub struct Empty; - -impl View for Empty { - fn into_tree(self) -> impl Tree { - self - } -} - -impl Tree for Empty { - unsafe fn build(&mut self) {} - - unsafe fn rebuild(&mut self, _last: &mut dyn Any, _is_changed: bool) {} - - unsafe fn remove(&mut self) {} -} diff --git a/crates/concoct/src/view/memo.rs b/crates/concoct/src/view/memo.rs deleted file mode 100644 index f23f24ec..00000000 --- a/crates/concoct/src/view/memo.rs +++ /dev/null @@ -1,46 +0,0 @@ -use crate::{Tree, View}; -use rustc_hash::FxHasher; -use std::{ - any::Any, - hash::{Hash, Hasher}, -}; - -/// Memoize a view, only rendering it when some input has changed. -pub fn memo(input: impl Hash, view: V) -> Memo { - let mut hasher = FxHasher::default(); - input.hash(&mut hasher); - let hash = hasher.finish(); - - Memo { hash, body: view } -} - -/// View for the [`memo`] function. -pub struct Memo { - hash: u64, - body: V, -} - -impl View for Memo { - fn into_tree(self) -> impl Tree { - Memo { - hash: self.hash, - body: self.body.into_tree(), - } - } -} - -impl Tree for Memo { - unsafe fn build(&mut self) { - self.body.build() - } - - unsafe fn rebuild(&mut self, last: &mut dyn Any, is_changed: bool) { - let last = last.downcast_mut::().unwrap(); - self.body - .rebuild(&mut last.body, is_changed && self.hash != last.hash) - } - - unsafe fn remove(&mut self) { - self.body.remove() - } -} diff --git a/crates/concoct/src/view/mod.rs b/crates/concoct/src/view/mod.rs index 7c8d8c01..6c7c97bd 100644 --- a/crates/concoct/src/view/mod.rs +++ b/crates/concoct/src/view/mod.rs @@ -1,63 +1,49 @@ -//! Viewable components of a user-interface. +use crate::{build_inner, hook::use_context, rebuild_inner, Scope, TextViewContext}; +use std::{cell::Cell, rc::Rc}; -use crate::{Node, Tree, ViewBuilder}; -use std::hash::Hash; - -mod cell; -pub use cell::ViewCell; - -mod empty; -pub use empty::Empty; - -mod memo; -pub use memo::{memo, Memo}; - -mod one_of; -pub use one_of::*; - -/// Viewable component of a user-interface. -/// -/// This trait creates a statically-typed tree of views -/// for efficient state updates. -/// -/// Most implementations should come from [`ViewBuilder`], which this trait -/// is implemented for. -pub trait View: 'static { - fn into_tree(self) -> impl Tree; -} - -impl View for Option { - fn into_tree(self) -> impl Tree { - self.map(|me| me.into_tree()) - } +pub trait View { + fn body(&mut self, cx: &Scope) -> impl View; } -impl View for Vec<(K, B)> { - fn into_tree(self) -> impl Tree { - self.into_iter() - .map(|(key, body)| (key, body.into_tree())) - .collect::>() +impl View for () { + fn body(&mut self, cx: &Scope) -> impl View { + cx.is_empty.set(true); } } -impl View for V { - fn into_tree(self) -> impl Tree { - Node { - view: self, - body: None, - builder: |me: &'static V| me.build().into_tree(), - scope: None, - key: None, - } +impl> View for &mut V { + fn body(&mut self, cx: &Scope) -> impl View { + (&mut **self).body(cx) } } macro_rules! impl_view_for_tuple { ($($t:tt : $idx:tt),*) => { - impl<$($t: View),*> View for ($($t),*) { - fn into_tree(self) -> impl Tree { - ($( self.$idx.into_tree() ),*) + impl),*> View for ($($t),*) { + fn body(&mut self, cx: &Scope) -> impl View { + if cx.node.inner.borrow().children.is_empty() { + $( build_inner(&mut self.$idx, cx); )* + } else { + $( { + let key = cx.node.inner.borrow().children[$idx]; + let node = cx.nodes.borrow()[key].clone(); + + let cx = Scope { + key, + node, + update: cx.update.clone(), + is_empty: Cell::new(false), + nodes: cx.nodes.clone(), + contexts: cx.contexts.clone() + }; + let mut body = self.$idx.body(&cx); + if !cx.is_empty.get() { + rebuild_inner(&mut body, &cx); + } + } )* + } + cx.is_empty.set(true); } } }; @@ -72,3 +58,19 @@ impl_view_for_tuple!(V1: 0, V2: 1, V3: 2, V4: 3, V5: 4, V6: 5, V7: 6); impl_view_for_tuple!(V1: 0, V2: 1, V3: 2, V4: 3, V5: 4, V6: 5, V7: 6, V8: 7); impl_view_for_tuple!(V1: 0, V2: 1, V3: 2, V4: 3, V5: 4, V6: 5, V7: 6, V8: 7, V9: 8); impl_view_for_tuple!(V1: 0, V2: 1, V3: 2, V4: 3, V5: 4, V6: 5, V7: 6, V8: 7, V9: 8, V10: 9); + +impl View for &str { + fn body(&mut self, cx: &Scope) -> impl View { + let text_cx: Rc> = use_context(cx); + let mut view = text_cx.view.borrow_mut(); + view(cx, self) + } +} + +impl View for String { + fn body(&mut self, cx: &Scope) -> impl View { + let text_cx: Rc> = use_context(cx); + let mut view = text_cx.view.borrow_mut(); + view(cx, self) + } +} diff --git a/crates/concoct/src/view/one_of.rs b/crates/concoct/src/view/one_of.rs deleted file mode 100644 index e09a435c..00000000 --- a/crates/concoct/src/view/one_of.rs +++ /dev/null @@ -1,63 +0,0 @@ -use crate::{Tree, View}; -use std::any::Any; - -macro_rules! one_of { - ($name:tt, $($t:tt),*) => { - /// Container view for children of multiple types. - pub enum $name<$($t),*> { - $($t($t)),* - } - - impl<$($t: View),*> View for $name<$($t),*> { - fn into_tree(self) -> impl Tree { - match self { - $( - $name::$t(body) => $name::$t(body.into_tree()), - )* - } - } - } - - impl<$($t: Tree),*> Tree for $name<$($t),*> { - unsafe fn build(&mut self) { - match self { - $( - $name::$t(tree) => tree.build(), - )* - } - } - - unsafe fn rebuild(&mut self, last: &mut dyn Any, is_changed: bool) { - let last = last.downcast_mut::().unwrap(); - match (self, last) { - $( - ($name::$t(tree), $name::$t(last_tree)) => { - tree.rebuild(last_tree, is_changed) - } - ),* - (me, last) => { - last.remove(); - me.build(); - } - } - - } - - unsafe fn remove(&mut self) { - match self { - $( - $name::$t(tree) => tree.remove(), - )* - } - } - } - }; -} - -one_of!(OneOf2, A, B); -one_of!(OneOf3, A, B, C); -one_of!(OneOf4, A, B, C, D); -one_of!(OneOf5, A, B, C, D, E); -one_of!(OneOf6, A, B, C, D, E, F); -one_of!(OneOf7, A, B, C, D, E, F, G); -one_of!(OneOf8, A, B, C, D, E, F, G, H); diff --git a/crates/concoct/src/view_builder.rs b/crates/concoct/src/view_builder.rs deleted file mode 100644 index acdba3ad..00000000 --- a/crates/concoct/src/view_builder.rs +++ /dev/null @@ -1,36 +0,0 @@ -use crate::{view::Empty, View}; -use std::rc::Rc; - -/// Builder for a [`View`]. -/// -/// [`View`] is implemented for anything that implements this trait. -pub trait ViewBuilder: 'static { - fn build(&self) -> impl View; -} - -impl ViewBuilder for () { - fn build(&self) -> impl View { - Empty - } -} - -impl ViewBuilder for Rc { - fn build(&self) -> impl View { - (&**self).build() - } -} - -macro_rules! impl_string_view { - ($t:ty) => { - impl ViewBuilder for $t { - fn build(&self) -> impl View { - let cx = crate::hook::use_context::().unwrap(); - let mut view = cx.view.borrow_mut(); - view(self.clone().into()) - } - } - }; -} - -impl_string_view!(&'static str); -impl_string_view!(String); diff --git a/web_examples/counter/src/main.rs b/web_examples/counter/src/main.rs index b2b5be8b..4a4e6b5b 100644 --- a/web_examples/counter/src/main.rs +++ b/web_examples/counter/src/main.rs @@ -1,19 +1,11 @@ -use concoct::{hook::use_state, View, ViewBuilder}; +use concoct::{Scope, View}; use concoct_web::html; -use wasm_bindgen_futures::spawn_local; struct App; -impl ViewBuilder for App { - fn build(&self) -> impl View { - let (count, set_high) = use_state(|| 0); - let set_low = set_high.clone(); - - ( - format!("High five count: {}", count), - html::button("Up high!").on_click(move |_| set_high(count + 1)), - html::button("Down low!").on_click(move |_| set_low(count - 1)), - ) +impl View for App { + fn body(&mut self, _cx: &Scope) -> impl View { + html::button("Up high!") } } @@ -24,6 +16,4 @@ fn main() { .set_max_level(tracing::Level::TRACE) .build(), ); - - spawn_local(concoct_web::run(App)) }