diff --git a/examples/borrowed.rs b/examples/borrowed.rs new file mode 100644 index 000000000..0be0c668b --- /dev/null +++ b/examples/borrowed.rs @@ -0,0 +1,86 @@ +#![allow(non_snake_case)] +//! Example: Extremely nested borrowing +//! ----------------------------------- +//! +//! Dioxus manages borrow lifetimes for you. This means any child may borrow from its parent. However, it is not possible +//! to hand out an &mut T to children - all props are consumed by &P, so you'd only get an &&mut T. +//! +//! How does it work? +//! +//! Dioxus will manually drop closures and props - things that borrow data before the component is ran again. This is done +//! "bottom up" from the lowest child all the way to the initiating parent. As it traverses each listener and prop, the +//! drop implementation is manually called, freeing any memory and ensuring that memory is not leaked. +//! +//! We cannot drop from the parent to the children - if the drop implementation modifies the data, downstream references +//! might be broken since we take an &mut T and and &T to the data. Instead, we work bottom up, making sure to remove any +//! potential references to the data before finally giving out an &mut T. This prevents us from mutably aliasing the data, +//! and is proven to be safe with MIRI. + +use dioxus::prelude::*; + +fn main() { + dioxus::desktop::launch(App, |c| c); +} + +fn App<'a>(cx: Context<'a, ()>) -> DomTree<'a> { + let text: &'a mut Vec = cx.use_hook(|_| vec![String::from("abc=def")], |f| f, |_| {}); + + let first = text.get_mut(0).unwrap(); + + cx.render(rsx! { + div { + Child1 { + text: first + } + } + }) +} + +#[derive(Props)] +struct C1Props<'a> { + text: &'a mut String, +} + +impl<'a> Drop for C1Props<'a> { + fn drop(&mut self) {} +} + +fn Child1<'a>(cx: Context<'a, C1Props>) -> DomTree<'a> { + let (left, right) = cx.text.split_once("=").unwrap(); + + cx.render(rsx! { + div { + Child2 { text: left } + Child2 { text: right } + } + }) +} + +#[derive(Props)] +struct C2Props<'a> { + text: &'a str, +} +impl<'a> Drop for C2Props<'a> { + fn drop(&mut self) { + todo!() + } +} + +fn Child2<'a>(cx: Context<'a, C2Props>) -> DomTree<'a> { + cx.render(rsx! { + Child3 { + text: cx.text + } + }) +} + +#[derive(Props)] +struct C3Props<'a> { + text: &'a str, +} + +fn Child3<'a>(cx: Context<'a, C3Props>) -> DomTree<'a> { + cx.render(rsx! { + div { "{cx.text}"} + }) +} diff --git a/packages/core-macro/README.md b/packages/core-macro/README.md new file mode 100644 index 000000000..d9527a411 --- /dev/null +++ b/packages/core-macro/README.md @@ -0,0 +1,7 @@ +# core-macro + +This crate implements these macros: +- `format_args_f`: for f-string interpolation inside of text blocks +- `Props`: derive macro for typed-builder with props configurations +- +- diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 450b64b23..00038baca 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -39,9 +39,12 @@ smallvec = "1.6.1" slab = "0.4.3" +futures-channel = "0.3.16" + [dev-dependencies] anyhow = "1.0.42" +async-std = { version = "1.9.0", features = ["attributes"] } dioxus-html = { path = "../html" } diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index 18fdee8a0..1759aeebc 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -33,19 +33,22 @@ impl ElementId { } type Shared = Rc>; +type TaskReceiver = futures_channel::mpsc::UnboundedReceiver; +type TaskSender = futures_channel::mpsc::UnboundedSender; /// These are resources shared among all the components and the virtualdom itself #[derive(Clone)] pub struct SharedResources { pub components: Rc>>, - pub event_queue: Shared>, - - pub events: Shared>, - pub(crate) heuristics: Shared, - pub tasks: Shared>, + /// + pub task_sender: TaskSender, + + pub task_receiver: Shared, + + pub async_tasks: Shared>, /// We use a SlotSet to keep track of the keys that are currently being used. /// However, we don't store any specific data since the "mirror" @@ -63,30 +66,39 @@ impl SharedResources { // elements are super cheap - the value takes no space let raw_elements = Slab::with_capacity(2000); - let event_queue = Rc::new(RefCell::new(Vec::new())); - let tasks = Vec::new(); + // let pending_events = Rc::new(RefCell::new(Vec::new())); + + let (sender, receiver) = futures_channel::mpsc::unbounded(); + let heuristics = HeuristicsEngine::new(); + // we allocate this task setter once to save us from having to allocate later let task_setter = { - let queue = event_queue.clone(); + let queue = sender.clone(); let components = components.clone(); Rc::new(move |idx: ScopeId| { let comps = unsafe { &*components.get() }; if let Some(scope) = comps.get(idx.0) { - queue.borrow_mut().push(HeightMarker { - height: scope.height, - idx, - }) + queue + .unbounded_send(EventTrigger::new( + VirtualEvent::ScheduledUpdate { + height: scope.height, + }, + idx, + None, + EventPriority::High, + )) + .expect("The event queu receiver should *never* be dropped"); } - }) + }) as Rc }; Self { - event_queue, components, - tasks: Rc::new(RefCell::new(FuturesUnordered::new())), - events: Rc::new(RefCell::new(tasks)), + async_tasks: Rc::new(RefCell::new(FuturesUnordered::new())), + task_receiver: Rc::new(RefCell::new(receiver)), + task_sender: sender, heuristics: Rc::new(RefCell::new(heuristics)), raw_elements: Rc::new(RefCell::new(raw_elements)), task_setter, @@ -136,12 +148,7 @@ impl SharedResources { /// return the id, freeing the space of the original node pub fn collect_garbage(&self, id: ElementId) { - let mut r: RefMut> = self.raw_elements.borrow_mut(); - r.remove(id.0); - } - - pub fn borrow_queue(&self) -> RefMut> { - self.event_queue.borrow_mut() + self.raw_elements.borrow_mut().remove(id.0); } pub fn insert_scope_with_key(&self, f: impl FnOnce(ScopeId) -> Scope) -> ScopeId { @@ -157,7 +164,7 @@ impl SharedResources { } pub fn submit_task(&self, task: FiberTask) -> TaskHandle { - self.tasks.borrow_mut().push(task); + self.async_tasks.borrow_mut().push(task); TaskHandle {} } } diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index f640e1f4c..475bc530e 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -65,10 +65,11 @@ //! - https://hacks.mozilla.org/2019/03/fast-bump-allocated-virtual-doms-with-rust-and-wasm/ use crate::{arena::SharedResources, innerlude::*}; +use futures_util::Future; use fxhash::{FxBuildHasher, FxHashMap, FxHashSet}; use smallvec::{smallvec, SmallVec}; -use std::{any::Any, cell::Cell, cmp::Ordering}; +use std::{any::Any, cell::Cell, cmp::Ordering, pin::Pin}; use DomEdit::*; /// Instead of having handles directly over nodes, Dioxus uses simple u32 as node IDs. @@ -79,16 +80,32 @@ use DomEdit::*; /// nodes as the diffing algorithm descenes through the tree. This means that whatever is on top of the stack will receive /// any modifications that follow. This technique enables the diffing algorithm to avoid directly handling or storing any /// target-specific Node type as well as easily serializing the edits to be sent over a network or IPC connection. -pub trait RealDom<'a> { +pub trait RealDom { fn raw_node_as_any(&self) -> &mut dyn Any; + + /// Essentially "are we out of time to do more work?" + /// Right now, defaults to "no" giving us unlimited time to process work. + /// This will lead to blocking behavior in the UI, so implementors will want to override this. + fn must_commit(&self) -> bool { + false + } + + fn commit_edits<'a>(&mut self, edits: &mut Vec>) {} + + // pause the virtualdom until the main loop is ready to process more work + fn wait_until_ready<'s>(&'s mut self) -> Pin + 's>> { + // + Box::pin(async { + // + }) + } } -pub struct DiffMachine<'real, 'bump> { - pub real_dom: &'real dyn RealDom<'bump>, - +pub struct DiffMachine<'r, 'bump> { + // pub real_dom: &'real dyn RealDom, pub vdom: &'bump SharedResources, - pub edits: &'real mut Vec>, + pub edits: &'r mut Vec>, pub scope_stack: SmallVec<[ScopeId; 5]>, @@ -99,15 +116,13 @@ pub struct DiffMachine<'real, 'bump> { pub seen_scopes: FxHashSet, } -impl<'real, 'bump> DiffMachine<'real, 'bump> { +impl<'r, 'bump> DiffMachine<'r, 'bump> { pub(crate) fn new( - edits: &'real mut Vec>, - dom: &'real dyn RealDom<'bump>, + edits: &'r mut Vec>, cur_scope: ScopeId, shared: &'bump SharedResources, ) -> Self { Self { - real_dom: dom, edits, scope_stack: smallvec![cur_scope], vdom: shared, @@ -121,12 +136,10 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { /// /// This will PANIC if any component elements are passed in. pub fn new_headless( - edits: &'real mut Vec>, - dom: &'real dyn RealDom<'bump>, + edits: &'r mut Vec>, shared: &'bump SharedResources, ) -> Self { Self { - real_dom: dom, edits, scope_stack: smallvec![ScopeId(0)], vdom: shared, @@ -135,6 +148,14 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { } } + // + pub fn diff_scope(&mut self, id: ScopeId) -> Result<()> { + let component = self.get_scope_mut(&id).ok_or_else(|| Error::NotMounted)?; + let (old, new) = (component.frames.wip_head(), component.frames.fin_head()); + self.diff_node(old, new); + Ok(()) + } + // Diff the `old` node with the `new` node. Emits instructions to modify a // physical DOM node that reflects `old` into something that reflects `new`. // @@ -396,11 +417,7 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { } = el; let real_id = self.vdom.reserve_node(); - if let Some(namespace) = namespace { - self.edit_create_element(tag_name, Some(namespace), real_id) - } else { - self.edit_create_element(tag_name, None, real_id) - }; + self.edit_create_element(tag_name, *namespace, real_id); dom_id.set(Some(real_id)); let cur_scope = self.current_scope().unwrap(); @@ -1451,6 +1468,29 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> { }); } + pub(crate) fn edit_set_attribute_ns( + &mut self, + attribute: &'bump Attribute, + namespace: &'bump str, + ) { + let Attribute { + name, + value, + is_static, + is_volatile, + // namespace, + .. + } = attribute; + // field: &'static str, + // value: &'bump str, + // ns: Option<&'static str>, + self.edits.push(SetAttribute { + field: name, + value, + ns: Some(namespace), + }); + } + pub(crate) fn edit_remove_attribute(&mut self, attribute: &Attribute) { let name = attribute.name; self.edits.push(RemoveAttribute { name }); @@ -1627,131 +1667,3 @@ fn compare_strs(a: &str, b: &str) -> bool { true } } - -// // Now we will iterate from the end of the new children back to the -// // beginning, diffing old children we are reusing and if they aren't in the -// // LIS moving them to their new destination, or creating new children. Note -// // that iterating in reverse order lets us use `Node.prototype.insertBefore` -// // to move/insert children. -// // -// // But first, we ensure that we have a child on the change list stack that -// // we can `insertBefore`. We handle this once before looping over `new` -// // children, so that we don't have to keep checking on every loop iteration. -// if shared_suffix_count > 0 { -// // There is a shared suffix after these middle children. We will be -// // inserting before that shared suffix, so add the first child of that -// // shared suffix to the change list stack. -// // -// // [... parent] - -// // TODO - -// // self.edits -// // .go_down_to_child(old_shared_suffix_start - removed_count); -// // [... parent first_child_of_shared_suffix] -// } else { -// // There is no shared suffix coming after these middle children. -// // Therefore we have to process the last child in `new` and move it to -// // the end of the parent's children if it isn't already there. -// let last_index = new.len() - 1; -// // uhhhh why an unwrap? -// let last = new.last().unwrap(); -// // let last = new.last().unwrap_throw(); -// new = &new[..new.len() - 1]; -// if shared_keys.contains(&last.key()) { -// let old_index = new_index_to_old_index[last_index]; -// let temp = old_index_to_temp[old_index]; -// // [... parent] -// // self.go_down_to_temp_child(temp); -// // [... parent last] -// self.diff_node(&old[old_index], last); - -// if new_index_is_in_lis.contains(&last_index) { -// // Don't move it, since it is already where it needs to be. -// } else { -// // self.commit_traversal(); -// // [... parent last] -// // self.append_child(); -// // [... parent] -// // self.go_down_to_temp_child(temp); -// // [... parent last] -// } -// } else { -// // self.commit_traversal(); -// // [... parent] -// let meta = self.create_vnode(last); - -// // [... parent last] -// // self.append_child(); -// // [... parent] -// // self.go_down_to_reverse_child(0); -// // [... parent last] -// } -// } - -// for (new_index, new_child) in new.iter().enumerate().rev() { -// let old_index = new_index_to_old_index[new_index]; -// if old_index == u32::MAX as usize { -// debug_assert!(!shared_keys.contains(&new_child.key())); -// // self.commit_traversal(); -// // [... parent successor] -// let meta = self.create_vnode(new_child); -// // [... parent successor new_child] -// self.edit_insert_after(meta.added_to_stack); -// // self.insert_before(); -// // [... parent new_child] -// } else { -// debug_assert!(shared_keys.contains(&new_child.key())); -// let temp = old_index_to_temp[old_index]; -// debug_assert_ne!(temp, u32::MAX); - -// if new_index_is_in_lis.contains(&new_index) { -// // [... parent successor] -// // self.go_to_temp_sibling(temp); -// // [... parent new_child] -// } else { -// // self.commit_traversal(); -// // [... parent successor] -// // self.push_temporary(temp); -// // [... parent successor new_child] -// // self.insert_before(); -// // [... parent new_child] -// } - -// self.diff_node(&old[old_index], new_child); -// } -// } - -// Save each of the old children whose keys are reused in the new -// children -// let reused_children = vec![]; -// let mut old_index_to_temp = vec![u32::MAX; old.len()]; -// let mut start = 0; -// loop { -// let end = (start..old.len()) -// .find(|&i| { -// let key = old[i].key(); -// !shared_keys.contains(&key) -// }) -// .unwrap_or(old.len()); - -// if end - start > 0 { -// // self.commit_traversal(); -// // let mut t = 5; -// let mut t = self.save_children_to_temporaries( -// shared_prefix_count + start, -// shared_prefix_count + end, -// ); -// for i in start..end { -// old_index_to_temp[i] = t; -// t += 1; -// } -// } - -// debug_assert!(end <= old.len()); -// if end == old.len() { -// break; -// } else { -// start = end + 1; -// } -// } diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 3c8cc13c8..17ec98d81 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -10,7 +10,7 @@ use std::{ }; use crate::{ - innerlude::{ElementId, HeightMarker, ScopeId}, + innerlude::{ElementId, ScopeId}, VNode, }; @@ -28,18 +28,35 @@ pub struct EventTrigger { /// The priority of the event pub priority: EventPriority, } - impl EventTrigger { - pub fn new_from_task(originator: ScopeId, hook_idx: usize) -> Self { - Self { - originator, - event: VirtualEvent::AsyncEvent { hook_idx }, - priority: EventPriority::Low, - real_node_id: None, + pub fn make_key(&self) -> EventKey { + EventKey { + originator: self.originator, + priority: self.priority, } } } +#[derive(PartialEq, Eq)] +pub struct EventKey { + /// The originator of the event trigger + pub originator: ScopeId, + /// The priority of the event + pub priority: EventPriority, + // TODO: add the time that the event was queued +} + +impl PartialOrd for EventKey { + fn partial_cmp(&self, other: &Self) -> Option { + todo!() + } +} +impl Ord for EventKey { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + todo!() + } +} + /// Priority of Event Triggers. /// /// Internally, Dioxus will abort work that's taking too long if new, more important, work arrives. Unlike React, Dioxus @@ -47,7 +64,7 @@ impl EventTrigger { /// implement this form of scheduling internally, however Dioxus will perform its own scheduling as well. /// /// The ultimate goal of the scheduler is to manage latency of changes, prioritizing "flashier" changes over "subtler" changes. -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)] pub enum EventPriority { /// Garbage collection is a type of work than can be scheduled around other work, but must be completed in a specific /// order. The GC must be run for a component before any other future work for that component is run. Otherwise, @@ -92,6 +109,45 @@ impl EventTrigger { } pub enum VirtualEvent { + /// Generated during diffing to signal that a component's nodes to be given back + /// + /// Typically has a high priority + /// + /// If an event is scheduled for a component that has "garbage", that garabge will be cleaned up before the event can + /// be processed. + GarbageCollection, + + /// + DiffComponent, + + /// A type of "immediate" event scheduled by components + ScheduledUpdate { + height: u32, + }, + + // Whenever a task is ready (complete) Dioxus produces this "AsyncEvent" + // + // Async events don't necessarily propagate into a scope being ran. It's up to the event itself + // to force an update for itself. + // + // Most async events should have a low priority. + // + // This type exists for the task/concurrency system to signal that a task is ready. + // However, this does not necessarily signal that a scope must be re-ran, so the hook implementation must cause its + // own re-run. + AsyncEvent {}, + + // Suspense events are a type of async event + // + // they have the lowest priority + SuspenseEvent { + hook_idx: usize, + domnode: Rc>>, + }, + + // image event has conflicting method types + // ImageEvent(event_data::ImageEvent), + // Real events ClipboardEvent(on::ClipboardEvent), CompositionEvent(on::CompositionEvent), @@ -108,30 +164,6 @@ pub enum VirtualEvent { ToggleEvent(on::ToggleEvent), MouseEvent(on::MouseEvent), PointerEvent(on::PointerEvent), - - GarbageCollection, - - // image event has conflicting method types - // ImageEvent(event_data::ImageEvent), - SetStateEvent { - height: HeightMarker, - }, - - // Whenever a task is ready (complete) Dioxus produces this "AsyncEvent" - // - // Async events don't necessarily propagate into a scope being ran. It's up to the event itself - // to force an update for itself. - AsyncEvent { - hook_idx: usize, - }, - - // These are more intrusive than the rest - SuspenseEvent { - hook_idx: usize, - domnode: Rc>>, - }, - // TOOD: make garbage collection its own dedicated event - // GarbageCollection {} } impl std::fmt::Debug for VirtualEvent { @@ -153,9 +185,10 @@ impl std::fmt::Debug for VirtualEvent { VirtualEvent::MouseEvent(_) => "MouseEvent", VirtualEvent::PointerEvent(_) => "PointerEvent", VirtualEvent::GarbageCollection => "GarbageCollection", - VirtualEvent::SetStateEvent { .. } => "SetStateEvent", + VirtualEvent::ScheduledUpdate { .. } => "SetStateEvent", VirtualEvent::AsyncEvent { .. } => "AsyncEvent", VirtualEvent::SuspenseEvent { .. } => "SuspenseEvent", + VirtualEvent::DiffComponent { .. } => "DiffComponent", }; f.debug_struct("VirtualEvent").field("type", &name).finish() diff --git a/packages/core/src/hooks.rs b/packages/core/src/hooks.rs index d7e478606..4fef6aa1c 100644 --- a/packages/core/src/hooks.rs +++ b/packages/core/src/hooks.rs @@ -111,7 +111,7 @@ where // whenever the task is complete, save it into th cx.use_hook( - move |hook_idx| { + move |_| { let task_fut = task_initializer(); let task_dump = Rc::new(RefCell::new(None)); @@ -127,7 +127,7 @@ where *slot.as_ref().borrow_mut() = Some(output); updater(update_id); EventTrigger { - event: VirtualEvent::AsyncEvent { hook_idx }, + event: VirtualEvent::AsyncEvent {}, originator, priority: EventPriority::Low, real_node_id: None, diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 4986dd07a..49958d905 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -359,7 +359,6 @@ impl<'a> NodeFactory<'a> { component: FC

