mirror of
https://github.com/DioxusLabs/dioxus
synced 2025-02-17 06:08:26 +00:00
Feat: found a fast solution to hook state
This commit is contained in:
parent
62d4ad5878
commit
4d01436c3f
9 changed files with 759 additions and 85 deletions
|
@ -18,6 +18,7 @@ members = [
|
|||
"packages/cli",
|
||||
"examples",
|
||||
"packages/html-macro",
|
||||
"packages/html-macro-2",
|
||||
#
|
||||
#
|
||||
#
|
||||
|
|
2
packages/core/.vscode/settings.json
vendored
2
packages/core/.vscode/settings.json
vendored
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"rust-analyzer.inlayHints.enable": false
|
||||
"rust-analyzer.inlayHints.enable": true
|
||||
}
|
|
@ -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
|
||||
|
||||
[dependencies]
|
||||
# dodrio-derive = { path = "../html-macro-2", version = "0.1.0" }
|
||||
dioxus-html-macro = { path = "../html-macro", version = "0.1.0" }
|
||||
dioxus-core-macro = { path = "../core-macro" }
|
||||
# Backs some static data
|
||||
|
@ -17,6 +18,12 @@ once_cell = "1.5.2"
|
|||
|
||||
# Backs the scope creation and reutilization
|
||||
generational-arena = "0.2.8"
|
||||
|
||||
# Bumpalo backs the VNode creation
|
||||
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"
|
||||
|
|
|
@ -1,41 +1,84 @@
|
|||
#![allow(unused, non_upper_case_globals, non_snake_case)]
|
||||
use bumpalo::Bump;
|
||||
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};
|
||||
|
||||
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! ~~~
|
||||
// Super duper efficient :)
|
||||
struct Props {
|
||||
struct Props<'src> {
|
||||
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
|
||||
// The concurrent API will then progress this component when the future finishes
|
||||
// You can suspend the entire component, or just parts of it
|
||||
let product_list = ctx.suspend(async {
|
||||
// Suspend the rendering that completes when the future is done
|
||||
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
|
||||
// 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
|
||||
|
@ -43,7 +86,7 @@ fn BuilderComp(ctx: Context<Props>) -> VNode {
|
|||
div(bump)
|
||||
.attr("class", "edit")
|
||||
.child(text("Hello"))
|
||||
.child(text(ctx.props.text.as_str()))
|
||||
.child(text(ctx.props.text))
|
||||
.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 {
|
||||
let i: i32 = 0;
|
||||
|
||||
|
|
43
packages/core/examples/sketch.rs
Normal file
43
packages/core/examples/sketch.rs
Normal file
|
@ -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
|
||||
)
|
||||
}
|
|
@ -70,6 +70,10 @@ pub mod nodes;
|
|||
pub mod validation;
|
||||
pub mod virtual_dom;
|
||||
|
||||
pub mod builder {
|
||||
pub use super::nodebuilder::*;
|
||||
}
|
||||
|
||||
/// Re-export common types for ease of development use.
|
||||
/// Essential when working with the html! macro
|
||||
///
|
||||
|
@ -89,8 +93,16 @@ pub mod prelude {
|
|||
pub type VirtualNode<'a> = VNode<'a>;
|
||||
|
||||
// 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
|
||||
pub use dioxus_core_macro::fc;
|
||||
pub use dioxus_html_macro::html;
|
||||
|
||||
pub use crate as dioxus;
|
||||
|
||||
pub use crate::nodebuilder as builder;
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
```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,
|
||||
|
@ -18,7 +18,7 @@ into its own lib (IE, lazy loading wasm chunks by function (exciting stuff!))
|
|||
```ignore
|
||||
#[fc] // gets translated into a function.
|
||||
static Component: FC = |ctx| {
|
||||
html! {<div> "hello world" </div>}
|
||||
ctx.view(html! {<div> "hello world" </div>})
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -50,70 +50,152 @@ A Context
|
|||
use crate::nodes::VNode;
|
||||
use crate::prelude::*;
|
||||
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.
|
||||
/// 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
|
||||
/// A generational arean is used to re-use slots of deleted scopes without having to resize the underlying arena.
|
||||
components: Arena<Scope>,
|
||||
|
||||
base_scope: Index,
|
||||
|
||||
/// Components generate lifecycle events
|
||||
event_queue: Vec<LifecycleEvent>,
|
||||
|
||||
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.
|
||||
///
|
||||
/// 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.
|
||||
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.
|
||||
/// 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
|
||||
/// 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 {
|
||||
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
|
||||
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(())?;
|
||||
|
||||
/// 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;
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
/// 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)
|
||||
// The parent for this component generated new props and the component needs update
|
||||
LifecycleType::PropsChanged {} => {}
|
||||
|
||||
// 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) {}
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -152,37 +234,38 @@ impl Properties for () {
|
|||
#[cfg(test)]
|
||||
mod fc_test {
|
||||
use super::*;
|
||||
use crate::prelude::*;
|
||||
|
||||
// // 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
|
||||
// }
|
||||
// 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(ctx: &Context<()>) -> impl DomTree {
|
||||
// // todo: helper should be part of html! macro
|
||||
// html! { <div> </div> }
|
||||
// }
|
||||
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(ctx: &Context<()>) -> impl DomTree {
|
||||
// __domtree_helper(move |bump: &Bump| VNode::text("blah"))
|
||||
// }
|
||||
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();
|
||||
#[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
|
||||
// Happiness! The VNodes are now allocated onto the bump vdom
|
||||
let nodes0 = test_static_fn(&b, test_component);
|
||||
|
||||
// let g: FC<_, _> = test_component2;
|
||||
// let nodes1 = test_static_fn(&b, &g);
|
||||
// }
|
||||
let nodes1 = test_static_fn(&b, test_component2);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub struct Scope {
|
||||
hook_idx: i32,
|
||||
hooks: Vec<()>,
|
||||
hooks: Vec<OLDHookState>,
|
||||
props_type: TypeId,
|
||||
}
|
||||
|
||||
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 {
|
||||
hook_idx: 0,
|
||||
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.
|
||||
/// 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
|
||||
// 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.
|
||||
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> {
|
||||
/// Access the children elements passed into the component
|
||||
pub fn children(&self) -> Vec<VNode> {
|
||||
todo!("Children API not yet implemented for component Context")
|
||||
|
@ -271,4 +368,78 @@ impl<'a, T> Context<'a, T> {
|
|||
) -> VNode<'a> {
|
||||
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
packages/html-macro-2/Cargo.toml
Normal file
17
packages/html-macro-2/Cargo.toml
Normal file
|
@ -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
packages/html-macro-2/src/lib.rs
Normal file
380
packages/html-macro-2/src/lib.rs
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue