Explorar o código

Feat: found a fast solution to hook state

Jonathan Kelley %!s(int64=4) %!d(string=hai) anos
pai
achega
4d01436c3f

+ 1 - 0
Cargo.toml

@@ -18,6 +18,7 @@ members = [
     "packages/cli",
     "packages/cli",
     "examples",
     "examples",
     "packages/html-macro",
     "packages/html-macro",
+    "packages/html-macro-2",
     #
     #
     #
     #
     #
     #

+ 1 - 1
packages/core/.vscode/settings.json

@@ -1,3 +1,3 @@
 {
 {
-    "rust-analyzer.inlayHints.enable": false
+    "rust-analyzer.inlayHints.enable": true
 }
 }

+ 8 - 1
packages/core/Cargo.toml

@@ -10,6 +10,7 @@ description = "Core functionality for Dioxus - a concurrent renderer-agnostic Vi
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 
 [dependencies]
 [dependencies]
+# dodrio-derive = { path = "../html-macro-2", version = "0.1.0" }
 dioxus-html-macro = { path = "../html-macro", version = "0.1.0" }
 dioxus-html-macro = { path = "../html-macro", version = "0.1.0" }
 dioxus-core-macro = { path = "../core-macro" }
 dioxus-core-macro = { path = "../core-macro" }
 # Backs some static data
 # Backs some static data
@@ -17,6 +18,12 @@ once_cell = "1.5.2"
 
 
 # Backs the scope creation and reutilization
 # Backs the scope creation and reutilization
 generational-arena = "0.2.8"
 generational-arena = "0.2.8"
-
 # Bumpalo backs the VNode creation
 # Bumpalo backs the VNode creation
 bumpalo = { version = "3.6.0", features = ["collections"] }
 bumpalo = { version = "3.6.0", features = ["collections"] }
+
+owning_ref = "0.4.1"
+
+# all the arenas 👿
+typed-arena = "2.0.1"
+toolshed = "0.8.1"
+id-arena = "2.2.1"

+ 61 - 18
packages/core/examples/macrosrc.rs

@@ -1,41 +1,84 @@
 #![allow(unused, non_upper_case_globals, non_snake_case)]
 #![allow(unused, non_upper_case_globals, non_snake_case)]
 use bumpalo::Bump;
 use bumpalo::Bump;
 use dioxus_core::prelude::*;
 use dioxus_core::prelude::*;
-use dioxus_core::{nodebuilder::*, virtual_dom::DomTree};
+use dioxus_core::{nodebuilder::*, virtual_dom::Properties};
 use std::{collections::HashMap, future::Future, marker::PhantomData};
 use std::{collections::HashMap, future::Future, marker::PhantomData};
 
 
-fn main() {}
+fn main() {
+    let mut vdom = VirtualDom::new_with_props(
+        component,
+        Props {
+            blah: false,
+            text: "blah",
+        },
+    );
+
+    vdom.progress();
+
+    let somet = String::from("asd");
+    let text = somet.as_str();
+
+    /*
+    this could be auto-generated via the macro
+    this props is allocated in this
+    but the component and props would like need to be cached
+    we could box this fn, abstracting away the props requirement and just keep the entrance and allocator requirement
+    How do we keep cached things around?
+    Need some sort of caching mechanism
+
+    how do we enter into a childscope from a parent scope?
+
+    Problems:
+    1: Comp props need to be stored somewhere so we can re-evalute components when they receive updates
+    2: Trees are not evaluated
+
+    */
+    let example_caller = move |ctx: &Bump| {
+        todo!()
+        // let p = Props { blah: true, text };
+        // let c = Context { props: &p };
+        // let r = component(&c);
+    };
+
+    // check the edit list
+}
 
 
 // ~~~ Text shared between components via props can be done with lifetimes! ~~~
 // ~~~ Text shared between components via props can be done with lifetimes! ~~~
 // Super duper efficient :)
 // Super duper efficient :)
-struct Props {
+struct Props<'src> {
     blah: bool,
     blah: bool,
-    text: String,
+    text: &'src str,
+}
+impl<'src> Properties for Props<'src> {
+    fn new() -> Self {
+        todo!()
+    }
 }
 }
 
 
-fn Component<'a>(ctx: &'a Context<Props>) -> VNode<'a> {
+fn component<'a>(ctx: &'a Context<Props>) -> VNode<'a> {
     // Write asynchronous rendering code that immediately returns a "suspended" VNode
     // Write asynchronous rendering code that immediately returns a "suspended" VNode
     // The concurrent API will then progress this component when the future finishes
     // The concurrent API will then progress this component when the future finishes
     // You can suspend the entire component, or just parts of it
     // You can suspend the entire component, or just parts of it
     let product_list = ctx.suspend(async {
     let product_list = ctx.suspend(async {
         // Suspend the rendering that completes when the future is done
         // Suspend the rendering that completes when the future is done
         match fetch_data().await {
         match fetch_data().await {
-            Ok(data) => html! {<div> </div>},
-            Err(_) => html! {<div> </div>},
+            Ok(data) => html! { <div> </div>},
+            Err(_) => html! { <div> </div>},
         }
         }
     });
     });
 
 
-    ctx.view(html! {
-        <div>
-            // <h1> "Products" </h1>
-            // // Subnodes can even be suspended
-            // // When completely rendered, they won't cause the component itself to re-render, just their slot
-            // <p> { product_list } </p>
-        </div>
-    })
+    todo!()
+    // ctx.view(html! {
+    //     <div>
+    //         // <h1> "Products" </h1>
+    //         // // Subnodes can even be suspended
+    //         // // When completely rendered, they won't cause the component itself to re-render, just their slot
+    //         // <p> { product_list } </p>
+    //     </div>
+    // })
 }
 }
 
 
-fn BuilderComp(ctx: Context<Props>) -> VNode {
+fn BuilderComp<'a>(ctx: &'a Context<'a, Props>) -> VNode<'a> {
     // VNodes can be constructed via a builder or the html! macro
     // VNodes can be constructed via a builder or the html! macro
     // However, both of these are "lazy" - they need to be evaluated (aka, "viewed")
     // However, both of these are "lazy" - they need to be evaluated (aka, "viewed")
     // We can "view" them with Context for ultimate speed while inside components
     // We can "view" them with Context for ultimate speed while inside components
@@ -43,7 +86,7 @@ fn BuilderComp(ctx: Context<Props>) -> VNode {
         div(bump)
         div(bump)
             .attr("class", "edit")
             .attr("class", "edit")
             .child(text("Hello"))
             .child(text("Hello"))
-            .child(text(ctx.props.text.as_str()))
+            .child(text(ctx.props.text))
             .finish()
             .finish()
     })
     })
 }
 }
@@ -79,7 +122,7 @@ fn EffcComp(ctx: &Context, name: &str) -> VNode {
     })
     })
 }
 }
 
 
-fn FullySuspended(ctx: Context<Props>) -> VNode {
+fn FullySuspended<'a>(ctx: &'a Context<Props>) -> VNode<'a> {
     ctx.suspend(async {
     ctx.suspend(async {
         let i: i32 = 0;
         let i: i32 = 0;
 
 

+ 43 - 0
packages/core/examples/sketch.rs

@@ -0,0 +1,43 @@
+use bumpalo::Bump;
+use dioxus_core::prelude::{Context, VNode};
+use std::{any::Any, cell::RefCell, rc::Rc};
+use std::{borrow::Borrow, sync::atomic::AtomicUsize};
+use typed_arena::Arena;
+
+fn main() {
+    let ar = Arena::new();
+
+    (0..5).for_each(|f| {
+        // Create the temporary context obect
+        let c = Context {
+            _p: std::marker::PhantomData {},
+            props: (),
+            idx: 0.into(),
+            arena: &ar,
+            hooks: RefCell::new(Vec::new()),
+        };
+
+        component(c);
+    });
+}
+
+// we need to do something about props and context being borrowed from different sources....
+// kinda anooying
+/// use_ref creates a new value when the component is created and then borrows that value on every render
+fn component(ctx: Context<()>) {
+    (0..10).for_each(|f| {
+        let r = use_ref(&ctx, move || f);
+        assert_eq!(*r, f);
+    });
+}
+
+pub fn use_ref<'a, P, T: 'static>(
+    ctx: &'a Context<'a, P>,
+    initial_state_fn: impl FnOnce() -> T + 'static,
+) -> &'a T {
+    ctx.use_hook(
+        || initial_state_fn(), // initializer
+        |state| state,         // runner, borrows the internal value
+        |b| {},                // tear down
+    )
+}

+ 13 - 1
packages/core/src/lib.rs

@@ -70,6 +70,10 @@ pub mod nodes;
 pub mod validation;
 pub mod validation;
 pub mod virtual_dom;
 pub mod virtual_dom;
 
 
+pub mod builder {
+    pub use super::nodebuilder::*;
+}
+
 /// Re-export common types for ease of development use.
 /// Re-export common types for ease of development use.
 /// Essential when working with the html! macro
 /// Essential when working with the html! macro
 ///
 ///
@@ -89,8 +93,16 @@ pub mod prelude {
     pub type VirtualNode<'a> = VNode<'a>;
     pub type VirtualNode<'a> = VNode<'a>;
 
 
     // Re-export from the macro crate
     // Re-export from the macro crate
-    pub use dioxus_html_macro::html;
+    // pub use dodrio_derive::html;
+
+    pub use bumpalo;
+    // pub use dioxus_html_macro::html;
 
 
     // Re-export the FC macro
     // Re-export the FC macro
     pub use dioxus_core_macro::fc;
     pub use dioxus_core_macro::fc;
+    pub use dioxus_html_macro::html;
+
+    pub use crate as dioxus;
+
+    pub use crate::nodebuilder as builder;
 }
 }

+ 239 - 68
packages/core/src/virtual_dom.rs

@@ -7,8 +7,8 @@ the DomTree trait is simply an abstraction over a lazy dom builder, much like th
 
 
 This means we can accept DomTree anywhere as well as return it. All components therefore look like this:
 This means we can accept DomTree anywhere as well as return it. All components therefore look like this:
 ```ignore
 ```ignore
-function Component(ctx: Context<()>) -> impl DomTree {
-    html! {<div> "hello world" </div>}
+function Component(ctx: Context<()>) -> VNode {
+    ctx.view(html! {<div> "hello world" </div>})
 }
 }
 ```
 ```
 It's not quite as sexy as statics, but there's only so much you can do. The goal is to get statics working with the FC macro,
 It's not quite as sexy as statics, but there's only so much you can do. The goal is to get statics working with the FC macro,
@@ -18,7 +18,7 @@ into its own lib (IE, lazy loading wasm chunks by function (exciting stuff!))
 ```ignore
 ```ignore
 #[fc] // gets translated into a function.
 #[fc] // gets translated into a function.
 static Component: FC = |ctx| {
 static Component: FC = |ctx| {
-    html! {<div> "hello world" </div>}
+    ctx.view(html! {<div> "hello world" </div>})
 }
 }
 ```
 ```
 
 
@@ -50,71 +50,153 @@ A Context
 use crate::nodes::VNode;
 use crate::nodes::VNode;
 use crate::prelude::*;
 use crate::prelude::*;
 use bumpalo::Bump;
 use bumpalo::Bump;
-use generational_arena::Arena;
-use std::future::Future;
+use generational_arena::{Arena, Index};
+use std::{
+    any::TypeId,
+    cell::{RefCell, UnsafeCell},
+    future::Future,
+    sync::atomic::AtomicUsize,
+};
 
 
 /// An integrated virtual node system that progresses events and diffs UI trees.
 /// An integrated virtual node system that progresses events and diffs UI trees.
 /// Differences are converted into patches which a renderer can use to draw the UI.
 /// Differences are converted into patches which a renderer can use to draw the UI.
-pub struct VirtualDom {
+pub struct VirtualDom<P: Properties> {
     /// All mounted components are arena allocated to make additions, removals, and references easy to work with
     /// All mounted components are arena allocated to make additions, removals, and references easy to work with
     /// A generational arean is used to re-use slots of deleted scopes without having to resize the underlying arena.
     /// A generational arean is used to re-use slots of deleted scopes without having to resize the underlying arena.
     components: Arena<Scope>,
     components: Arena<Scope>,
 
 
+    base_scope: Index,
+
     /// Components generate lifecycle events
     /// Components generate lifecycle events
     event_queue: Vec<LifecycleEvent>,
     event_queue: Vec<LifecycleEvent>,
 
 
     buffers: [Bump; 2],
     buffers: [Bump; 2],
+
+    selected_buf: u8,
+
+    root_props: P,
 }
 }
 
 
-impl VirtualDom {
+/// Implement VirtualDom with no props for components that initialize their state internal to the VDom rather than externally.
+impl VirtualDom<()> {
     /// Create a new instance of the Dioxus Virtual Dom with no properties for the root component.
     /// Create a new instance of the Dioxus Virtual Dom with no properties for the root component.
     ///
     ///
     /// This means that the root component must either consumes its own context, or statics are used to generate the page.
     /// This means that the root component must either consumes its own context, or statics are used to generate the page.
     /// The root component can access things like routing in its context.
     /// The root component can access things like routing in its context.
     pub fn new(root: FC<()>) -> Self {
     pub fn new(root: FC<()>) -> Self {
-        Self::new_with_props(root)
+        Self::new_with_props(root, ())
     }
     }
+}
 
 
+/// Implement the VirtualDom for any Properties
+impl<P: Properties + 'static> VirtualDom<P> {
     /// Start a new VirtualDom instance with a dependent props.
     /// Start a new VirtualDom instance with a dependent props.
     /// Later, the props can be updated by calling "update" with a new set of props, causing a set of re-renders.
     /// Later, the props can be updated by calling "update" with a new set of props, causing a set of re-renders.
     ///
     ///
     /// This is useful when a component tree can be driven by external state (IE SSR) but it would be too expensive
     /// This is useful when a component tree can be driven by external state (IE SSR) but it would be too expensive
     /// to toss out the entire tree.
     /// to toss out the entire tree.
-    pub fn new_with_props<P: Properties>(root: FC<P>) -> Self {
+    pub fn new_with_props(root: FC<P>, root_props: P) -> Self {
+        // 1. Create the buffers
+        // 2. Create the component arena
+        // 3. Create the base scope (can never be removed)
+        // 4. Create the lifecycle queue
+        // 5. Create the event queue
+        let buffers = [Bump::new(), Bump::new()];
+
+        // Arena allocate all the components
+        // This should make it *really* easy to store references in events and such
+        let mut components = Arena::new();
+
+        // Create a reference to the component in the arena
+        let base_scope = components.insert(Scope::new(root));
+
+        // Create an event queue with a mount for the base scope
+        let event_queue = vec![];
+
         Self {
         Self {
-            components: Arena::new(),
-            event_queue: vec![],
-            buffers: [Bump::new(), Bump::new()],
+            components,
+            base_scope,
+            event_queue,
+            buffers,
+            root_props,
+            selected_buf: 0,
         }
         }
     }
     }
 
 
     /// Pop an event off the even queue and process it
     /// Pop an event off the even queue and process it
-    pub fn progress_event() {}
-}
+    pub fn progress(&mut self) -> Result<(), ()> {
+        let LifecycleEvent { index, event_type } = self.event_queue.pop().ok_or(())?;
 
 
-/// The internal lifecycle event system is managed by these
-/// All events need to be confused before swapping doms over
-pub enum LifecycleEvent {
-    Add {},
-}
+        let scope = self.components.get(index).ok_or(())?;
+
+        match event_type {
+            // Component needs to be mounted to the virtual dom
+            LifecycleType::Mount {} => {
+                // todo! run the FC with the bump allocator
+                // Run it with its properties
+            }
+
+            // The parent for this component generated new props and the component needs update
+            LifecycleType::PropsChanged {} => {}
 
 
-/// Anything that takes a "bump" and returns VNodes is a "DomTree"
-/// This is used as a "trait alias" for function return types to look less hair
-pub trait DomTree {
-    fn render(self, b: &Bump) -> VNode;
+            // Component was successfully mounted to the dom
+            LifecycleType::Mounted {} => {}
+
+            // Component was removed from the DOM
+            // Run any destructors and cleanup for the hooks and the dump the component
+            LifecycleType::Removed {} => {
+                let f = self.components.remove(index);
+            }
+
+            // Component was moved around in the DomTree
+            // Doesn't generate any event but interesting to keep track of
+            LifecycleType::Moved {} => {}
+
+            // Component was messaged via the internal subscription service
+            LifecycleType::Messaged => {}
+        }
+
+        Ok(())
+    }
+
+    /// Update the root props, causing a full event cycle
+    pub fn update_props(&mut self, new_props: P) {}
+
+    /// Run through every event in the event queue until the events are empty.
+    /// Function is asynchronous to allow for async components to finish their work.
+    pub async fn progess_completely() {}
+
+    /// Create a new context object for a given component and scope
+    fn new_context<T: Properties>(&self) -> Context<T> {
+        todo!()
+    }
+
+    /// Stop writing to the current buffer and start writing to the new one.
+    /// This should be done inbetween CallbackEvent handling, but not between lifecycle events.
+    pub fn swap_buffers(&mut self) {}
 }
 }
 
 
-/// Implement DomTree for the type returned by the html! macro.
-/// This lets the caller of the static function evaluate the builder closure with its own bump.
-/// It keeps components pretty and removes the need for the user to get too involved with allocation.
-impl<F> DomTree for F
-where
-    F: FnOnce(&Bump) -> VNode,
-{
-    fn render(self, b: &Bump) -> VNode {
-        self(b)
+pub struct LifecycleEvent {
+    pub index: Index,
+    pub event_type: LifecycleType,
+}
+impl LifecycleEvent {
+    fn mount(index: Index) -> Self {
+        Self {
+            index,
+            event_type: LifecycleType::Mount,
+        }
     }
     }
 }
 }
+/// The internal lifecycle event system is managed by these
+pub enum LifecycleType {
+    Mount,
+    PropsChanged,
+    Mounted,
+    Removed,
+    Moved,
+    Messaged,
+}
 
 
 /// The `Component` trait refers to any struct or funciton that can be used as a component
 /// The `Component` trait refers to any struct or funciton that can be used as a component
 /// We automatically implement Component for FC<T>
 /// We automatically implement Component for FC<T>
@@ -152,37 +234,38 @@ impl Properties for () {
 #[cfg(test)]
 #[cfg(test)]
 mod fc_test {
 mod fc_test {
     use super::*;
     use super::*;
+    use crate::prelude::*;
+
+    // Make sure this function builds properly.
+    fn test_static_fn<'a, P: Properties>(b: &'a Bump, r: FC<P>) -> VNode<'a> {
+        todo!()
+        // let p = P::new(); // new props
+        // let c = Context { props: &p }; // new context with props
+        // let g = r(&c); // calling function with context
+        // g
+    }
+
+    fn test_component<'a>(ctx: &'a Context<()>) -> VNode<'a> {
+        // todo: helper should be part of html! macro
+        todo!()
+        // ctx.view(|bump| html! {bump,  <div> </div> })
+    }
+
+    fn test_component2<'a>(ctx: &'a Context<()>) -> VNode<'a> {
+        ctx.view(|bump: &Bump| VNode::text("blah"))
+    }
+
+    #[test]
+    fn ensure_types_work() {
+        // TODO: Get the whole casting thing to work properly.
+        // For whatever reason, FC is not auto-implemented, depsite it being a static type
+        let b = Bump::new();
 
 
-    // // Make sure this function builds properly.
-    // fn test_static_fn<'a, P: Properties, F: DomTree>(b: &'a Bump, r: &FC<P, F>) -> VNode<'a> {
-    //     let p = P::new(); // new props
-    //     let c = Context { props: p }; // new context with props
-    //     let g = r(&c); // calling function with context
-    //     g.render(&b) // rendering closure with bump allocator
-    // }
-
-    // fn test_component(ctx: &Context<()>) -> impl DomTree {
-    //     // todo: helper should be part of html! macro
-    //     html! { <div> </div> }
-    // }
-
-    // fn test_component2(ctx: &Context<()>) -> impl DomTree {
-    //     __domtree_helper(move |bump: &Bump| VNode::text("blah"))
-    // }
-
-    // #[test]
-    // fn ensure_types_work() {
-    //     // TODO: Get the whole casting thing to work properly.
-    //     // For whatever reason, FC is not auto-implemented, depsite it being a static type
-    //     let b = Bump::new();
-
-    //     let g: FC<_, _> = test_component;
-    //     let nodes0 = test_static_fn(&b, &g);
-    //     // Happiness! The VNodes are now allocated onto the bump vdom
-
-    //     let g: FC<_, _> = test_component2;
-    //     let nodes1 = test_static_fn(&b, &g);
-    // }
+        // Happiness! The VNodes are now allocated onto the bump vdom
+        let nodes0 = test_static_fn(&b, test_component);
+
+        let nodes1 = test_static_fn(&b, test_component2);
+    }
 }
 }
 
 
 /// The Scope that wraps a functional component
 /// The Scope that wraps a functional component