, props: P, key: Option, - // key: Option<&'a str>, children: V, ) -> VNode<'a> where diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index 5c1352a16..b303170f3 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -115,6 +115,8 @@ impl Scope { // This breaks any latent references, invalidating every pointer referencing into it. // Remove all the outdated listeners if !self.pending_garbage.borrow().is_empty() { + // We have some garbage to clean up + log::error!("Cleaning up garabge"); panic!("cannot run scope while garbage is pending! Please clean up your mess first"); } @@ -122,18 +124,17 @@ impl Scope { // make sure we call the drop implementation on all the listeners // this is important to not leak memory - for listener in self - .listeners + self.listeners .borrow_mut() .drain(..) .map(|li| unsafe { &*li }) - { - let mut cb = listener.callback.borrow_mut(); - match cb.take() { - Some(val) => std::mem::drop(val), - None => log::info!("no callback to drop. component must be broken"), - }; - } + .for_each(|listener| { + listener.callback.borrow_mut().take(); + }); + + // make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we + // run the hooks (which hold an &mut Referrence) + // right now, we don't drop // Safety: // - We dropped the listeners, so no more &mut T can be used while these are held diff --git a/packages/core/src/util.rs b/packages/core/src/util.rs index 673a1923c..33a6d9fb2 100644 --- a/packages/core/src/util.rs +++ b/packages/core/src/util.rs @@ -8,24 +8,24 @@ pub fn empty_cell() -> Cell> { Cell::new(None) } -/// A helper type that lets scopes be ordered by their height -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct HeightMarker { - pub idx: ScopeId, - pub height: u32, -} +// /// A helper type that lets scopes be ordered by their height +// #[derive(Debug, Clone, Copy, PartialEq, Eq)] +// pub struct HeightMarker { +// pub idx: ScopeId, +// pub height: u32, +// } -impl Ord for HeightMarker { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.height.cmp(&other.height) - } -} +// 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 { - Some(self.cmp(other)) - } -} +// impl PartialOrd for HeightMarker { +// fn partial_cmp(&self, other: &Self) -> Option { +// Some(self.cmp(other)) +// } +// } pub struct DebugDom {} impl DebugDom { @@ -34,7 +34,7 @@ impl DebugDom { } } -impl<'a> RealDom<'a> for DebugDom { +impl RealDom for DebugDom { fn raw_node_as_any(&self) -> &mut dyn std::any::Any { todo!() } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index e3dbd2ab2..335b5f8c1 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -18,8 +18,9 @@ //! //! This module includes just the barebones for a complete VirtualDOM API. //! Additional functionality is defined in the respective files. - +#![allow(unreachable_code)] use futures_util::StreamExt; +use fxhash::FxHashMap; use crate::hooks::{SuspendedContext, SuspenseHook}; use crate::{arena::SharedResources, innerlude::*}; @@ -28,6 +29,7 @@ use std::any::Any; use std::any::TypeId; use std::cell::{Ref, RefCell, RefMut}; +use std::collections::{BTreeMap, BTreeSet, BinaryHeap}; use std::pin::Pin; /// An integrated virtual node system that progresses events and diffs UI trees. @@ -51,7 +53,7 @@ pub struct VirtualDom { /// Should always be the first (gen=0, id=0) pub base_scope: ScopeId, - pub triggers: RefCell>, + pending_events: BTreeMap, // for managing the props that were used to create the dom #[doc(hidden)] @@ -61,9 +63,6 @@ pub struct VirtualDom { _root_props: std::pin::Pin>, } -// ====================================== -// Public Methods for the VirtualDom -// ====================================== impl VirtualDom { /// Create a new instance of the Dioxus Virtual Dom with no properties for the root component. /// @@ -145,14 +144,14 @@ impl VirtualDom { base_scope, _root_props: root_props, shared: components, - triggers: Default::default(), + pending_events: BTreeMap::new(), _root_prop_type: TypeId::of::

