Feat: wire up rebuild

This commit is contained in:
Jonathan Kelley 2021-02-24 03:51:26 -05:00
parent 4d01455729
commit 06ae4fc178
11 changed files with 210 additions and 170 deletions

View file

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

View file

@ -1,6 +1,4 @@
use builder::{button};
use builder::button;
use dioxus_core::prelude::*;
fn main() {}

View file

@ -1,10 +1,11 @@
use crate::nodes::VNode;
use crate::prelude::*;
use crate::{nodes::VNode};
use bumpalo::Bump;
use hooks::Hook;
use log::info;
use std::{
any::TypeId, borrow::Borrow, cell::RefCell, future::Future, marker::PhantomData,
sync::atomic::AtomicUsize,
any::TypeId, borrow::Borrow, cell::RefCell, future::Future, marker::PhantomData, ops::Deref,
rc::Rc, sync::atomic::AtomicUsize,
};
/// Components in Dioxus use the "Context" object to interact with their lifecycle.
@ -35,6 +36,8 @@ pub struct Context<'src> {
pub(crate) hooks: &'src RefCell<Vec<*mut Hook>>,
pub(crate) bump: &'src Bump,
pub(crate) final_nodes: Rc<RefCell<Option<VNode<'static>>>>,
// holder for the src lifetime
// todo @jon remove this
pub _p: std::marker::PhantomData<&'src ()>,
@ -73,10 +76,10 @@ impl<'a> Context<'a> {
/// }
///```
pub fn view(self, lazy_nodes: impl FnOnce(&'a Bump) -> VNode<'a> + 'a) -> DomTree {
// pub fn view(self, lazy_nodes: impl for<'b> FnOnce(&'b Bump) -> VNode<'b> + 'a + 'p) -> DomTree {
// pub fn view<'p>(self, lazy_nodes: impl FnOnce(&'a Bump) -> VNode<'a> + 'a + 'p) -> DomTree {
// pub fn view(self, lazy_nodes: impl FnOnce(&'a Bump) -> VNode<'a> + 'a) -> VNode<'a> {
let _g = lazy_nodes(self.bump);
let safe_nodes = lazy_nodes(self.bump);
let unsafe_nodes = unsafe { std::mem::transmute::<VNode<'a>, VNode<'static>>(safe_nodes) };
self.final_nodes.deref().borrow_mut().replace(unsafe_nodes);
info!("lazy nodes have been generated");
DomTree {}
}
@ -187,8 +190,7 @@ mod context_api {
//!
//!
use std::{ops::Deref};
use std::ops::Deref;
pub struct RemoteState<T> {
inner: *const T,

View file

@ -16,34 +16,45 @@ impl EventTrigger {
pub fn new() -> Self {
todo!()
}
/// Create a new "start" event that boots up the virtual dom if it is paused
pub fn start_event() -> Self {
todo!()
}
}
pub enum VirtualEvent {
// the event to drain the current lifecycle queue
// Used to initate the dom
StartEvent,
// Real events
ClipboardEvent,
CompositionEvent,
KeyboardEvent,
FocusEvent,
FormEvent,
GenericEvent,
MouseEvent,
PointerEvent,
SelectionEvent,
TouchEvent,
UIEvent,
WheelEvent,
MediaEvent,
ImageEvent,
AnimationEvent,
TransitionEvent,
ClipboardEvent(ClipboardEvent),
CompositionEvent(CompositionEvent),
KeyboardEvent(KeyboardEvent),
FocusEvent(FocusEvent),
FormEvent(FormEvent),
GenericEvent(GenericEvent),
MouseEvent(MouseEvent),
PointerEvent(PointerEvent),
SelectionEvent(SelectionEvent),
TouchEvent(TouchEvent),
UIEvent(UIEvent),
WheelEvent(WheelEvent),
MediaEvent(MediaEvent),
ImageEvent(ImageEvent),
AnimationEvent(AnimationEvent),
TransitionEvent(TransitionEvent),
OtherEvent,
}
// these should reference the underlying event
pub struct ClipboardEvent {}
pub struct CompositionEvent {}
pub struct KeyboardEvent {}
pub struct FocusEvent {}
pub struct FormEvent {}
pub struct GenericEvent {}
pub struct MouseEvent {}
pub struct PointerEvent {}
pub struct SelectionEvent {}
pub struct TouchEvent {}
pub struct UIEvent {}
pub struct WheelEvent {}
pub struct MediaEvent {}
pub struct ImageEvent {}
pub struct AnimationEvent {}
pub struct TransitionEvent {}

View file

@ -101,9 +101,7 @@ pub(crate) mod innerlude {
pub type FC<P> = for<'scope> fn(Context<'scope>, &'scope P) -> DomTree;
mod fc2 {
}
mod fc2 {}
// pub type FC<'a, P: 'a> = for<'scope> fn(Context<'scope>, &'scope P) -> DomTree;
// pub type FC<P> = for<'scope, 'r> fn(Context<'scope>, &'scope P) -> DomTree;
// pub type FC<P> = for<'scope, 'r> fn(Context<'scope>, &'r P) -> VNode<'scope>;

View file

@ -12,6 +12,7 @@ use std::{
marker::PhantomData,
ops::{Deref, DerefMut},
sync::atomic::AtomicUsize,
sync::atomic::Ordering,
todo,
};
@ -40,34 +41,23 @@ pub struct Scope {
// lying, cheating reference >:(
pub props: Box<dyn std::any::Any>,
// pub props: Box<dyn Properties>,
//
// pub props_type: TypeId,
pub caller: *const (),
}
impl Scope {
// create a new scope from a function
pub fn new<'a, P1, P2: 'static>(
// pub fn new<'a, P: Properties, PFree: P + 'a, PLocked: P + 'static>(
f: FC<P1>,
props: P1,
parent: Option<Index>,
) -> Self
// where
// PFree: 'a,
// PLocked: 'static,
{
// Capture the props type
// let props_type = TypeId::of::<P>();
pub fn new<'a, P1, P2: 'static>(f: FC<P1>, props: P1, parent: Option<Index>) -> Self {
let hook_arena = typed_arena::Arena::new();
let hooks = RefCell::new(Vec::new());
// Capture the caller
let caller = f as *const ();
let listeners = Vec::new();
let listeners: Vec<Box<dyn Fn()>> = vec![Box::new(|| {
log::info!("Base listener called");
})];
let old_frame = BumpFrame {
bump: Bump::new(),
@ -84,16 +74,11 @@ impl Scope {
// box the props
let props = Box::new(props);
// erase the lifetime
// we'll manage this with dom lifecycle
let props = unsafe { std::mem::transmute::<_, Box<P2>>(props) };
// todo!()
Self {
hook_arena,
hooks,
// props_type,
caller,
frames,
listeners,
@ -102,13 +87,6 @@ impl Scope {
}
}
/// Update this component's props with a new set of props, remotely
///
///
pub(crate) fn update_props<'a, P>(&self, _new_props: P) -> crate::error::Result<()> {
Ok(())
}
/// 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
///
@ -120,67 +98,67 @@ impl Scope {
frame
};
let node_slot = std::rc::Rc::new(RefCell::new(None));
let ctx: Context<'bump> = Context {
arena: &self.hook_arena,
hooks: &self.hooks,
bump: &frame.bump,
idx: 0.into(),
_p: PhantomData {},
final_nodes: node_slot.clone(),
};
unsafe {
/*
SAFETY ALERT
This particular usage of transmute is outlined in its docs https://doc.rust-lang.org/std/mem/fn.transmute.html
We hide the generic bound on the function item by casting it to raw pointer. When the function is actually called,
we transmute the function back using the props as reference.
we could do a better check to make sure that the TypeID is correct before casting
--
This is safe because we check that the generic type matches before casting.
*/
// we use plocked to be able to remove the borrowed lifetime
// these lifetimes could be very broken, so we need to dynamically manage them
let caller = std::mem::transmute::<*const (), FC<PLocked>>(self.caller);
let props = self.props.downcast_ref::<PLocked>().unwrap();
// Note that the actual modification of the vnode head element occurs during this call
let _nodes: DomTree = caller(ctx, props);
todo!("absorb domtree into self")
// let nodes: VNode<'bump> = caller(ctx, props);
// let unsafe_node = std::mem::transmute::<VNode<'bump>, VNode<'static>>(nodes);
// frame.head_node = unsafe_node;
/*
SAFETY ALERT
DO NOT USE THIS VNODE WITHOUT THE APPOPRIATE ACCESSORS.
KEEPING THIS STATIC REFERENCE CAN LEAD TO UB.
Some things to note:
- The VNode itself is bound to the lifetime, but it itself is owned by scope.
- The VNode has a private API and can only be used from accessors.
- Public API cannot drop or destructure VNode
*/
// the nodes we care about have been unsafely extended to a static lifetime in context
frame.head_node = node_slot
.deref()
.borrow_mut()
.take()
.expect("Viewing did not happen");
}
/*
SAFETY ALERT
This particular usage of transmute is outlined in its docs https://doc.rust-lang.org/std/mem/fn.transmute.html
We hide the generic bound on the function item by casting it to raw pointer. When the function is actually called,
we transmute the function back using the props as reference.
we could do a better check to make sure that the TypeID is correct before casting
--
This is safe because we check that the generic type matches before casting.
*/
/*
SAFETY ALERT
DO NOT USE THIS VNODE WITHOUT THE APPOPRIATE ACCESSORS.
KEEPING THIS STATIC REFERENCE CAN LEAD TO UB.
Some things to note:
- The VNode itself is bound to the lifetime, but it itself is owned by scope.
- The VNode has a private API and can only be used from accessors.
- Public API cannot drop or destructure VNode
*/
}
/// Accessor to get the root node and its children (safely)\
/// Scope is self-referntial, so we are forced to use the 'static lifetime to cheat
pub fn current_root_node<'bump>(&'bump self) -> &'bump VNode<'bump> {
pub fn new_frame<'bump>(&'bump self) -> &'bump VNode<'bump> {
self.frames.current_head_node()
}
pub fn prev_root_node<'bump>(&'bump self) -> &'bump VNode<'bump> {
todo!()
pub fn old_frame<'bump>(&'bump self) -> &'bump VNode<'bump> {
self.frames.prev_head_node()
}
}
pub struct BumpFrame {
pub bump: Bump,
pub head_node: VNode<'static>,
}
// todo, do better with the active frame stuff
// somehow build this vnode with a lifetime tied to self
// This root node has "static" lifetime, but it's really not static.
@ -192,6 +170,11 @@ pub struct ActiveFrame {
pub frames: [BumpFrame; 2],
}
pub struct BumpFrame {
pub bump: Bump,
pub head_node: VNode<'static>,
}
impl ActiveFrame {
fn from_frames(a: BumpFrame, b: BumpFrame) -> Self {
Self {
@ -201,8 +184,23 @@ impl ActiveFrame {
}
fn current_head_node<'b>(&'b self) -> &'b VNode<'b> {
let cur_idx = self.idx.borrow().load(std::sync::atomic::Ordering::Relaxed) % 1;
let raw_node = &self.frames[cur_idx];
let raw_node = match self.idx.borrow().load(Ordering::Relaxed) & 1 != 0 {
true => &self.frames[0],
false => &self.frames[1],
};
unsafe {
let unsafe_head = &raw_node.head_node;
let safe_node = std::mem::transmute::<&VNode<'static>, &VNode<'b>>(unsafe_head);
safe_node
}
}
fn prev_head_node<'b>(&'b self) -> &'b VNode<'b> {
let raw_node = match self.idx.borrow().load(Ordering::Relaxed) & 1 == 0 {
true => &self.frames[0],
false => &self.frames[1],
};
unsafe {
let unsafe_head = &raw_node.head_node;
let safe_node = std::mem::transmute::<&VNode<'static>, &VNode<'b>>(unsafe_head);
@ -211,8 +209,8 @@ impl ActiveFrame {
}
fn next(&mut self) -> &mut BumpFrame {
self.idx.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let cur = self.idx.borrow().load(std::sync::atomic::Ordering::Relaxed);
self.idx.fetch_add(1, Ordering::Relaxed);
let cur = self.idx.borrow().load(Ordering::Relaxed);
match cur % 1 {
1 => &mut self.frames[1],
0 => &mut self.frames[0],
@ -276,7 +274,7 @@ mod tests {
})
}
fn child_example(ctx: Context, props: &ExampleProps) -> DomTree {
fn child_example<'b>(ctx: Context<'b>, props: &'b ExampleProps) -> DomTree {
ctx.view(move |b| {
div(b)
.child(text(props.name))

View file

@ -78,6 +78,34 @@ impl VirtualDom {
}
}
//// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom
pub fn rebuild(&mut self) -> Result<EditList<'_>> {
// Reset and then build a new diff machine
// The previous edit list cannot be around while &mut is held
// Make sure variance doesnt break this
self.diff_bump.reset();
let mut diff_machine = DiffMachine::new(&self.diff_bump);
// this is still a WIP
// we'll need to re-fecth all the scopes that were changed and build the diff machine
// fetch the component again
let component = self
.components
.get_mut(self.base_scope)
.expect("Root should always exist");
component.run::<()>();
let old = component.old_frame();
let new = component.new_frame();
dbg!(old);
dbg!(new);
diff_machine.diff_node(old, new);
Ok(diff_machine.consume())
}
/// This method is the most sophisticated way of updating the virtual dom after an external event has been triggered.
///
/// Given a synthetic event, the component that triggered the event, and the index of the callback, this runs the virtual
@ -102,17 +130,16 @@ impl VirtualDom {
/// }
///
/// ```
pub fn progress_with_event(&mut self, evt: EventTrigger) -> Result<EditList<'_>> {
pub fn progress_with_event(&mut self, event: EventTrigger) -> Result<EditList<'_>> {
let EventTrigger {
component_id,
listener_id,
event: _,
} = evt;
} = event;
let component = self
.components
.get(component_id)
// todo: update this with a dedicated error type so implementors know what went wrong
.expect("Component should exist if an event was triggered");
let listener = component
@ -121,47 +148,37 @@ impl VirtualDom {
.expect("Listener should exist if it was triggered")
.as_ref();
// Run the callback
// This should cause internal state to progress, dumping events into the event queue
// todo: integrate this with a tracing mechanism exposed to a dev tool
// Run the callback with the user event
listener();
// Run through our events, tagging which Indexes are receiving updates
// Prop updates take prescedence over subscription updates
// Run all prop updates *first* as they will cascade into children.
// *then* run the non-prop updates that were not already covered by props
// Mark dirty components. Descend from the highest node until all dirty nodes are updated.
let mut affected_components = Vec::new();
// It's essentially draining the vec, but with some dancing to release the RefMut
// We also want to be able to push events into the queue from processing the event
while let Some(event) = {
let new_evt = self.event_queue.as_ref().borrow_mut().pop_front();
new_evt
} {
while let Some(event) = self.pop_event() {
if let Some(component_idx) = event.index() {
affected_components.push(component_idx);
}
self.process_lifecycle(event)?;
}
// Reset and then build a new diff machine
// The previous edit list cannot be around while &mut is held
// Make sure variance doesnt break this
self.diff_bump.reset();
let diff_machine = DiffMachine::new(&self.diff_bump);
Ok(diff_machine.consume())
todo!()
}
/// Using mutable access to the Virtual Dom, progress a given lifecycle event
fn process_lifecycle(&mut self, LifecycleEvent { event_type }: LifecycleEvent) -> Result<()> {
match event_type {
// Component needs to be mounted to the virtual dom
LifecycleType::Mount { to: _, under: _, props: _ } => {}
LifecycleType::Mount {
to: _,
under: _,
props: _,
} => {}
// The parent for this component generated new props and the component needs update
LifecycleType::PropsChanged { props: _, component: _ } => {}
LifecycleType::PropsChanged {
props: _,
component: _,
} => {}
// Component was messaged via the internal subscription service
LifecycleType::Callback { component: _ } => {}
@ -170,6 +187,11 @@ impl VirtualDom {
Ok(())
}
/// Pop the top event of the internal lifecycle event queu
pub fn pop_event(&self) -> Option<LifecycleEvent> {
self.event_queue.as_ref().borrow_mut().pop_front()
}
/// With access to the virtual dom, schedule an update to the Root component's props.
/// This generates the appropriate Lifecycle even. It's up to the renderer to actually feed this lifecycle event
/// back into the event system to get an edit list.
@ -215,10 +237,30 @@ pub enum LifecycleType {
impl LifecycleEvent {
fn index(&self) -> Option<Index> {
match &self.event_type {
LifecycleType::Mount { to: _, under: _, props: _ } => None,
LifecycleType::Mount {
to: _,
under: _,
props: _,
} => None,
LifecycleType::PropsChanged { component, .. }
| LifecycleType::Callback { component } => Some(component.clone()),
}
}
}
mod tests {
use super::*;
#[test]
fn start_dom() {
let mut dom = VirtualDom::new(|ctx, props| {
ctx.view(|bump| {
use crate::builder::*;
div(bump).child(text("hello, world")).finish()
})
});
let edits = dom.rebuild().unwrap();
println!("{:#?}", edits);
}
}

View file

@ -4,15 +4,10 @@ use dioxus_web::WebsysRenderer;
fn main() {
// todo: set this up so the websys render can spawn itself rather than having to wrap it
// almost like bundling an executor with the wasm version
wasm_bindgen_futures::spawn_local(async {
WebsysRenderer::new(Example)
.run()
.await
.expect("Dioxus Failed! This should *not* happen!")
});
wasm_bindgen_futures::spawn_local(WebsysRenderer::start(Example));
}
static Example: FC<()> = |ctx, props| {
static Example: FC<()> = |ctx, _props| {
ctx.view(html! {
<div>
"Hello world!"

View file

@ -9,7 +9,7 @@ fn main() {
wasm_logger::init(wasm_logger::Config::new(log::Level::Debug));
console_error_panic_hook::set_once();
WebsysRenderer::new(|ctx, _| {
wasm_bindgen_futures::spawn_local(WebsysRenderer::start(|ctx, _| {
ctx.view(html! {
<div>
<div class="flex items-center justify-center flex-col">
@ -43,5 +43,5 @@ fn main() {
</div>
</div>
})
});
}));
}

View file

@ -1,6 +1,6 @@
use dioxus_core::changelist::Edit;
use fxhash::FxHashMap;
use log::{debug, info, log};
use log::{debug};
use wasm_bindgen::{closure::Closure, JsCast};
use web_sys::{window, Document, Element, Event, Node};
@ -95,7 +95,7 @@ impl PatchMachine {
self.templates.get(&id)
}
pub fn init_events_trampoline(&mut self, mut trampoline: ()) {
pub fn init_events_trampoline(&mut self, _trampoline: ()) {
todo!("Event trampoline not a thing anymore")
// pub fn init_events_trampoline(&mut self, mut trampoline: EventsTrampoline) {
// self.callback = Some(Closure::wrap(Box::new(move |event: &web_sys::Event| {

View file

@ -5,20 +5,14 @@
use fxhash::FxHashMap;
use web_sys::{window, Document, Element, Event, Node};
use dioxus::prelude::VElement;
pub use dioxus_core as dioxus;
use dioxus_core::{
events::EventTrigger,
prelude::{bumpalo::Bump, html, DiffMachine, VNode, VirtualDom, FC},
prelude::{VirtualDom, FC},
};
use futures::{
channel::mpsc::{self, Sender},
future, SinkExt, StreamExt,
};
use mpsc::UnboundedSender;
use futures::{channel::mpsc, SinkExt, StreamExt};
pub mod interpreter;
use interpreter::PatchMachine;
/// The `WebsysRenderer` provides a way of rendering a Dioxus Virtual DOM to the browser's DOM.
/// Under the hood, we leverage WebSys and interact directly with the DOM
@ -27,7 +21,7 @@ pub struct WebsysRenderer {
internal_dom: VirtualDom,
// this should be a component index
event_map: FxHashMap<(u32, u32), u32>,
_event_map: FxHashMap<(u32, u32), u32>,
}
impl WebsysRenderer {
@ -57,34 +51,36 @@ impl WebsysRenderer {
pub fn from_vdom(dom: VirtualDom) -> Self {
Self {
internal_dom: dom,
event_map: FxHashMap::default(),
_event_map: FxHashMap::default(),
}
}
pub async fn run(&mut self) -> dioxus_core::error::Result<()> {
let (mut sender, mut receiver) = mpsc::unbounded::<EventTrigger>();
sender
.send(EventTrigger::start_event())
.await
.expect("Should not fail");
let (sender, mut receiver) = mpsc::unbounded::<EventTrigger>();
let body = prepare_websys_dom();
let mut patch_machine = PatchMachine::new(body.clone());
let mut patch_machine = interpreter::PatchMachine::new(body.clone());
let root_node = body.first_child().unwrap();
patch_machine.stack.push(root_node);
self.internal_dom
.rebuild()?
.into_iter()
.for_each(|edit| patch_machine.handle_edit(&edit));
// Event loop waits for the receiver to finish up
// TODO! Connect the sender to the virtual dom's suspense system
// Suspense is basically an external event that can force renders to specific nodes
while let Some(event) = receiver.next().await {
let diffs = self.internal_dom.progress_with_event(event)?;
for edit in &diffs {
patch_machine.handle_edit(edit);
}
self.internal_dom
.progress_with_event(event)?
.into_iter()
.for_each(|edit| {
patch_machine.handle_edit(&edit);
});
}
Ok(()) // should actually never return from this, should be an error
Ok(()) // should actually never return from this, should be an error, rustc just cant see it
}
}