@@ -190,19 +273,30 @@ mod fc_test {
 /// The actualy contents of the hooks, though, will be allocated with the standard allocator. These should not allocate as frequently.
 /// The actualy contents of the hooks, though, will be allocated with the standard allocator. These should not allocate as frequently.
 pub struct Scope {
 pub struct Scope {
     hook_idx: i32,
     hook_idx: i32,
-    hooks: Vec<()>,
+    hooks: Vec<OLDHookState>,
+    props_type: TypeId,
 }
 }
 
 
 impl Scope {
 impl Scope {
-    fn new<T>() -> Self {
+    // create a new scope from a function
+    fn new<T: 'static>(f: FC<T>) -> Self {
+        // Capture the props type
+        let props_type = TypeId::of::<T>();
+
+        // Obscure the function
         Self {
         Self {
             hook_idx: 0,
             hook_idx: 0,
             hooks: vec![],
             hooks: vec![],
+            props_type,
         }
         }
     }
     }
+
+    /// Create a new context and run the component with references from the Virtual Dom
+    /// This function downcasts the function pointer based on the stored props_type
+    fn run() {}
 }
 }
 
 
-pub struct HookState {}
+pub struct OLDHookState {}
 
 
 /// Components in Dioxus use the "Context" object to interact with their lifecycle.
 /// 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.
 /// This lets components schedule updates, integrate hooks, and expose their context via the context api.
@@ -224,13 +318,16 @@ pub struct HookState {}
 /// ```
 /// ```
 // todo: force lifetime of source into T as a valid lifetime too
 // todo: force lifetime of source into T as a valid lifetime too
 // it's definitely possible, just needs some more messing around
 // it's definitely possible, just needs some more messing around
-pub struct Context<'source, T> {
+pub struct Context<'src, T> {
     /// Direct access to the properties used to create this component.
     /// Direct access to the properties used to create this component.
-    pub props: &'source T,
+    pub props: T,
+    pub idx: AtomicUsize,
+    pub arena: &'src typed_arena::Arena<Hook>,
+    pub hooks: RefCell<Vec<*mut Hook>>,
+    pub _p: std::marker::PhantomData<&'src ()>,
 }
 }
 
 
 impl<'a, T> Context<'a, T> {
 impl<'a, T> Context<'a, T> {
-    // impl<'a, T> Context<'a, T> {
     /// Access the children elements passed into the component
     /// Access the children elements passed into the component
     pub fn children(&self) -> Vec<VNode> {
     pub fn children(&self) -> Vec<VNode> {
         todo!("Children API not yet implemented for component Context")
         todo!("Children API not yet implemented for component Context")
@@ -271,4 +368,78 @@ impl<'a, T> Context<'a, T> {
     ) -> VNode<'a> {
     ) -> VNode<'a> {
         todo!()
         todo!()
     }
     }
+
+    /// use_hook provides a way to store data between renders for functional components.
+    pub fn use_hook<'comp, InternalHookState: 'static, Output: 'static>(
+        &'comp self,
+        // The closure that builds the hook state
+        initializer: impl FnOnce() -> InternalHookState,
+        // The closure that takes the hookstate and returns some value
+        runner: impl for<'b> FnOnce(&'comp mut InternalHookState) -> &'comp Output,
+        // The closure that cleans up whatever mess is left when the component gets torn down
+        // TODO: add this to the "clean up" group for when the component is dropped
+        tear_down: impl FnOnce(InternalHookState),
+    ) -> &'comp Output {
+        let raw_hook = {
+            let idx = self.idx.load(std::sync::atomic::Ordering::Relaxed);
+
+            // 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<dyn std::any::Any> = Box::new(new_state);
+                let hook = self.arena.alloc(Hook::new(boxed_state));
+
+                // 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);
+
+            *hooks.get(idx).unwrap()
+        };
+
+        /*
+        ** UNSAFETY ALERT **
+        Here, we dereference a raw pointer. Normally, we aren't guaranteed that this is okay.
+
+        However, typed-arena gives a mutable reference to the stored data which is stable for any inserts
+        into the arena. During the first call of the function, we need to add the mutable reference given to use by
+        the arena into our list of hooks. The arena provides stability of the &mut references and is only deallocated
+        when the component itself is deallocated.
+
+        This is okay because:
+        - The lifetime of the component arena is tied to the lifetime of these raw hooks
+        - Usage of the raw hooks is tied behind the Vec refcell
+        - Output is static, meaning it can't take a reference to the data
+        - We don't expose the raw hook pointer outside of the scope of use_hook
+        */
+        let borrowed_hook: &'comp mut _ = unsafe { raw_hook.as_mut().unwrap() };
+
+        let internal_state = borrowed_hook
+            .state
+            .downcast_mut::<InternalHookState>()
+            .unwrap();
+
+        runner(internal_state)
+    }
 }
 }