(), } } pub fn launch_in_place(root: FC<()>) -> Self { let mut s = Self::new(root); - s.rebuild_in_place(); + s.rebuild_in_place().unwrap(); s } @@ -160,10 +159,18 @@ impl VirtualDom { /// pub fn launch_with_props_in_place(root: FC

, root_props: P) -> Self { let mut s = Self::new_with_props(root, root_props); - s.rebuild_in_place(); + s.rebuild_in_place().unwrap(); s } + pub fn base_scope(&self) -> &Scope { + unsafe { self.shared.get_scope(self.base_scope).unwrap() } + } + + pub fn get_scope(&self, id: ScopeId) -> Option<&Scope> { + unsafe { self.shared.get_scope(id) } + } + /// Rebuilds the VirtualDOM from scratch, but uses a "dummy" RealDom. /// /// Used in contexts where a real copy of the structure doesn't matter, and the VirtualDOM is the source of truth. @@ -174,21 +181,22 @@ impl VirtualDom { /// /// SSR takes advantage of this by using Dioxus itself as the source of truth, and rendering from the tree directly. pub fn rebuild_in_place(&mut self) -> Result> { - let mut realdom = DebugDom::new(); - let mut edits = Vec::new(); - self.rebuild(&mut realdom, &mut edits)?; - Ok(edits) + todo!(); + // let mut realdom = DebugDom::new(); + // let mut edits = Vec::new(); + // self.rebuild(&mut realdom, &mut edits)?; + // Ok(edits) } /// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom rom scratch /// /// The diff machine expects the RealDom's stack to be the root of the application - pub fn rebuild<'s>( - &'s mut self, - realdom: &'_ mut dyn RealDom<'s>, - edits: &'_ mut Vec>, - ) -> Result<()> { - let mut diff_machine = DiffMachine::new(edits, realdom, self.base_scope, &self.shared); + /// + /// Events like garabge collection, application of refs, etc are not handled by this method and can only be progressed + /// through "run" + /// + pub fn rebuild<'s>(&'s mut self, edits: &'_ mut Vec>) -> Result<()> { + let mut diff_machine = DiffMachine::new(edits, self.base_scope, &self.shared); let cur_component = diff_machine .get_scope_mut(&self.base_scope) @@ -209,219 +217,220 @@ impl VirtualDom { Ok(()) } - /// - /// - /// - /// - /// - pub fn queue_event(&self, trigger: EventTrigger) { - let mut triggers = self.triggers.borrow_mut(); - triggers.push(trigger); + async fn select_next_event(&mut self) -> Option { + let mut receiver = self.shared.task_receiver.borrow_mut(); + + // drain the in-flight events so that we can sort them out with the current events + while let Ok(Some(trigger)) = receiver.try_next() { + log::info!("scooping event from receiver"); + self.pending_events.insert(trigger.make_key(), trigger); + } + + if self.pending_events.is_empty() { + // Continuously poll the future pool and the event receiver for work + let mut tasks = self.shared.async_tasks.borrow_mut(); + let tasks_tasks = tasks.next(); + + let mut receiver = self.shared.task_receiver.borrow_mut(); + let reciv_task = receiver.next(); + + futures_util::pin_mut!(tasks_tasks); + futures_util::pin_mut!(reciv_task); + + let trigger = match futures_util::future::select(tasks_tasks, reciv_task).await { + futures_util::future::Either::Left((trigger, _)) => trigger, + futures_util::future::Either::Right((trigger, _)) => trigger, + } + .unwrap(); + self.pending_events.insert(trigger.make_key(), trigger); + } + + todo!() + + // let trigger = self.select_next_event().unwrap(); + // trigger } - /// 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 - /// dom to completion, tagging components that need updates, compressing events together, and finally emitting a single - /// change list. - /// - /// If implementing an external renderer, this is the perfect method to combine with an async event loop that waits on - /// listeners, something like this: - /// - /// ```ignore - /// while let Ok(event) = receiver.recv().await { - /// let edits = self.internal_dom.progress_with_event(event)?; - /// for edit in &edits { - /// patch_machine.handle_edit(edit); - /// } - /// } - /// ``` - /// - /// Note: this method is not async and does not provide suspense-like functionality. It is up to the renderer to provide the - /// executor and handlers for suspense as show in the example. - /// - /// ```ignore - /// let (sender, receiver) = channel::new(); - /// sender.send(EventTrigger::start()); - /// - /// let mut dom = VirtualDom::new(); - /// dom.suspense_handler(|event| sender.send(event)); - /// - /// while let Ok(diffs) = dom.progress_with_event(receiver.recv().await) { - /// render(diffs); - /// } - /// - /// ``` + // the cooperartive, fiber-based scheduler + // is "awaited" and will always return some edits for the real dom to apply // - // Developer notes: - // ---- - // This method has some pretty complex safety guarantees to uphold. - // We interact with bump arenas, raw pointers, and use UnsafeCell to get a partial borrow of the arena. - // The final EditList has edits that pull directly from the Bump Arenas which add significant complexity - // in crafting a 100% safe solution with traditional lifetimes. Consider this method to be internally unsafe - // but the guarantees provide a safe, fast, and efficient abstraction for the VirtualDOM updating framework. + // Right now, we'll happily partially render component trees // - // A good project would be to remove all unsafe from this crate and move the unsafety into safer abstractions. - pub async fn progress_with_event<'a, 's>( - &'s mut self, - realdom: &'a mut dyn RealDom<'s>, - edits: &'a mut Vec>, - ) -> Result<()> { - let trigger = self.triggers.borrow_mut().pop().expect("failed"); + // Normally, you would descend through the tree depth-first, but we actually descend breadth-first. + pub async fn run(&mut self, realdom: &mut dyn RealDom) -> Result<()> { + let cur_component = self.base_scope; + let mut edits = Vec::new(); + let resources = self.shared.clone(); - let mut diff_machine = DiffMachine::new(edits, realdom, trigger.originator, &self.shared); + let mut diff_machine = DiffMachine::new(&mut edits, cur_component, &resources); - match &trigger.event { - // When a scope gets destroyed during a diff, it gets its own garbage collection event - // However, an old scope might be attached - VirtualEvent::GarbageCollection => { - let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap(); + loop { + let trigger = self.select_next_event().await.unwrap(); - let mut garbage_list = scope.consume_garbage(); + match &trigger.event { + // Collecting garabge is not currently interruptible. + // + // In the future, it could be though + VirtualEvent::GarbageCollection => { + let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap(); - while let Some(node) = garbage_list.pop() { - match &node.kind { - VNodeKind::Text(_) => { - // - self.shared.collect_garbage(node.direct_id()) - } - VNodeKind::Anchor(anchor) => { - // - } + let mut garbage_list = scope.consume_garbage(); - VNodeKind::Element(el) => { - self.shared.collect_garbage(node.direct_id()); - for child in el.children { - garbage_list.push(child); + let mut scopes_to_kill = Vec::new(); + while let Some(node) = garbage_list.pop() { + match &node.kind { + VNodeKind::Text(_) => { + self.shared.collect_garbage(node.direct_id()); + } + VNodeKind::Anchor(_) => { + self.shared.collect_garbage(node.direct_id()); + } + VNodeKind::Suspended(_) => { + self.shared.collect_garbage(node.direct_id()); + } + + VNodeKind::Element(el) => { + self.shared.collect_garbage(node.direct_id()); + for child in el.children { + garbage_list.push(child); + } + } + + VNodeKind::Fragment(frag) => { + for child in frag.children { + garbage_list.push(child); + } + } + + VNodeKind::Component(comp) => { + // TODO: run the hook destructors and then even delete the scope + // TODO: allow interruption here + if !realdom.must_commit() { + let scope_id = comp.ass_scope.get().unwrap(); + let scope = self.get_scope(scope_id).unwrap(); + let root = scope.root(); + garbage_list.push(root); + scopes_to_kill.push(scope_id); + } } } + } - VNodeKind::Fragment(frag) => { - for child in frag.children { - garbage_list.push(child); - } - } - VNodeKind::Component(comp) => { - // run the destructors - todo!(); - } - VNodeKind::Suspended(node) => { - // make sure the task goes away - todo!(); - } + for scope in scopes_to_kill { + // oy kill em + log::debug!("should be removing scope {:#?}", scope); } } - } - // Nothing yet - VirtualEvent::AsyncEvent { .. } => {} - - // Suspense Events! A component's suspended node is updated - VirtualEvent::SuspenseEvent { hook_idx, domnode } => { - // Safety: this handler is the only thing that can mutate shared items at this moment in tim - let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap(); - - // safety: we are sure that there are no other references to the inner content of suspense hooks - let hook = unsafe { scope.hooks.get_mut::(*hook_idx) }.unwrap(); - - let cx = Context { scope, props: &() }; - let scx = SuspendedContext { inner: cx }; - - // generate the new node! - let nodes: Option> = (&hook.callback)(scx); - match nodes { - None => { - log::warn!("Suspense event came through, but there was no mounted node to update >:("); - } - Some(nodes) => { - // todo!("using the wrong frame"); - let nodes = scope.frames.finished_frame().bump.alloc(nodes); - - // push the old node's root onto the stack - let real_id = domnode.get().ok_or(Error::NotMounted)?; - diff_machine.edit_push_root(real_id); - - // push these new nodes onto the diff machines stack - let meta = diff_machine.create_vnode(&*nodes); - - // replace the placeholder with the new nodes we just pushed on the stack - diff_machine.edit_replace_with(1, meta.added_to_stack); - } + VirtualEvent::AsyncEvent { .. } => { + // we want to progress these events + // However, there's nothing we can do for these events, they must generate their own events. } - } - // This is the "meat" of our cooperative scheduler - // As updates flow in, we re-evalute the event queue and decide if we should be switching the type of work - // - // We use the reconciler to request new IDs and then commit/uncommit the IDs when the scheduler is finished - _ => { - diff_machine - .get_scope_mut(&trigger.originator) - .map(|f| f.call_listener(trigger)); + // Suspense Events! A component's suspended node is updated + VirtualEvent::SuspenseEvent { hook_idx, domnode } => { + // Safety: this handler is the only thing that can mutate shared items at this moment in tim + let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap(); - // Now, there are events in the queue - let mut updates = self.shared.borrow_queue(); + // safety: we are sure that there are no other references to the inner content of suspense hooks + let hook = unsafe { scope.hooks.get_mut::(*hook_idx) }.unwrap(); - // Order the nodes by their height, we want the nodes with the smallest depth on top - // This prevents us from running the same component multiple times - updates.sort_unstable(); + let cx = Context { scope, props: &() }; + let scx = SuspendedContext { inner: cx }; - log::debug!("There are: {:#?} updates to be processed", updates.len()); - - // Iterate through the triggered nodes (sorted by height) and begin to diff them - for update in updates.drain(..) { - log::debug!("Running updates for: {:#?}", update); - - // Make sure this isn't a node we've already seen, we don't want to double-render anything - // If we double-renderer something, this would cause memory safety issues - if diff_machine.seen_scopes.contains(&update.idx) { - log::debug!("Skipping update for: {:#?}", update); - continue; - } - - // Start a new mutable borrow to components - // We are guaranteeed that this scope is unique because we are tracking which nodes have modified in the diff machine - let cur_component = diff_machine - .get_scope_mut(&update.idx) - .expect("Failed to find scope or borrow would be aliasing"); - - // Now, all the "seen nodes" are nodes that got notified by running this listener - diff_machine.seen_scopes.insert(update.idx.clone()); - - if cur_component.run_scope().is_ok() { - let (old, new) = ( - cur_component.frames.wip_head(), - cur_component.frames.fin_head(), + // generate the new node! + let nodes: Option = (&hook.callback)(scx); + match nodes { + None => { + log::warn!( + "Suspense event came through, but there were no generated nodes >:(." ); - diff_machine.diff_node(old, new); + } + Some(nodes) => { + // allocate inside the finished frame - not the WIP frame + let nodes = scope.frames.finished_frame().bump.alloc(nodes); + + // push the old node's root onto the stack + let real_id = domnode.get().ok_or(Error::NotMounted)?; + diff_machine.edit_push_root(real_id); + + // push these new nodes onto the diff machines stack + let meta = diff_machine.create_vnode(&*nodes); + + // replace the placeholder with the new nodes we just pushed on the stack + diff_machine.edit_replace_with(1, meta.added_to_stack); + } } } + + // Run the component + VirtualEvent::ScheduledUpdate { height: u32 } => { + let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap(); + + match scope.run_scope() { + Ok(_) => { + let event = VirtualEvent::DiffComponent; + let trigger = EventTrigger { + event, + originator: trigger.originator, + priority: EventPriority::High, + real_node_id: None, + }; + self.shared.task_sender.unbounded_send(trigger); + } + Err(_) => { + log::error!("failed to run this component!"); + } + } + } + + VirtualEvent::DiffComponent => { + diff_machine.diff_scope(trigger.originator)?; + } + + // Process any user-facing events just by calling their listeners + // This performs no actual work - but the listeners themselves will cause insert new work + VirtualEvent::ClipboardEvent(_) + | VirtualEvent::CompositionEvent(_) + | VirtualEvent::KeyboardEvent(_) + | VirtualEvent::FocusEvent(_) + | VirtualEvent::FormEvent(_) + | VirtualEvent::SelectionEvent(_) + | VirtualEvent::TouchEvent(_) + | VirtualEvent::UIEvent(_) + | VirtualEvent::WheelEvent(_) + | VirtualEvent::MediaEvent(_) + | VirtualEvent::AnimationEvent(_) + | VirtualEvent::TransitionEvent(_) + | VirtualEvent::ToggleEvent(_) + | VirtualEvent::MouseEvent(_) + | VirtualEvent::PointerEvent(_) => { + let scope_id = &trigger.originator; + let scope = unsafe { self.shared.get_scope_mut(*scope_id) }; + match scope { + Some(scope) => scope.call_listener(trigger)?, + None => { + log::warn!("No scope found for event: {:#?}", scope_id); + } + } + } + } + + if realdom.must_commit() || self.pending_events.is_empty() { + realdom.commit_edits(&mut diff_machine.edits); + realdom.wait_until_ready().await; } } Ok(()) } - pub async fn wait_for_event(&mut self) -> Option { - let r = self.shared.tasks.clone(); - let mut r = r.borrow_mut(); - let gh = r.next().await; - - gh - } - - pub fn any_pending_events(&self) -> bool { - let r = self.shared.tasks.clone(); - let r = r.borrow(); - !r.is_empty() - } - - pub fn base_scope(&self) -> &Scope { - unsafe { self.shared.get_scope(self.base_scope).unwrap() } - } - - pub fn get_scope(&self, id: ScopeId) -> Option<&Scope> { - unsafe { self.shared.get_scope(id) } + pub fn get_event_sender(&self) -> futures_channel::mpsc::UnboundedSender { + todo!() + // std::rc::Rc::new(move |_| { + // // + // }) + // todo() } } @@ -429,3 +438,9 @@ impl VirtualDom { // These impls are actually wrong. The DOM needs to have a mutex implemented. unsafe impl Sync for VirtualDom {} unsafe impl Send for VirtualDom {} + +fn select_next_event( + pending_events: &mut BTreeMap, +) -> Option { + None +} diff --git a/packages/core/tests/channels.rs b/packages/core/tests/channels.rs new file mode 100644 index 000000000..2eb64e5b1 --- /dev/null +++ b/packages/core/tests/channels.rs @@ -0,0 +1,19 @@ +use futures_channel::mpsc::unbounded; + +#[async_std::test] +async fn channels() { + let (sender, mut receiver) = unbounded::(); + + // drop(sender); + + match receiver.try_next() { + Ok(a) => { + dbg!(a); + } + Err(no) => { + dbg!(no); + } + } + + sender.unbounded_send(1).unwrap(); +} diff --git a/packages/core/tests/diffing.rs b/packages/core/tests/diffing.rs index e8a7522b7..8f7049431 100644 --- a/packages/core/tests/diffing.rs +++ b/packages/core/tests/diffing.rs @@ -41,8 +41,7 @@ impl TestDom { fn diff<'a>(&'a self, old: &'a VNode<'a>, new: &'a VNode<'a>) -> Vec> { let mut edits = Vec::new(); - let dom = DebugDom::new(); - let mut machine = DiffMachine::new_headless(&mut edits, &dom, &self.resources); + let mut machine = DiffMachine::new_headless(&mut edits, &self.resources); machine.diff_node(old, new); edits } @@ -53,9 +52,8 @@ impl TestDom { { let old = self.bump.alloc(self.render(left)); let mut edits = Vec::new(); - let dom = DebugDom::new(); - let mut machine = DiffMachine::new_headless(&mut edits, &dom, &self.resources); + let mut machine = DiffMachine::new_headless(&mut edits, &self.resources); let meta = machine.create_vnode(old); (meta, edits) } @@ -74,13 +72,12 @@ impl TestDom { let new = self.bump.alloc(self.render(right)); let mut create_edits = Vec::new(); - let dom = DebugDom::new(); - let mut machine = DiffMachine::new_headless(&mut create_edits, &dom, &self.resources); + let mut machine = DiffMachine::new_headless(&mut create_edits, &self.resources); machine.create_vnode(old); let mut edits = Vec::new(); - let mut machine = DiffMachine::new_headless(&mut edits, &dom, &self.resources); + let mut machine = DiffMachine::new_headless(&mut edits, &self.resources); machine.diff_node(old, new); (create_edits, edits) } diff --git a/packages/desktop/src/dom.rs b/packages/desktop/src/dom.rs index 4c08b0c82..001552586 100644 --- a/packages/desktop/src/dom.rs +++ b/packages/desktop/src/dom.rs @@ -29,7 +29,7 @@ impl WebviewDom<'_> { // self.registry // } } -impl<'bump> RealDom<'bump> for WebviewDom<'bump> { +impl RealDom for WebviewDom<'_> { fn raw_node_as_any(&self) -> &mut dyn std::any::Any { todo!() // self.edits.push(PushRoot { root }); diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index afcfc8383..108883991 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -5,6 +5,7 @@ use std::sync::{Arc, RwLock}; use cfg::DesktopConfig; use dioxus_core::*; +use serde::{Deserialize, Serialize}; pub use wry; use wry::application::event::{Event, WindowEvent}; @@ -83,7 +84,33 @@ impl WebviewRenderer { let window = window.build(&event_loop)?; - let vir = VirtualDom::new_with_props(root, props); + let mut vir = VirtualDom::new_with_props(root, props); + + let channel = vir.get_event_sender(); + struct WebviewBridge {} + impl RealDom for WebviewBridge { + fn raw_node_as_any(&self) -> &mut dyn std::any::Any { + todo!() + } + + fn must_commit(&self) -> bool { + false + } + + fn commit_edits<'a>(&mut self, edits: &mut Vec>) {} + + fn wait_until_ready<'s>( + &'s mut self, + ) -> std::pin::Pin + 's>> { + // + Box::pin(async { + // + }) + } + } + + let mut real_dom = WebviewBridge {}; + // async_std::task::spawn_local(vir.run(&mut real_dom)); // todo: combine these or something let vdom = Arc::new(RwLock::new(vir)); @@ -91,94 +118,80 @@ impl WebviewRenderer { let webview = WebViewBuilder::new(window)? .with_url(&format!("data:text/html,{}", HTML_CONTENT))? - .with_rpc_handler(move |_window: &Window, mut req: RpcRequest| { - match req.method.as_str() { - "initiate" => { - let edits = if let Some(edits) = &redits { - serde_json::to_value(edits).unwrap() - } else { - let mut lock = vdom.write().unwrap(); - // let mut reg_lock = registry.write().unwrap(); - - // Create the thin wrapper around the registry to collect the edits into - let mut real = dom::WebviewDom::new(); - let pre = pre_rendered.clone(); - - let response = match pre { - Some(content) => { - lock.rebuild_in_place().unwrap(); - - Response { - edits: Vec::new(), - pre_rendered: Some(content), - } - } - None => { - // - let edits = { - let mut edits = Vec::new(); - lock.rebuild(&mut real, &mut edits).unwrap(); - edits - }; - Response { - edits, - pre_rendered: None, - } - } - }; - - serde_json::to_value(&response).unwrap() - }; - - // Return the edits into the webview runtime - Some(RpcResponse::new_result(req.id.take(), Some(edits))) - } - "user_event" => { - log::debug!("User event received"); - - // let registry = registry.clone(); - let vdom = vdom.clone(); - let response = async_std::task::block_on(async move { - let mut lock = vdom.write().unwrap(); - // let mut reg_lock = registry.write().unwrap(); - - // a deserialized event - let data = req.params.unwrap(); - log::debug!("Data: {:#?}", data); - let event = trigger_from_serialized(data); - - lock.queue_event(event); - - // Create the thin wrapper around the registry to collect the edits into - let mut real = dom::WebviewDom::new(); - - // Serialize the edit stream - // - let mut edits = Vec::new(); - lock.progress_with_event(&mut real, &mut edits) - .await - .expect("failed to progress"); - - let response = Response { - edits, - pre_rendered: None, - }; - let response = serde_json::to_value(&response).unwrap(); - - // Give back the registry into its slot - // *reg_lock = Some(real.consume()); - - // Return the edits into the webview runtime - Some(RpcResponse::new_result(req.id.take(), Some(response))) - }); - - response - - // spawn a task to clean up the garbage - } - _ => todo!("this message failed"), - } - }) + // .with_rpc_handler(move |_window: &Window, mut req: RpcRequest| { + // match req.method.as_str() { + // "initiate" => { + // let edits = if let Some(edits) = &redits { + // serde_json::to_value(edits).unwrap() + // } else { + // let mut lock = vdom.write().unwrap(); + // // let mut reg_lock = registry.write().unwrap(); + // // Create the thin wrapper around the registry to collect the edits into + // let mut real = dom::WebviewDom::new(); + // let pre = pre_rendered.clone(); + // let response = match pre { + // Some(content) => { + // lock.rebuild_in_place().unwrap(); + // Response { + // edits: Vec::new(), + // pre_rendered: Some(content), + // } + // } + // None => { + // // + // let edits = { + // // let mut edits = Vec::new(); + // todo!() + // // lock.rebuild(&mut real, &mut edits).unwrap(); + // // edits + // }; + // Response { + // edits, + // pre_rendered: None, + // } + // } + // }; + // serde_json::to_value(&response).unwrap() + // }; + // // Return the edits into the webview runtime + // Some(RpcResponse::new_result(req.id.take(), Some(edits))) + // } + // "user_event" => { + // log::debug!("User event received"); + // // let registry = registry.clone(); + // let vdom = vdom.clone(); + // let response = async_std::task::block_on(async move { + // let mut lock = vdom.write().unwrap(); + // // let mut reg_lock = registry.write().unwrap(); + // // a deserialized event + // let data = req.params.unwrap(); + // log::debug!("Data: {:#?}", data); + // let event = trigger_from_serialized(data); + // // lock.queue_event(event); + // // Create the thin wrapper around the registry to collect the edits into + // let mut real = dom::WebviewDom::new(); + // // Serialize the edit stream + // // + // let mut edits = Vec::new(); + // // lock.run(&mut real, &mut edits) + // // .await + // // .expect("failed to progress"); + // let response = Response { + // edits, + // pre_rendered: None, + // }; + // let response = serde_json::to_value(&response).unwrap(); + // // Give back the registry into its slot + // // *reg_lock = Some(real.consume()); + // // Return the edits into the webview runtime + // Some(RpcResponse::new_result(req.id.take(), Some(response))) + // }); + // response + // // spawn a task to clean up the garbage + // } + // _ => todo!("this message failed"), + // } + // }) .build()?; event_loop.run(move |event, _, control_flow| { @@ -194,33 +207,17 @@ impl WebviewRenderer { } _ => {} }, + Event::MainEventsCleared => { webview.resize(); // window.request_redraw(); } - _ => {} // Event::WindowEvent { event, .. } => { - // // - // match event { - // WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, - // _ => {} - // } - // } - // _ => { - // // let _ = webview.resize(); - // } + _ => {} } }); } - /// Create a new text-renderer instance from a functional component root. - /// Automatically progresses the creation of the VNode tree to completion. - /// - /// A VDom is automatically created. If you want more granular control of the VDom, use `from_vdom` - // pub fn new(root: FC, builder: impl FnOnce() -> WVResult>) -> Self { - // Self { root } - // } - /// Create a new text renderer from an existing Virtual DOM. /// This will progress the existing VDom's events to completion. pub fn from_vdom() -> Self { @@ -237,47 +234,3 @@ impl WebviewRenderer { todo!() } } - -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -// use crate::dom::WebviewRegistry; - -#[derive(Debug, Serialize, Deserialize)] -struct MessageParameters { - message: String, -} - -fn HANDLER(window: &Window, mut req: RpcRequest) -> Option { - let mut response = None; - if &req.method == "fullscreen" { - if let Some(params) = req.params.take() { - if let Ok(mut args) = serde_json::from_value::>(params) { - if !args.is_empty() { - if args.swap_remove(0) { - window.set_fullscreen(Some(Fullscreen::Borderless(None))); - } else { - window.set_fullscreen(None); - } - }; - response = Some(RpcResponse::new_result(req.id.take(), None)); - } - } - } else if &req.method == "send-parameters" { - if let Some(params) = req.params.take() { - if let Ok(mut args) = serde_json::from_value::>(params) { - let result = if !args.is_empty() { - let msg = args.swap_remove(0); - Some(Value::String(format!("Hello, {}!", msg.message))) - } else { - // NOTE: in the real-world we should send an error response here! - None - }; - // Must always send a response as this is a `call()` - response = Some(RpcResponse::new_result(req.id.take(), result)); - } - } - } - - response -} diff --git a/packages/mobile/src/dom.rs b/packages/mobile/src/dom.rs index cea4f530d..c9e457479 100644 --- a/packages/mobile/src/dom.rs +++ b/packages/mobile/src/dom.rs @@ -30,7 +30,7 @@ impl WebviewDom<'_> { self.registry } } -impl<'bump> RealDom<'bump> for WebviewDom<'bump> { +impl<'bump> RealDom for WebviewDom<'bump> { fn raw_node_as_any(&self) -> &mut dyn std::any::Any { todo!() // self.edits.push(PushRoot { root }); diff --git a/packages/mobile/src/lib.rs b/packages/mobile/src/lib.rs index 9cd24460a..1ee4bc652 100644 --- a/packages/mobile/src/lib.rs +++ b/packages/mobile/src/lib.rs @@ -118,7 +118,7 @@ impl WebviewRenderer { // Serialize the edit stream let edits = { let mut edits = Vec::new(); - lock.rebuild(&mut real, &mut edits).unwrap(); + lock.rebuild(&mut edits).unwrap(); serde_json::to_value(edits).unwrap() }; @@ -140,7 +140,7 @@ impl WebviewRenderer { // Serialize the edit stream let edits = { let mut edits = Vec::new(); - lock.rebuild(&mut real, &mut edits).unwrap(); + lock.rebuild(&mut edits).unwrap(); serde_json::to_value(edits).unwrap() }; diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 3b25700dd..10300c023 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -33,6 +33,7 @@ features = [ "Attr", "Document", "Element", + "CssStyleDeclaration", "HtmlElement", "HtmlInputElement", "HtmlSelectElement", diff --git a/packages/web/src/cfg.rs b/packages/web/src/cfg.rs index 002dc3535..376e466c9 100644 --- a/packages/web/src/cfg.rs +++ b/packages/web/src/cfg.rs @@ -1,9 +1,13 @@ pub struct WebConfig { pub(crate) hydrate: bool, + pub(crate) rootname: String, } impl Default for WebConfig { fn default() -> Self { - Self { hydrate: false } + Self { + hydrate: false, + rootname: "dioxusroot".to_string(), + } } } impl WebConfig { @@ -18,4 +22,9 @@ impl WebConfig { self.hydrate = f; self } + + pub fn rootname(mut self, name: impl Into) -> Self { + self.rootname = name.into(); + self + } } diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 5223fdc36..37bbfc50c 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -7,8 +7,8 @@ use dioxus_core::{ use fxhash::FxHashMap; use wasm_bindgen::{closure::Closure, JsCast}; use web_sys::{ - window, Document, Element, Event, HtmlElement, HtmlInputElement, HtmlOptionElement, Node, - NodeList, + window, CssStyleDeclaration, Document, Element, Event, HtmlElement, HtmlInputElement, + HtmlOptionElement, Node, NodeList, }; use crate::{nodeslab::NodeSlab, WebConfig}; @@ -23,9 +23,7 @@ pub struct WebsysDom { root: Element, - event_receiver: async_channel::Receiver, - - trigger: Arc, + sender_callback: Rc, // map of listener types to number of those listeners // This is roughly a delegater @@ -40,19 +38,9 @@ pub struct WebsysDom { last_node_was_text: bool, } impl WebsysDom { - pub fn new(root: Element, cfg: WebConfig) -> Self { + pub fn new(root: Element, cfg: WebConfig, sender_callback: Rc) -> Self { let document = load_document(); - let (sender, receiver) = async_channel::unbounded::(); - - let sender_callback = Arc::new(move |ev| { - let c = sender.clone(); - wasm_bindgen_futures::spawn_local(async move { - log::debug!("sending event through channel"); - c.send(ev).await.unwrap(); - }); - }); - let mut nodes = NodeSlab::new(2000); let mut listeners = FxHashMap::default(); @@ -67,12 +55,11 @@ impl WebsysDom { let el: &Element = node.dyn_ref::().unwrap(); let id: String = el.get_attribute("dio_el").unwrap(); let id = id.parse::().unwrap(); - - // this autoresizes the vector if needed nodes[id] = Some(node); } // Load all the event listeners into our listener register + // TODO } let mut stack = Stack::with_capacity(10); @@ -84,18 +71,12 @@ impl WebsysDom { nodes, listeners, document, - event_receiver: receiver, - trigger: sender_callback, + sender_callback, root, last_node_was_text: false, } } - pub async fn wait_for_event(&mut self) -> Option { - let v = self.event_receiver.recv().await.unwrap(); - Some(v) - } - pub fn process_edits(&mut self, edits: &mut Vec) { for edit in edits.drain(..) { log::info!("Handling edit: {:#?}", edit); @@ -310,7 +291,7 @@ impl WebsysDom { if let Some(entry) = self.listeners.get_mut(event) { entry.0 += 1; } else { - let trigger = self.trigger.clone(); + let trigger = self.sender_callback.clone(); let handler = Closure::wrap(Box::new(move |event: &web_sys::Event| { // "Result" cannot be received from JS @@ -339,25 +320,21 @@ impl WebsysDom { } fn set_attribute(&mut self, name: &str, value: &str, ns: Option<&str>) { - if name == "class" { + if let Some(el) = self.stack.top().dyn_ref::() { match ns { - Some("http://www.w3.org/2000/svg") => { - // - if let Some(el) = self.stack.top().dyn_ref::() { - let r: web_sys::SvgAnimatedString = el.class_name(); - r.set_base_val(value); - // el.set_class_name(value); - } - } - _ => { - if let Some(el) = self.stack.top().dyn_ref::() { - el.set_class_name(value); - } + // inline style support + Some("style") => { + let el = el.dyn_ref::().unwrap(); + let style_dc: CssStyleDeclaration = el.style(); + style_dc.set_property(name, value).unwrap(); } + _ => el.set_attribute(name, value).unwrap(), } - } else { - if let Some(el) = self.stack.top().dyn_ref::() { - el.set_attribute(name, value).unwrap(); + match name { + "value" => {} + "checked" => {} + "selected" => {} + _ => {} } } } @@ -418,7 +395,7 @@ impl WebsysDom { } } -impl<'a> dioxus_core::diff::RealDom<'a> for WebsysDom { +impl<'a> dioxus_core::diff::RealDom for WebsysDom { // fn request_available_node(&mut self) -> ElementId { // let key = self.nodes.insert(None); // log::debug!("making new key: {:#?}", key); diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index d70ac567f..d7cf7b653 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -2,6 +2,8 @@ //! -------------- //! This crate implements a renderer of the Dioxus Virtual DOM for the web browser using Websys. +use std::rc::Rc; + pub use crate::cfg::WebConfig; use crate::dom::load_document; use dioxus::prelude::{Context, Properties, VNode}; @@ -77,50 +79,25 @@ pub async fn run_with_props( ) -> Result<()> { let mut dom = VirtualDom::new_with_props(root, root_props); - // let tasks = dom.shared.tasks.clone(); + let root_el = load_document().get_element_by_id(&cfg.rootname).unwrap(); - let root_el = load_document().get_element_by_id("dioxusroot").unwrap(); - let mut websys_dom = dom::WebsysDom::new(root_el, cfg); + let tasks = dom.get_event_sender(); - let mut edits = Vec::new(); - dom.rebuild(&mut websys_dom, &mut edits)?; - websys_dom.process_edits(&mut edits); + let mut real = RealDomWebsys {}; - log::info!("Going into event loop"); - - // #[allow(unreachable_code)] - loop { - let trigger = { - let real_queue = websys_dom.wait_for_event(); - if dom.any_pending_events() { - log::info!("tasks is not empty, waiting for either tasks or event system"); - let mut task = dom.wait_for_event(); - - pin_mut!(real_queue); - pin_mut!(task); - - match futures_util::future::select(real_queue, task).await { - futures_util::future::Either::Left((trigger, _)) => trigger, - futures_util::future::Either::Right((trigger, _)) => trigger, - } - } else { - log::info!("tasks is empty, waiting for dom event to trigger soemthing"); - real_queue.await - } - }; - - if let Some(real_trigger) = trigger { - log::info!("event received"); - - dom.queue_event(real_trigger); - - let mut edits = Vec::new(); - dom.progress_with_event(&mut websys_dom, &mut edits).await?; - websys_dom.process_edits(&mut edits); - } + // initialize the virtualdom first + if cfg.hydrate { + dom.rebuild_in_place()?; } - // should actually never return from this, should be an error, rustc just cant see it + let mut websys_dom = dom::WebsysDom::new( + root_el, + cfg, + Rc::new(move |event| tasks.unbounded_send(event).unwrap()), + ); + + dom.run(&mut websys_dom).await?; + Ok(()) } @@ -128,3 +105,10 @@ struct HydrationNode { id: usize, node: Node, } + +struct RealDomWebsys {} +impl dioxus::RealDom for RealDomWebsys { + fn raw_node_as_any(&self) -> &mut dyn std::any::Any { + todo!() + } +}