Feat: context api wired up
This commit is contained in:
1 parent
4c8130c4e4
commit
24805a02f6
7 files changed
+373
-294
No files matched your search
@@ -11,20 +11,21 @@ struct Props {
|
||||
|
||||
#[allow(unused)]
|
||||
static Example: FC<Props> = |ctx, props| {
|
||||
let value = ctx.use_context(|c: &SomeContext| c.items.last().unwrap());
|
||||
todo!()
|
||||
// let value = ctx.use_context(|c: &SomeContext| c.items.last().unwrap());
|
||||
|
||||
ctx.render(LazyNodes::new(move |bump| {
|
||||
builder::ElementBuilder::new(bump, "button")
|
||||
.on("click", move |_| {
|
||||
println!("Value is {}", props.name);
|
||||
println!("Value is {}", value.as_str());
|
||||
println!("Value is {}", *value);
|
||||
})
|
||||
.on("click", move |_| {
|
||||
println!("Value is {}", props.name);
|
||||
})
|
||||
.finish()
|
||||
}))
|
||||
// ctx.render(LazyNodes::new(move |bump| {
|
||||
// builder::ElementBuilder::new(bump, "button")
|
||||
// .on("click", move |_| {
|
||||
// println!("Value is {}", props.name);
|
||||
// println!("Value is {}", value.as_str());
|
||||
// println!("Value is {}", *value);
|
||||
// })
|
||||
// .on("click", move |_| {
|
||||
// println!("Value is {}", props.name);
|
||||
// })
|
||||
// .finish()
|
||||
// }))
|
||||
// ctx.render(html! {
|
||||
// <div>
|
||||
// <button onclick={move |_| println!("Value is {}", value)} />
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
use std::{cell::UnsafeCell, collections::HashMap};
|
||||
use std::{
|
||||
cell::{RefCell, UnsafeCell},
|
||||
collections::HashMap,
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use generational_arena::Arena;
|
||||
|
||||
use crate::innerlude::*;
|
||||
|
||||
pub struct ScopeArena {
|
||||
#[derive(Clone)]
|
||||
pub struct ScopeArena(Rc<RefCell<ScopeArenaInner>>);
|
||||
|
||||
struct ScopeArenaInner {
|
||||
pub(crate) arena: UnsafeCell<Arena<Scope>>,
|
||||
locks: HashMap<ScopeIdx, MutStatus>,
|
||||
}
|
||||
@@ -16,10 +23,10 @@ enum MutStatus {
|
||||
|
||||
impl ScopeArena {
|
||||
pub fn new(arena: Arena<Scope>) -> Self {
|
||||
Self {
|
||||
ScopeArena(Rc::new(RefCell::new(ScopeArenaInner {
|
||||
arena: UnsafeCell::new(arena),
|
||||
locks: Default::default(),
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
pub fn try_get(&self, idx: ScopeIdx) -> Result<&Scope> {
|
||||
|
||||
@@ -55,14 +55,14 @@ impl<T> Deref for RemoteState<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> crate::virtual_dom::Context<'a> {
|
||||
// impl<'a, P> super::Context<'a, P> {
|
||||
pub fn use_context<I, O>(&'a self, _narrow: impl Fn(&'_ I) -> &'_ O) -> RemoteState<O> {
|
||||
todo!()
|
||||
}
|
||||
// impl<'a> crate::virtual_dom::Context<'a> {
|
||||
// // impl<'a, P> super::Context<'a, P> {
|
||||
// pub fn use_context<I, O>(&'a self, _narrow: impl Fn(&'_ I) -> &'_ O) -> RemoteState<O> {
|
||||
// todo!()
|
||||
// }
|
||||
|
||||
pub fn create_context<T: 'static>(&self, creator: impl FnOnce() -> T) {}
|
||||
}
|
||||
// pub fn create_context<T: 'static>(&self, creator: impl FnOnce() -> T) {}
|
||||
// }
|
||||
|
||||
/// # SAFETY ALERT
|
||||
///
|
||||
|
||||
@@ -10,6 +10,9 @@ pub enum Error {
|
||||
#[error("Fatal Internal Error: {0}")]
|
||||
FatalInternal(&'static str),
|
||||
|
||||
#[error("Context is missing")]
|
||||
MissingSharedContext,
|
||||
|
||||
#[error("No event to progress")]
|
||||
NoEvent,
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ pub mod on {
|
||||
let bump = &c.bump();
|
||||
Listener {
|
||||
event: stringify!($name),
|
||||
id: *c.idx.borrow(),
|
||||
id: *c.listener_id.borrow(),
|
||||
scope: c.scope_ref.myidx,
|
||||
callback: bump.alloc(move |evt: VirtualEvent| match evt {
|
||||
VirtualEvent::$eventdata(event) => callback(event),
|
||||
|
||||
@@ -341,7 +341,7 @@ where
|
||||
let listener = Listener {
|
||||
event,
|
||||
callback: bump.alloc(callback),
|
||||
id: *self.ctx.idx.borrow(),
|
||||
id: *self.ctx.listener_id.borrow(),
|
||||
scope: self.ctx.scope_ref.myidx,
|
||||
};
|
||||
self.add_listener(listener)
|
||||
@@ -351,7 +351,7 @@ where
|
||||
self.listeners.push(listener);
|
||||
|
||||
// bump the context id forward
|
||||
*self.ctx.idx.borrow_mut() += 1;
|
||||
*self.ctx.listener_id.borrow_mut() += 1;
|
||||
|
||||
// Add this listener to the context list
|
||||
// This casts the listener to a self-referential pointer
|
||||
|
||||
+335
-267
@@ -5,7 +5,8 @@
|
||||
//! navigate the innerworkings of the Dom. We try to keep these main mechanics in this file to limit
|
||||
//! the possible exposed API surface (keep fields private). This particular implementation of VDOM
|
||||
//! is extremely efficient, but relies on some unsafety under the hood to do things like manage
|
||||
//! micro-heaps for components.
|
||||
//! micro-heaps for components. We are currently working on refactoring the safety out into safe(r)
|
||||
//! abstractions, but current tests (MIRI and otherwise) show no issues with the current implementation.
|
||||
//!
|
||||
//! Included is:
|
||||
//! - The [`VirtualDom`] itself
|
||||
@@ -23,7 +24,8 @@ use bumpalo::Bump;
|
||||
use generational_arena::Arena;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
cell::{RefCell, UnsafeCell},
|
||||
borrow::{Borrow, BorrowMut},
|
||||
cell::RefCell,
|
||||
collections::{HashMap, HashSet},
|
||||
fmt::Debug,
|
||||
future::Future,
|
||||
@@ -31,8 +33,6 @@ use std::{
|
||||
rc::{Rc, Weak},
|
||||
};
|
||||
|
||||
pub use support::*;
|
||||
|
||||
/// 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 {
|
||||
@@ -44,16 +44,12 @@ pub struct VirtualDom {
|
||||
pub components: ScopeArena,
|
||||
|
||||
/// The index of the root component
|
||||
/// Should always be the first (gen0, id0)
|
||||
/// Should always be the first (gen=0, id=0)
|
||||
pub base_scope: ScopeIdx,
|
||||
|
||||
/// All components dump their updates into a queue to be processed
|
||||
pub(crate) event_queue: EventQueue,
|
||||
|
||||
/// Global contexts shared within the VirtualDOM
|
||||
/// These are anchored to individual scopes, making them inaccessible if a context is created from a sibiling
|
||||
pub(crate) contexts: HashMap<ContextId, Box<dyn Any>>,
|
||||
|
||||
/// a strong allocation to the "caller" for the original component and its props
|
||||
#[doc(hidden)]
|
||||
_root_caller: Rc<OpaqueComponent<'static>>,
|
||||
@@ -134,7 +130,7 @@ impl VirtualDom {
|
||||
/// let dom = VirtualDom::new(Example);
|
||||
/// ```
|
||||
pub fn new_with_props<P: Properties + 'static>(root: FC<P>, root_props: P) -> Self {
|
||||
let mut components = Arena::new();
|
||||
let mut components = ScopeArena::new(Arena::new());
|
||||
|
||||
// Normally, a component would be passed as a child in the RSX macro which automatically produces OpaqueComponents
|
||||
// Here, we need to make it manually, using an RC to force the Weak reference to stick around for the main scope.
|
||||
@@ -149,20 +145,25 @@ impl VirtualDom {
|
||||
|
||||
// Make the first scope
|
||||
// We don't run the component though, so renderers will need to call "rebuild" when they initialize their DOM
|
||||
let link = components.clone();
|
||||
let base_scope = components
|
||||
.insert_with(move |myidx| Scope::new(caller_ref, myidx, None, 0, _event_queue));
|
||||
.with(|arena| {
|
||||
arena.insert_with(move |myidx| {
|
||||
Scope::new(caller_ref, myidx, None, 0, _event_queue, link)
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
Self {
|
||||
_root_caller,
|
||||
base_scope,
|
||||
event_queue,
|
||||
contexts: Default::default(),
|
||||
components: ScopeArena::new(components),
|
||||
components,
|
||||
_root_prop_type: TypeId::of::<P>(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom. from scratch
|
||||
/// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom rom scratch
|
||||
pub fn rebuild<'s>(&'s mut self) -> Result<EditList<'s>> {
|
||||
let mut diff_machine = DiffMachine::new();
|
||||
|
||||
@@ -171,8 +172,8 @@ impl VirtualDom {
|
||||
// Instead, this is done on top-level component
|
||||
|
||||
let base = self.components.try_get(self.base_scope)?;
|
||||
let update = self.event_queue.schedule_update(base);
|
||||
update();
|
||||
let immediate_update = self.event_queue.schedule_update(base);
|
||||
immediate_update();
|
||||
|
||||
self.progress_completely(&mut diff_machine)?;
|
||||
|
||||
@@ -317,6 +318,7 @@ impl VirtualDom {
|
||||
None,
|
||||
cur_height + 1,
|
||||
self.event_queue.clone(),
|
||||
self.components.clone(),
|
||||
)
|
||||
})
|
||||
})?;
|
||||
@@ -437,6 +439,12 @@ pub struct Scope {
|
||||
// The parent's scope ID
|
||||
pub parent: Option<ScopeIdx>,
|
||||
|
||||
// A reference to the list of components.
|
||||
// This lets us traverse the component list whenever we need to access our parent or children.
|
||||
arena_link: ScopeArena,
|
||||
|
||||
pub shared_contexts: RefCell<HashMap<TypeId, Rc<dyn Any>>>,
|
||||
|
||||
// Our own ID accessible from the component map
|
||||
pub myidx: ScopeIdx,
|
||||
|
||||
@@ -444,12 +452,10 @@ pub struct Scope {
|
||||
|
||||
pub event_queue: EventQueue,
|
||||
|
||||
// A list of children
|
||||
// TODO, repalce the hashset with a faster hash function
|
||||
pub children: HashSet<ScopeIdx>,
|
||||
|
||||
pub caller: Weak<OpaqueComponent<'static>>,
|
||||
|
||||
pub hookidx: RefCell<usize>,
|
||||
|
||||
// ==========================
|
||||
// slightly unsafe stuff
|
||||
// ==========================
|
||||
@@ -460,7 +466,7 @@ pub struct Scope {
|
||||
// These two could be combined with "OwningRef" to remove unsafe usage
|
||||
// or we could dedicate a tiny bump arena just for them
|
||||
// could also use ourborous
|
||||
pub(crate) hooks: RefCell<Vec<Hook>>,
|
||||
hooks: RefCell<Vec<Hook>>,
|
||||
|
||||
// Unsafety:
|
||||
// - is self-refenrential and therefore needs to point into the bump
|
||||
@@ -483,6 +489,7 @@ impl Scope {
|
||||
parent: Option<ScopeIdx>,
|
||||
height: u32,
|
||||
event_queue: EventQueue,
|
||||
arena_link: ScopeArena,
|
||||
) -> Self {
|
||||
log::debug!(
|
||||
"New scope created, height is {}, idx is {:?}",
|
||||
@@ -490,8 +497,14 @@ impl Scope {
|
||||
myidx
|
||||
);
|
||||
|
||||
// The Componet has a lifetime that's "stuck" to its original allocation.
|
||||
// We need to "break" this reference and manually manage the lifetime.
|
||||
// The function to run this scope is actually located in the parent's bump arena.
|
||||
// Every time the parent is updated, that function is invalidated via double-buffering wiping the old frame.
|
||||
// If children try to run this invalid caller, it *will* result in UB.
|
||||
//
|
||||
// During the lifecycle progression process, this caller will need to be updated. Right now,
|
||||
// until formal safety abstractions are implemented, we will just use unsafe to "detach" the caller
|
||||
// lifetime from the bump arena, exposing ourselves to this potential for invalidation. Truthfully,
|
||||
// this is a bit of a hack, but will remain this way until we've figured out a cleaner solution.
|
||||
//
|
||||
// Not the best solution, so TODO on removing this in favor of a dedicated resource abstraction.
|
||||
let broken_caller = unsafe {
|
||||
@@ -502,15 +515,17 @@ impl Scope {
|
||||
};
|
||||
|
||||
Self {
|
||||
shared_contexts: Default::default(),
|
||||
caller: broken_caller,
|
||||
hooks: RefCell::new(Vec::new()),
|
||||
frames: ActiveFrame::new(),
|
||||
children: HashSet::new(),
|
||||
listeners: Default::default(),
|
||||
hookidx: Default::default(),
|
||||
parent,
|
||||
myidx,
|
||||
height,
|
||||
event_queue,
|
||||
arena_link,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -529,17 +544,11 @@ impl Scope {
|
||||
/// This function downcasts the function pointer based on the stored props_type
|
||||
///
|
||||
/// Props is ?Sized because we borrow the props and don't need to know the size. P (sized) is used as a marker (unsized)
|
||||
pub fn run_scope<'b>(&'b mut self) -> Result<()> {
|
||||
pub fn run_scope<'sel>(&'sel mut self) -> Result<()> {
|
||||
// Cycle to the next frame and then reset it
|
||||
// This breaks any latent references, invalidating every pointer referencing into it.
|
||||
self.frames.next().bump.reset();
|
||||
|
||||
let ctx = Context {
|
||||
idx: 0.into(),
|
||||
_p: std::marker::PhantomData {},
|
||||
scope: self,
|
||||
};
|
||||
|
||||
let caller = self
|
||||
.caller
|
||||
.upgrade()
|
||||
@@ -547,8 +556,10 @@ impl Scope {
|
||||
|
||||
let new_head = unsafe {
|
||||
// Cast the caller ptr from static to one with our own reference
|
||||
std::mem::transmute::<&OpaqueComponent<'static>, &OpaqueComponent<'b>>(caller.as_ref())
|
||||
}(ctx);
|
||||
std::mem::transmute::<&OpaqueComponent<'static>, &OpaqueComponent<'sel>>(
|
||||
caller.as_ref(),
|
||||
)
|
||||
}(&self);
|
||||
|
||||
self.frames.cur_frame_mut().head_node = new_head.root;
|
||||
|
||||
@@ -604,6 +615,297 @@ impl Scope {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// Properties passed down from the parent component are also directly accessible via the exposed "props" field.
|
||||
///
|
||||
/// ```ignore
|
||||
/// #[derive(Properties)]
|
||||
/// struct Props {
|
||||
/// name: String
|
||||
///
|
||||
/// }
|
||||
///
|
||||
/// fn example(ctx: Context, props: &Props -> VNode {
|
||||
/// html! {
|
||||
/// <div> "Hello, {ctx.props.name}" </div>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
// todo: force lifetime of source into T as a valid lifetime too
|
||||
// it's definitely possible, just needs some more messing around
|
||||
pub type Context<'src> = &'src Scope;
|
||||
|
||||
impl Scope {
|
||||
/// Access the children elements passed into the component
|
||||
pub fn children(&self) -> Vec<VNode> {
|
||||
todo!("Children API not yet implemented for component Context")
|
||||
}
|
||||
|
||||
/// Create a subscription that schedules a future render for the reference component
|
||||
pub fn schedule_update(&self) -> impl Fn() -> () {
|
||||
self.event_queue.schedule_update(&self)
|
||||
}
|
||||
|
||||
/// Create a suspended component from a future.
|
||||
///
|
||||
/// When the future completes, the component will be renderered
|
||||
pub fn suspend<'a, F: for<'b> FnOnce(&'b NodeCtx<'a>) -> VNode<'a> + 'a>(
|
||||
&'a self,
|
||||
_fut: impl Future<Output = LazyNodes<'a, F>>,
|
||||
) -> VNode<'a> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================
|
||||
// Render Implementation for Components
|
||||
// ================================================
|
||||
//
|
||||
impl Scope {
|
||||
// impl<'scope> Context<'scope> {
|
||||
/// Take a lazy VNode structure and actually build it with the context of the VDom's efficient VNode allocator.
|
||||
///
|
||||
/// This function consumes the context and absorb the lifetime, so these VNodes *must* be returned.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```ignore
|
||||
/// fn Component(ctx: Context, props: &()) -> VNode {
|
||||
/// // Lazy assemble the VNode tree
|
||||
/// let lazy_tree = html! {<div> "Hello World" </div>};
|
||||
///
|
||||
/// // Actually build the tree and allocate it
|
||||
/// ctx.render(lazy_tree)
|
||||
/// }
|
||||
///```
|
||||
pub fn render<'scope, F: for<'b> FnOnce(&'b NodeCtx<'scope>) -> VNode<'scope> + 'scope>(
|
||||
&'scope self,
|
||||
lazy_nodes: LazyNodes<'scope, F>,
|
||||
) -> DomTree {
|
||||
let ctx = NodeCtx {
|
||||
scope_ref: self,
|
||||
listener_id: 0.into(),
|
||||
};
|
||||
|
||||
DomTree {
|
||||
root: unsafe {
|
||||
std::mem::transmute::<VNode<'scope>, VNode<'static>>(lazy_nodes.into_vnode(&ctx))
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================
|
||||
// Hooks Implementation for Components
|
||||
// ================================================
|
||||
|
||||
// We need to pin the hook so it doesn't move as we initialize the list of hooks
|
||||
type Hook = Pin<Box<dyn std::any::Any>>;
|
||||
|
||||
impl Scope {
|
||||
// impl<'scope> Context<'scope> {
|
||||
/// Store a value between renders
|
||||
///
|
||||
/// - Initializer: closure used to create the initial hook state
|
||||
/// - Runner: closure used to output a value every time the hook is used
|
||||
/// - Cleanup: closure used to teardown the hook once the dom is cleaned up
|
||||
///
|
||||
/// ```ignore
|
||||
/// // use_ref is the simplest way of storing a value between renders
|
||||
/// pub fn use_ref<T: 'static>(initial_value: impl FnOnce() -> T + 'static) -> Rc<RefCell<T>> {
|
||||
/// use_hook(
|
||||
/// || Rc::new(RefCell::new(initial_value())),
|
||||
/// |state, _| state.clone(),
|
||||
/// |_| {},
|
||||
/// )
|
||||
/// }
|
||||
/// ```
|
||||
pub fn use_hook<'scope, InternalHookState: 'static, Output: 'scope>(
|
||||
&'scope self,
|
||||
|
||||
// The closure that builds the hook state
|
||||
initializer: impl FnOnce() -> InternalHookState,
|
||||
|
||||
// The closure that takes the hookstate and returns some value
|
||||
runner: impl FnOnce(&'scope mut InternalHookState) -> 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
|
||||
_cleanup: impl FnOnce(InternalHookState),
|
||||
) -> Output {
|
||||
let idx = *self.hookidx.borrow();
|
||||
|
||||
// Grab out the hook list
|
||||
let mut hooks = self.hooks.borrow_mut();
|
||||
|
||||
// If the idx is the same as the hook length, then we need to add the current hook
|
||||
if idx >= hooks.len() {
|
||||
let new_state = initializer();
|
||||
hooks.push(Box::pin(new_state));
|
||||
}
|
||||
|
||||
*self.hookidx.borrow_mut() += 1;
|
||||
|
||||
let stable_ref = hooks
|
||||
.get_mut(idx)
|
||||
.expect("Should not fail, idx is validated")
|
||||
.as_mut();
|
||||
|
||||
let pinned_state = unsafe { Pin::get_unchecked_mut(stable_ref) };
|
||||
|
||||
let internal_state = pinned_state.downcast_mut::<InternalHookState>().expect(
|
||||
r###"
|
||||
Unable to retrive the hook that was initialized in this index.
|
||||
Consult the `rules of hooks` to understand how to use hooks properly.
|
||||
|
||||
You likely used the hook in a conditional. Hooks rely on consistent ordering between renders.
|
||||
"###,
|
||||
);
|
||||
|
||||
// We extend the lifetime of the internal state
|
||||
runner(unsafe { &mut *(internal_state as *mut _) })
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================
|
||||
// Context API Implementation for Components
|
||||
// ================================================
|
||||
impl Scope {
|
||||
pub fn create_context<T: 'static>(&self, init: impl Fn() -> T) {
|
||||
let mut ctxs = self.shared_contexts.borrow_mut();
|
||||
let ty = TypeId::of::<T>();
|
||||
|
||||
let initialized = self.use_hook(
|
||||
|| false,
|
||||
|s| {
|
||||
let i = *s;
|
||||
*s = true;
|
||||
i
|
||||
},
|
||||
|_| {},
|
||||
);
|
||||
|
||||
match (initialized, ctxs.contains_key(&ty)) {
|
||||
// Do nothing, already initialized and already exists
|
||||
(true, true) => {}
|
||||
|
||||
// Needs to be initialized
|
||||
(false, false) => {
|
||||
ctxs.insert(ty, Rc::new(init())).unwrap();
|
||||
}
|
||||
|
||||
(false, true) => panic!("Cannot initialize two contexts of the same type"),
|
||||
(true, false) => panic!("Implementation failure resulted in missing context"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_use_context<T: 'static>(&self) -> Result<Rc<T>> {
|
||||
let ty = TypeId::of::<T>();
|
||||
|
||||
let mut scope = Some(self);
|
||||
|
||||
while let Some(inner) = scope {
|
||||
let shared_contexts = inner.shared_contexts.borrow();
|
||||
if let Some(shared_ctx) = shared_contexts.get(&ty) {
|
||||
return Ok(shared_ctx.clone().downcast().unwrap());
|
||||
} else {
|
||||
match inner.parent {
|
||||
Some(parid) => {
|
||||
let parent = inner
|
||||
.arena_link
|
||||
.try_get(parid)
|
||||
.map_err(|_| Error::FatalInternal("Failed to find parent"))?;
|
||||
|
||||
scope = Some(parent);
|
||||
}
|
||||
None => return Err(Error::MissingSharedContext),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(Error::MissingSharedContext)
|
||||
}
|
||||
|
||||
pub fn use_context<T: 'static>(&self) -> Rc<T> {
|
||||
self.try_use_context().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
// ==================================================================================
|
||||
// Supporting structs for the above abstractions
|
||||
// ==================================================================================
|
||||
|
||||
// We actually allocate the properties for components in their parent's properties
|
||||
// We then expose a handle to use those props for render in the form of "OpaqueComponent"
|
||||
pub(crate) type OpaqueComponent<'a> = dyn for<'b> Fn(Context<'b>) -> DomTree + 'a;
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct EventQueue(pub(crate) Rc<RefCell<Vec<HeightMarker>>>);
|
||||
|
||||
impl EventQueue {
|
||||
pub fn schedule_update(&self, source: &Scope) -> impl Fn() {
|
||||
let inner = self.clone();
|
||||
let marker = HeightMarker {
|
||||
height: source.height,
|
||||
idx: source.myidx,
|
||||
};
|
||||
move || inner.0.as_ref().borrow_mut().push(marker)
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper type that lets scopes be ordered by their height
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct HeightMarker {
|
||||
pub idx: ScopeIdx,
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
impl Ord for HeightMarker {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.height.cmp(&other.height)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for HeightMarker {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
// NodeCtx is used to build VNodes in the component's memory space.
|
||||
// This struct adds metadata to the final DomTree about listeners, attributes, and children
|
||||
#[derive(Clone)]
|
||||
pub struct NodeCtx<'a> {
|
||||
pub scope_ref: &'a Scope,
|
||||
pub listener_id: RefCell<usize>,
|
||||
}
|
||||
|
||||
impl<'a> NodeCtx<'a> {
|
||||
pub fn bump(&self) -> &'a Bump {
|
||||
&self.scope_ref.cur_frame().bump
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for NodeCtx<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Hash)]
|
||||
pub struct ContextId {
|
||||
// Which component is the scope in
|
||||
original: ScopeIdx,
|
||||
|
||||
// What's the height of the scope
|
||||
height: u32,
|
||||
|
||||
// Which scope is it (in order)
|
||||
id: u32,
|
||||
}
|
||||
|
||||
pub struct ActiveFrame {
|
||||
// We use a "generation" for users of contents in the bump frames to ensure their data isn't broken
|
||||
pub generation: RefCell<usize>,
|
||||
@@ -690,240 +992,6 @@ impl ActiveFrame {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// Properties passed down from the parent component are also directly accessible via the exposed "props" field.
|
||||
///
|
||||
/// ```ignore
|
||||
/// #[derive(Properties)]
|
||||
/// struct Props {
|
||||
/// name: String
|
||||
///
|
||||
/// }
|
||||
///
|
||||
/// fn example(ctx: Context, props: &Props -> VNode {
|
||||
/// html! {
|
||||
/// <div> "Hello, {ctx.props.name}" </div>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
// todo: force lifetime of source into T as a valid lifetime too
|
||||
// it's definitely possible, just needs some more messing around
|
||||
pub struct Context<'src> {
|
||||
pub idx: RefCell<usize>,
|
||||
|
||||
// pub scope: ScopeIdx,
|
||||
pub scope: &'src Scope,
|
||||
|
||||
pub _p: std::marker::PhantomData<&'src ()>,
|
||||
}
|
||||
|
||||
impl<'a> Context<'a> {
|
||||
/// Access the children elements passed into the component
|
||||
pub fn children(&self) -> Vec<VNode> {
|
||||
todo!("Children API not yet implemented for component Context")
|
||||
}
|
||||
|
||||
/// Create a subscription that schedules a future render for the reference component
|
||||
pub fn schedule_update(&self) -> impl Fn() -> () {
|
||||
self.scope.event_queue.schedule_update(&self.scope)
|
||||
}
|
||||
|
||||
/// Create a suspended component from a future.
|
||||
///
|
||||
/// When the future completes, the component will be renderered
|
||||
pub fn suspend<F: for<'b> FnOnce(&'b NodeCtx<'a>) -> VNode<'a> + 'a>(
|
||||
&self,
|
||||
_fut: impl Future<Output = LazyNodes<'a, F>>,
|
||||
) -> VNode<'a> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'scope> Context<'scope> {
|
||||
/// Take a lazy VNode structure and actually build it with the context of the VDom's efficient VNode allocator.
|
||||
///
|
||||
/// This function consumes the context and absorb the lifetime, so these VNodes *must* be returned.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```ignore
|
||||
/// fn Component(ctx: Context<Props>) -> VNode {
|
||||
/// // Lazy assemble the VNode tree
|
||||
/// let lazy_tree = html! {<div> "Hello World" </div>};
|
||||
///
|
||||
/// // Actually build the tree and allocate it
|
||||
/// ctx.render(lazy_tree)
|
||||
/// }
|
||||
///```
|
||||
pub fn render<F: for<'b> FnOnce(&'b NodeCtx<'scope>) -> VNode<'scope> + 'scope>(
|
||||
&self,
|
||||
lazy_nodes: LazyNodes<'scope, F>,
|
||||
) -> DomTree {
|
||||
let ctx = NodeCtx {
|
||||
scope_ref: self.scope,
|
||||
idx: 0.into(),
|
||||
};
|
||||
|
||||
DomTree {
|
||||
root: unsafe {
|
||||
std::mem::transmute::<VNode<'scope>, VNode<'static>>(lazy_nodes.into_vnode(&ctx))
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We need to pin the hook so it doesn't move as we initialize the list of hooks
|
||||
type Hook = Pin<Box<dyn std::any::Any>>;
|
||||
|
||||
impl<'scope> Context<'scope> {
|
||||
/// Store a value between renders
|
||||
///
|
||||
/// - Initializer: closure used to create the initial hook state
|
||||
/// - Runner: closure used to output a value every time the hook is used
|
||||
/// - Cleanup: closure used to teardown the hook once the dom is cleaned up
|
||||
///
|
||||
/// ```ignore
|
||||
/// // use_ref is the simplest way of storing a value between renders
|
||||
/// pub fn use_ref<T: 'static>(initial_value: impl FnOnce() -> T + 'static) -> Rc<RefCell<T>> {
|
||||
/// use_hook(
|
||||
/// || Rc::new(RefCell::new(initial_value())),
|
||||
/// |state, _| state.clone(),
|
||||
/// |_| {},
|
||||
/// )
|
||||
/// }
|
||||
/// ```
|
||||
pub fn use_hook<'c, InternalHookState: 'static, Output: 'scope>(
|
||||
&'c self,
|
||||
|
||||
// The closure that builds the hook state
|
||||
initializer: impl FnOnce() -> InternalHookState,
|
||||
|
||||
// The closure that takes the hookstate and returns some value
|
||||
runner: impl FnOnce(&'scope mut InternalHookState) -> 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
|
||||
_cleanup: impl FnOnce(InternalHookState),
|
||||
) -> Output {
|
||||
let idx = *self.idx.borrow();
|
||||
|
||||
// Grab out the hook list
|
||||
let mut hooks = self.scope.hooks.borrow_mut();
|
||||
|
||||
// If the idx is the same as the hook length, then we need to add the current hook
|
||||
if idx >= hooks.len() {
|
||||
let new_state = initializer();
|
||||
hooks.push(Box::pin(new_state));
|
||||
}
|
||||
|
||||
*self.idx.borrow_mut() += 1;
|
||||
|
||||
let stable_ref = hooks
|
||||
.get_mut(idx)
|
||||
.expect("Should not fail, idx is validated")
|
||||
.as_mut();
|
||||
|
||||
let pinned_state = unsafe { Pin::get_unchecked_mut(stable_ref) };
|
||||
|
||||
let internal_state = pinned_state.downcast_mut::<InternalHookState>().expect(
|
||||
r###"
|
||||
Unable to retrive the hook that was initialized in this index.
|
||||
Consult the `rules of hooks` to understand how to use hooks properly.
|
||||
|
||||
You likely used the hook in a conditional. Hooks rely on consistent ordering between renders.
|
||||
"###,
|
||||
);
|
||||
|
||||
// We extend the lifetime of the internal state
|
||||
runner(unsafe { &mut *(internal_state as *mut _) })
|
||||
}
|
||||
|
||||
fn create_context_provider<T: 'static>(&self, init: impl Fn() -> T) {}
|
||||
|
||||
fn try_consume_context<T: 'static>(&self) -> Result<&T> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn consume_context<T: 'static>(&self) -> &T {
|
||||
self.try_consume_context().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
mod support {
|
||||
use super::*;
|
||||
|
||||
// We actually allocate the properties for components in their parent's properties
|
||||
// We then expose a handle to use those props for render in the form of "OpaqueComponent"
|
||||
pub(crate) type OpaqueComponent<'a> = dyn for<'b> Fn(Context<'b>) -> DomTree + 'a;
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct EventQueue(pub(crate) Rc<RefCell<Vec<HeightMarker>>>);
|
||||
|
||||
impl EventQueue {
|
||||
pub fn schedule_update(&self, source: &Scope) -> impl Fn() {
|
||||
let inner = self.clone();
|
||||
let marker = HeightMarker {
|
||||
height: source.height,
|
||||
idx: source.myidx,
|
||||
};
|
||||
move || inner.0.as_ref().borrow_mut().push(marker)
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper type that lets scopes be ordered by their height
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct HeightMarker {
|
||||
pub idx: ScopeIdx,
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
impl Ord for HeightMarker {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.height.cmp(&other.height)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for HeightMarker {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
// NodeCtx is used to build VNodes in the component's memory space.
|
||||
// This struct adds metadata to the final DomTree about listeners, attributes, and children
|
||||
#[derive(Clone)]
|
||||
pub struct NodeCtx<'a> {
|
||||
pub scope_ref: &'a Scope,
|
||||
pub idx: RefCell<usize>,
|
||||
}
|
||||
|
||||
impl<'a> NodeCtx<'a> {
|
||||
pub fn bump(&self) -> &'a Bump {
|
||||
&self.scope_ref.cur_frame().bump
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for NodeCtx<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Hash)]
|
||||
pub struct ContextId {
|
||||
// Which component is the scope in
|
||||
original: ScopeIdx,
|
||||
|
||||
// What's the height of the scope
|
||||
height: u32,
|
||||
|
||||
// Which scope is it (in order)
|
||||
id: u32,
|
||||
}
|
||||
}
|
||||
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
Reference in new issue
Block a user