+
+pub struct Hook {
+    state: Box<dyn std::any::Any>,
+}
+
+impl Hook {
+    fn new(state: Box<dyn std::any::Any>) -> Self {
+        Self { state }
+    }
+}
+
+/// A CallbackEvent wraps any event returned from the renderer's event system.
+pub struct CallbackEvent {}
+
+pub struct EventListener {}

+ 17 - 0
packages/html-macro-2/Cargo.toml

@@ -0,0 +1,17 @@
+[package]
+name = "dodrio-derive"
+version = "0.1.0"
+authors = ["Richard Dodd <richard.o.dodd@gmail.com>"]
+edition = "2018"
+
+[lib]
+proc-macro = true
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+syn = "1.0.18"
+quote = "1.0.3"
+proc-macro-hack = "0.5.15"
+proc-macro2 = "1.0.10"
+style-shared = { git = "https://github.com/derekdreery/style" }

+ 380 - 0
packages/html-macro-2/src/lib.rs

@@ -0,0 +1,380 @@
+use ::{
+    proc_macro::TokenStream,
+    proc_macro2::{Span, TokenStream as TokenStream2},
+    proc_macro_hack::proc_macro_hack,
+    quote::{quote, ToTokens, TokenStreamExt},
+    style_shared::Styles,
+    syn::{
+        ext::IdentExt,
+        parse::{Parse, ParseStream},
+        token, Error, Expr, ExprClosure, Ident, LitBool, LitStr, Path, Result, Token,
+    },
+};
+
+#[proc_macro]
+pub fn html(s: TokenStream) -> TokenStream {
+    let html: HtmlRender = match syn::parse(s) {
+        Ok(s) => s,
+        Err(e) => return e.to_compile_error().into(),
+    };
+    html.to_token_stream().into()
+}
+
+struct HtmlRender {
+    ctx: Ident,
+    kind: NodeOrList,
+}
+
+impl Parse for HtmlRender {
+    fn parse(s: ParseStream) -> Result<Self> {
+        let ctx: Ident = s.parse()?;
+        s.parse::<Token![,]>()?;
+        // if elements are in an array, return a bumpalo::collections::Vec rather than a Node.
+        let kind = if s.peek(token::Bracket) {
+            let nodes_toks;
+            syn::bracketed!(nodes_toks in s);
+            let mut nodes: Vec<MaybeExpr<Node>> = vec![nodes_toks.parse()?];
+            while nodes_toks.peek(Token![,]) {
+                nodes_toks.parse::<Token![,]>()?;
+                nodes.push(nodes_toks.parse()?);
+            }
+            NodeOrList::List(NodeList(nodes))
+        } else {
+            NodeOrList::Node(s.parse()?)
+        };
+        Ok(HtmlRender { ctx, kind })
+    }
+}
+
+impl ToTokens for HtmlRender {
+    fn to_tokens(&self, tokens: &mut TokenStream2) {
+        ToToksCtx::new(&self.ctx, &self.kind).to_tokens(tokens)
+    }
+}
+
+enum NodeOrList {
+    Node(Node),
+    List(NodeList),
+}
+
+impl ToTokens for ToToksCtx<'_, &NodeOrList> {
+    fn to_tokens(&self, tokens: &mut TokenStream2) {
+        match self.inner {
+            NodeOrList::Node(node) => self.recurse(node).to_tokens(tokens),
+            NodeOrList::List(list) => self.recurse(list).to_tokens(tokens),
+        }
+    }
+}
+
+struct NodeList(Vec<MaybeExpr<Node>>);
+
+impl ToTokens for ToToksCtx<'_, &NodeList> {
+    fn to_tokens(&self, tokens: &mut TokenStream2) {
+        let ctx = &self.ctx;
+        let nodes = self.inner.0.iter().map(|node| self.recurse(node));
+        tokens.append_all(quote! {
+            dioxus::bumpalo::vec![in #ctx.bump;
+                #(#nodes),*
+            ]
+        });
+    }
+}
+
+enum Node {
+    Element(Element),
+    Text(TextNode),
+}
+
+impl ToTokens for ToToksCtx<'_, &Node> {
+    fn to_tokens(&self, tokens: &mut TokenStream2) {
+        match &self.inner {
+            Node::Element(el) => self.recurse(el).to_tokens(tokens),
+            Node::Text(txt) => self.recurse(txt).to_tokens(tokens),
+        }
+    }
+}
+
+impl Node {
+    fn peek(s: ParseStream) -> bool {
+        (s.peek(Token![<]) && !s.peek2(Token![/])) || s.peek(token::Brace) || s.peek(LitStr)
+    }
+}
+
+impl Parse for Node {
+    fn parse(s: ParseStream) -> Result<Self> {
+        Ok(if s.peek(Token![<]) {
+            Node::Element(s.parse()?)
+        } else {
+            Node::Text(s.parse()?)
+        })
+    }
+}
+
+struct Element {
+    name: Ident,
+    attrs: Vec<Attr>,
+    children: MaybeExpr<Vec<Node>>,
+}
+
+impl ToTokens for ToToksCtx<'_, &Element> {
+    fn to_tokens(&self, tokens: &mut TokenStream2) {
+        let ctx = self.ctx;
+        let name = &self.inner.name;
+        tokens.append_all(quote! {
+            dioxus::builder::#name(&#ctx)
+        });
+        for attr in self.inner.attrs.iter() {
+            self.recurse(attr).to_tokens(tokens);
+        }
+        match &self.inner.children {
+            MaybeExpr::Expr(expr) => tokens.append_all(quote! {
+                .children(#expr)
+            }),
+            MaybeExpr::Literal(nodes) => {
+                let mut children = nodes.iter();
+                if let Some(child) = children.next() {
+                    let mut inner_toks = TokenStream2::new();
+                    self.recurse(child).to_tokens(&mut inner_toks);
+                    while let Some(child) = children.next() {
+                        quote!(,).to_tokens(&mut inner_toks);
+                        self.recurse(child).to_tokens(&mut inner_toks);
+                    }
+                    tokens.append_all(quote! {
+                        .children([#inner_toks])
+                    });
+                }
+            }
+        }
+        tokens.append_all(quote! {
+            .finish()
+        });
+    }
+}
+
+impl Parse for Element {
+    fn parse(s: ParseStream) -> Result<Self> {
+        s.parse::<Token![<]>()?;
+        let name = Ident::parse_any(s)?;
+        let mut attrs = vec![];
+        let mut children: Vec<Node> = vec![];
+
+        // keep looking for attributes
+        while !s.peek(Token![>]) {
+            // self-closing
+            if s.peek(Token![/]) {
+                s.parse::<Token![/]>()?;
+                s.parse::<Token![>]>()?;
+                return Ok(Self {
+                    name,
+                    attrs,
+                    children: MaybeExpr::Literal(vec![]),
+                });
+            }
+            attrs.push(s.parse()?);
+        }
+        s.parse::<Token![>]>()?;
+
+        // Contents of an element can either be a brace (in which case we just copy verbatim), or a
+        // sequence of nodes.
+        let children = if s.peek(token::Brace) {
+            // expr
+            let content;
+            syn::braced!(content in s);
+            MaybeExpr::Expr(content.parse()?)
+        } else {
+            // nodes
+            let mut children = vec![];
+            while !(s.peek(Token![<]) && s.peek2(Token![/])) {
+                children.push(s.parse()?);
+            }
+            MaybeExpr::Literal(children)
+        };
+
+        // closing element
+        s.parse::<Token![<]>()?;
+        s.parse::<Token![/]>()?;
+        let close = Ident::parse_any(s)?;
+        if close.to_string() != name.to_string() {
+            return Err(Error::new_spanned(
+                close,
+                "closing element does not match opening",
+            ));
+        }
+        s.parse::<Token![>]>()?;
+        Ok(Self {
+            name,
+            attrs,
+            children,
+        })
+    }
+}
+
+struct Attr {
+    name: Ident,
+    ty: AttrType,
+}
+
+impl Parse for Attr {
+    fn parse(s: ParseStream) -> Result<Self> {
+        let mut name = Ident::parse_any(s)?;
+        let name_str = name.to_string();
+        s.parse::<Token![=]>()?;
+        let ty = if name_str.starts_with("on") {
+            // remove the "on" bit
+            name = Ident::new(&name_str.trim_start_matches("on"), name.span());
+            let content;
+            syn::braced!(content in s);
+            AttrType::Event(content.parse()?)
+        } else {
+            let lit_str = if name_str == "style" && s.peek(token::Brace) {
+                // special-case to deal with literal styles.
+                let outer;
+                syn::braced!(outer in s);
+                // double brace for inline style.
+                if outer.peek(token::Brace) {
+                    let inner;
+                    syn::braced!(inner in outer);
+                    let styles: Styles = inner.parse()?;
+                    MaybeExpr::Literal(LitStr::new(&styles.to_string(), Span::call_site()))
+                } else {
+                    // just parse as an expression
+                    MaybeExpr::Expr(outer.parse()?)
+                }
+            } else {
+                s.parse()?
+            };
+            AttrType::Value(lit_str)
+        };
+        Ok(Attr { name, ty })
+    }
+}
+
+impl ToTokens for ToToksCtx<'_, &Attr> {
+    fn to_tokens(&self, tokens: &mut TokenStream2) {
+        let name = self.inner.name.to_string();
+        let mut attr_stream = TokenStream2::new();
+        match &self.inner.ty {
+            AttrType::Value(value) => {
+                let value = self.recurse(value);
+                tokens.append_all(quote! {
+                    .attr(#name, #value)
+                });
+            }
+            AttrType::Event(event) => {
+                tokens.append_all(quote! {
+                    .on(#name, #event)
+                });
+            }
+        }
+    }
+}
+
+enum AttrType {
+    Value(MaybeExpr<LitStr>),
+    Event(ExprClosure),
+    // todo Bool(MaybeExpr<LitBool>)
+}
+
+struct TextNode(MaybeExpr<LitStr>);
+
+impl Parse for TextNode {
+    fn parse(s: ParseStream) -> Result<Self> {
+        Ok(Self(s.parse()?))
+    }
+}
+
+impl ToTokens for ToToksCtx<'_, &TextNode> {
+    fn to_tokens(&self, tokens: &mut TokenStream2) {
+        let mut token_stream = TokenStream2::new();
+        self.recurse(&self.inner.0).to_tokens(&mut token_stream);
+        tokens.append_all(quote! {
+            dioxus::builder::text(#token_stream)
+        });
+    }
+}
+
+enum MaybeExpr<T> {
+    Literal(T),
+    Expr(Expr),
+}
+
+impl<T: Parse> Parse for MaybeExpr<T> {
+    fn parse(s: ParseStream) -> Result<Self> {
+        if s.peek(token::Brace) {
+            let content;
+            syn::braced!(content in s);
+            Ok(MaybeExpr::Expr(content.parse()?))
+        } else {
+            Ok(MaybeExpr::Literal(s.parse()?))
+        }
+    }
+}
+
+impl<'a, T> ToTokens for ToToksCtx<'a, &'a MaybeExpr<T>>
+where
+    T: 'a,
+    ToToksCtx<'a, &'a T>: ToTokens,
+{
+    fn to_tokens(&self, tokens: &mut TokenStream2) {
+        match &self.inner {
+            MaybeExpr::Literal(v) => self.recurse(v).to_tokens(tokens),
+            MaybeExpr::Expr(expr) => expr.to_tokens(tokens),
+        }
+    }
+}
+
+/// ToTokens context
+struct ToToksCtx<'a, T> {
+    inner: T,
+    ctx: &'a Ident,
+}
+
+impl<'a, T> ToToksCtx<'a, T> {
+    fn new(ctx: &'a Ident, inner: T) -> Self {
+        ToToksCtx { ctx, inner }
+    }
+
+    fn recurse<U>(&self, inner: U) -> ToToksCtx<'a, U> {
+        ToToksCtx {
+            ctx: &self.ctx,
+            inner,
+        }
+    }
+}
+
+impl ToTokens for ToToksCtx<'_, &LitStr> {
+    fn to_tokens(&self, tokens: &mut TokenStream2) {
+        self.inner.to_tokens(tokens)
+    }
+}
+
+#[cfg(test)]
+mod test {
+    fn parse(input: &str) -> super::Result<super::HtmlRender> {
+        syn::parse_str(input)
+    }
+
+    #[test]
+    fn div() {
+        parse("bump, <div class=\"test\"/>").unwrap();
+    }
+
+    #[test]
+    fn nested() {
+        parse("bump, <div class=\"test\"><div />\"text\"</div>").unwrap();
+    }
+
+    #[test]
+    fn complex() {
+        parse(
+            "bump,
+            <section style={{
+                display: flex;
+                flex-direction: column;
+                max-width: 95%;
+            }} class=\"map-panel\">{contact_details}</section>
+        ",
+        )
+        .unwrap();
+    }
+}