diff --git a/Cargo.toml b/Cargo.toml index 6dbd0ac8..64f7992b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,7 +53,7 @@ members = [ "packages/core", "packages/html-namespace", "packages/web", - "packages/cli", + # "packages/cli", # "packages/atoms", # "packages/ssr", # "packages/docsite", diff --git a/notes/TODO.md b/notes/TODO.md index d1fff211..74963669 100644 --- a/notes/TODO.md +++ b/notes/TODO.md @@ -2,7 +2,7 @@ - [] Transition away from names and towards compile-time safe tags - [] Fix diffing of fragments - [] Properly integrate memoization to prevent safety issues with children -- [x] Understand the issue with callbacks (outdated generations) +- [] Understand and fix the issue with callbacks (outdated generations) - [] Fix examples for core, web, ssr, and general - [] Finish up documentation - [] Polish the Recoil (Dirac?) API @@ -16,3 +16,5 @@ - [] ...some how deserialize (hydrate) the dom state? - [] Implement controlled inputs for select and textarea - [] ...somehow... noderefs.... + +use_state(&cx, ) diff --git a/packages/core/.vscode/settings.json b/packages/core/.vscode/settings.json index 80f51cff..2f418701 100644 --- a/packages/core/.vscode/settings.json +++ b/packages/core/.vscode/settings.json @@ -1,3 +1,3 @@ { - "rust-analyzer.inlayHints.enable": false + "rust-analyzer.inlayHints.enable": true } \ No newline at end of file diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index c77791ed..4fec2caf 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -16,7 +16,7 @@ dioxus-core-macro = { path="../core-macro", version="0.1.1" } # Backs scopes and graphs between parent and children generational-arena = { version="0.2.8" } -# Bumpalo backs the VNode creation +# Bumpalo is used as a micro heap backing each component bumpalo = { version="3.6.0", features=["collections", "boxed"] } # custom error type @@ -25,6 +25,9 @@ thiserror = "1" # faster hashmaps fxhash = "0.2.1" +# even *faster* hashmaps for index-based types +nohash-hasher = "0.2.0" + # Used in diffing longest-increasing-subsequence = "0.1.0" @@ -33,8 +36,10 @@ log = "0.4" # Serialize the Edits for use in Webview/Liveview instances serde = { version="1", features=["derive"], optional=true } + smallvec = "1.6.1" + [features] default = [] serialize = ["serde", "generational-arena/serde"] diff --git a/packages/core/src/debug_renderer.rs b/packages/core/src/debug_renderer.rs index 605758bc..28e90672 100644 --- a/packages/core/src/debug_renderer.rs +++ b/packages/core/src/debug_renderer.rs @@ -16,7 +16,7 @@ impl DebugRenderer { /// /// This means that the root component must either consumes its own context, or statics are used to generate the page. /// The root component can access things like routing in its context. - pub fn new(root: impl for<'a> Fn(Context<'a, ()>) -> VNode + 'static) -> Self { + pub fn new(root: FC<()>) -> Self { Self::new_with_props(root, ()) } @@ -24,10 +24,7 @@ impl DebugRenderer { /// 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_with_props( - root: impl for<'a> Fn(Context<'a, T>) -> VNode + 'static, - root_props: T, - ) -> Self { + pub fn new_with_props(root: FC, root_props: T) -> Self { Self::from_vdom(VirtualDom::new_with_props(root, root_props)) } diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index f235c453..9cbd2fe6 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -60,7 +60,13 @@ pub trait RealDom { fn create_element_ns(&mut self, tag: &str, namespace: &str) -> RealDomNode; // events - fn new_event_listener(&mut self, event: &str, scope: ScopeIdx, id: usize); + fn new_event_listener( + &mut self, + event: &str, + scope: ScopeIdx, + element_id: usize, + realnode: RealDomNode, + ); // fn new_event_listener(&mut self, event: &str); fn remove_event_listener(&mut self, event: &str); @@ -138,6 +144,7 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { self.dom.replace_with(); return; } + new.dom_id.set(old.dom_id.get()); self.diff_listeners(old.listeners, new.listeners); self.diff_attr(old.attributes, new.attributes, new.namespace.is_some()); @@ -145,6 +152,7 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { } // New node is a text element, need to replace the element with a simple text node VNode::Text(_) => { + log::debug!("Replacing el with text"); self.create(new_node); self.dom.replace_with(); } @@ -169,8 +177,10 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { VNode::Text(old) => match new_node { VNode::Text(new) => { if old.text != new.text { + log::debug!("Text has changed {}, {}", old.text, new.text); self.dom.set_text(new.text); } + new.dom_id.set(old.dom_id.get()); } VNode::Element(_) | VNode::Component(_) => { self.create(new_node); @@ -316,10 +326,10 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { }; el.dom_id.set(real_id); - listeners.iter().enumerate().for_each(|(_id, listener)| { - todo!() - // dom - // .new_event_listener(listener.event, listener.scope, listener.id) + listeners.iter().enumerate().for_each(|(idx, listener)| { + self.dom + .new_event_listener(listener.event, listener.scope, idx, real_id); + listener.mounted_node.set(real_id); }); for attr in *attributes { @@ -333,12 +343,12 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { // to emit three instructions to (1) create a text node, (2) set its // text content, and finally (3) append the text node to this // parent. - if children.len() == 1 { - if let VNode::Text(text) = &children[0] { - self.dom.set_text(text.text); - return; - } - } + // if children.len() == 1 { + // if let VNode::Text(text) = &children[0] { + // self.dom.set_text(text.text); + // return; + // } + // } for child in *children { self.create(child); @@ -405,8 +415,8 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { new_component.run_scope().unwrap(); // And then run the diff algorithm - todo!(); - // self.diff_node(new_component.old_frame(), new_component.next_frame()); + // todo!(); + self.diff_node(new_component.old_frame(), new_component.next_frame()); // Finally, insert this node as a seen node. self.seen_nodes.insert(idx); @@ -473,6 +483,8 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { if !old.is_empty() || !new.is_empty() { // self.dom.commit_traversal(); } + // TODO + // what does "diffing listeners" even mean? 'outer1: for (_l_idx, new_l) in new.iter().enumerate() { // go through each new listener @@ -480,34 +492,34 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { // if any characteristics changed, remove and then re-add // if nothing changed, then just move on - let event_type = new_l.event; for old_l in old { if new_l.event == old_l.event { - if new_l.id != old_l.id { - self.dom.remove_event_listener(event_type); - // TODO! we need to mess with events and assign them by RealDomNode - // self.dom - // .update_event_listener(event_type, new_l.scope, new_l.id) - } + new_l.mounted_node.set(old_l.mounted_node.get()); + // if new_l.id != old_l.id { + // self.dom.remove_event_listener(event_type); + // // TODO! we need to mess with events and assign them by RealDomNode + // // self.dom + // // .update_event_listener(event_type, new_l.scope, new_l.id) + // } continue 'outer1; } } - self.dom - .new_event_listener(event_type, new_l.scope, new_l.id); + // self.dom + // .new_event_listener(event_type, new_l.scope, new_l.id); } - 'outer2: for old_l in old { - for new_l in new { - if new_l.event == old_l.event { - continue 'outer2; - } - } - self.dom.remove_event_listener(old_l.event); - } + // 'outer2: for old_l in old { + // for new_l in new { + // if new_l.event == old_l.event { + // continue 'outer2; + // } + // } + // self.dom.remove_event_listener(old_l.event); + // } } // Diff a node's attributes. @@ -590,11 +602,12 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { // Don't take this fast path... } - (_, VNode::Text(text)) => { - // self.dom.commit_traversal(); - self.dom.set_text(text.text); - return; - } + // (_, VNode::Text(text)) => { + // // self.dom.commit_traversal(); + // log::debug!("using optimized text set"); + // self.dom.set_text(text.text); + // return; + // } // todo: any more optimizations (_, _) => {} @@ -1047,7 +1060,7 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { // [... parent] // // the change list stack is in the same state when this function returns. - fn diff_non_keyed_children(&self, old: &'a [VNode<'a>], new: &'a [VNode<'a>]) { + fn diff_non_keyed_children(&mut self, old: &'a [VNode<'a>], new: &'a [VNode<'a>]) { // Handled these cases in `diff_children` before calling this function. debug_assert!(!new.is_empty()); debug_assert!(!old.is_empty()); @@ -1057,13 +1070,26 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { // self.dom.push_root() // [... parent child] - todo!() - // for (i, (new_child, old_child)) in new.iter().zip(old.iter()).enumerate() { - // // [... parent prev_child] - // self.dom.go_to_sibling(i); - // // [... parent this_child] - // self.diff_node(old_child, new_child); - // } + // todo!() + for (i, (new_child, old_child)) in new.iter().zip(old.iter()).enumerate() { + // [... parent prev_child] + // self.dom.go_to_sibling(i); + // [... parent this_child] + self.dom.push_root(old_child.get_mounted_id().unwrap()); + self.diff_node(old_child, new_child); + + let old_id = old_child.get_mounted_id().unwrap(); + let new_id = new_child.get_mounted_id().unwrap(); + + log::debug!( + "pushed root. {:?}, {:?}", + old_child.get_mounted_id().unwrap(), + new_child.get_mounted_id().unwrap() + ); + if old_id != new_id { + log::debug!("Mismatch: {:?}", new_child); + } + } // match old.len().cmp(&new.len()) { // // old.len > new.len -> removing some nodes diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index d5b6a4d4..1f400c41 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -6,20 +6,20 @@ use std::rc::Rc; -use crate::innerlude::ScopeIdx; +use crate::{innerlude::ScopeIdx, virtual_dom::RealDomNode}; #[derive(Debug)] pub struct EventTrigger { pub component_id: ScopeIdx, - pub listener_id: usize, + pub real_node_id: RealDomNode, pub event: VirtualEvent, } impl EventTrigger { - pub fn new(event: VirtualEvent, scope: ScopeIdx, id: usize) -> Self { + pub fn new(event: VirtualEvent, scope: ScopeIdx, mounted_dom_id: RealDomNode) -> Self { Self { component_id: scope, - listener_id: id, + real_node_id: mounted_dom_id, event, } } @@ -55,17 +55,15 @@ pub mod on { use crate::{ builder::ElementBuilder, builder::NodeCtx, - innerlude::{Attribute, Listener, VNode}, + innerlude::{Attribute, Listener, RealDomNode, VNode}, }; + use std::cell::Cell; use super::VirtualEvent; macro_rules! event_directory { ( $( $eventdata:ident: [ $( $name:ident )* ]; )* ) => { $( - - - $( pub fn $name<'a>( c: &'_ NodeCtx<'a>, @@ -74,7 +72,7 @@ pub mod on { let bump = &c.bump(); Listener { event: stringify!($name), - id: *c.listener_id.borrow(), + mounted_node: bump.alloc(Cell::new(RealDomNode::empty())), scope: c.scope_ref.arena_idx, callback: bump.alloc(move |evt: VirtualEvent| match evt { VirtualEvent::$eventdata(event) => callback(event), @@ -121,31 +119,31 @@ pub mod on { /// Returns whether or not a specific event is a bubbling event fn bubbles(&self) -> bool; /// Sets or returns whether the event should propagate up the hierarchy or not - fn cancelBubble(&self) -> (); + fn cancel_bubble(&self); /// Returns whether or not an event can have its default action prevented fn cancelable(&self) -> bool; /// Returns whether the event is composed or not fn composed(&self) -> bool; /// Returns the event's path - fn composedPath(&self) -> (); + fn composed_path(&self) -> String; /// Returns the element whose event listeners triggered the event - fn currentTarget(&self) -> (); + fn current_target(&self); /// Returns whether or not the preventDefault method was called for the event - fn defaultPrevented(&self) -> (); + fn default_prevented(&self) -> bool; /// Returns which phase of the event flow is currently being evaluated - fn eventPhase(&self) -> (); + fn event_phase(&self) -> usize; /// Returns whether or not an event is trusted - fn isTrusted(&self) -> (); + fn is_trusted(&self) -> bool; /// Cancels the event if it is cancelable, meaning that the default action that belongs to the event will - fn preventDefault(&self) -> (); + fn prevent_default(&self); /// Prevents other listeners of the same event from being called - fn stopImmediatePropagation(&self) -> (); + fn stop_immediate_propagation(&self); /// Prevents further propagation of an event during event flow - fn stopPropagation(&self) -> (); + fn stop_propagation(&self); /// Returns the element that triggered the event - fn target(&self) -> (); + fn target(&self); /// Returns the time (in milliseconds relative to the epoch) at which the event was created - fn timeStamp(&self) -> usize; + fn time_stamp(&self) -> usize; } pub trait ClipboardEvent: Debug { diff --git a/packages/core/src/nodebuilder.rs b/packages/core/src/nodebuilder.rs index ff8f04c4..c3ea6e93 100644 --- a/packages/core/src/nodebuilder.rs +++ b/packages/core/src/nodebuilder.rs @@ -1,7 +1,12 @@ //! Helpers for building virtual DOM VNodes. use std::{ - any::Any, borrow::BorrowMut, cell::RefCell, fmt::Arguments, intrinsics::transmute, u128, + any::Any, + borrow::BorrowMut, + cell::{Cell, RefCell}, + fmt::Arguments, + intrinsics::transmute, + u128, }; use crate::{ @@ -9,7 +14,7 @@ use crate::{ innerlude::{Properties, VComponent, FC}, nodes::{Attribute, Listener, NodeKey, VNode}, prelude::{VElement, VFragment}, - virtual_dom::Scope, + virtual_dom::{RealDomNode, Scope}, }; /// A virtual DOM element builder. @@ -353,12 +358,11 @@ where /// ``` pub fn on(self, event: &'static str, callback: impl Fn(VirtualEvent) + 'a) -> Self { let bump = &self.ctx.bump(); - let listener = Listener { event, callback: bump.alloc(callback), - id: *self.ctx.listener_id.borrow(), scope: self.ctx.scope_ref.arena_idx, + mounted_node: bump.alloc(Cell::new(RealDomNode::empty())), }; self.add_listener(listener) } @@ -367,7 +371,8 @@ where self.listeners.push(listener); // bump the context id forward - *self.ctx.listener_id.borrow_mut() += 1; + let id = self.ctx.listener_id.get(); + self.ctx.listener_id.set(id + 1); // Add this listener to the context list // This casts the listener to a self-referential pointer @@ -378,7 +383,7 @@ where .scope_ref .listeners .borrow_mut() - .push(r.callback as *const _); + .push((r.mounted_node as *const _, r.callback as *const _)); }); self @@ -499,15 +504,19 @@ where let child = item.into_vnode(&self.ctx); self.children.push(child); } - let len_after = self.children.len(); - if len_after > len_before { - let last_child = self.children.last().unwrap(); - if last_child.key().is_none() { - // TODO: Somehow get the name of the component when NodeCtx is being made - const ERR_MSG: &str = r#"Warning: Each child in an array or iterator should have a unique "key" prop. - Check the render method of XXXX. - See fb.me/react-warning-keys for more information. "#; - log::error!("{}", ERR_MSG); + if self.children.len() > len_before + 1 { + if self.children.last().unwrap().key().is_none() { + if cfg!(debug_assertions) { + log::error!( + r#" +Warning: Each child in an array or iterator should have a unique "key" prop. +Not providing a key will lead to poor performance with lists. +See docs.rs/dioxus for more information. +--- +To help you identify where this error is coming from, we've generated a backtrace. + "#, + ); + } } } self @@ -549,17 +558,6 @@ impl<'a, F> VNodeBuilder<'a, LazyNodes<'a, F>> for LazyNodes<'a, F> where { } -// Cover the cases where nodes are pre-rendered. -// Likely used by enums. -// ---- -// let nodes = ctx.render(rsx!{ ... }; -// rsx! { {nodes } } -// impl<'a> IntoVNode<'a> for VNode { -// fn into_vnode(self, _ctx: &NodeCtx<'a>) -> VNode<'a> { -// self.root -// } -// } - // Wrap the the node-builder closure in a concrete type. // --- // This is a bit of a hack to implement the IntoVNode trait for closure types. @@ -611,12 +609,14 @@ where impl<'a> IntoVNode<'a> for () { fn into_vnode(self, ctx: &NodeCtx<'a>) -> VNode<'a> { + todo!(); VNode::Suspended } } impl<'a> IntoVNode<'a> for Option<()> { fn into_vnode(self, ctx: &NodeCtx<'a>) -> VNode<'a> { + todo!(); VNode::Suspended } } @@ -735,7 +735,8 @@ impl<'a, 'b> ChildrenList<'a, 'b> { #[derive(Clone)] pub struct NodeCtx<'a> { pub scope_ref: &'a Scope, - pub listener_id: RefCell, + pub listener_id: Cell, + // pub listener_id: RefCell, } impl<'a> NodeCtx<'a> { @@ -744,11 +745,13 @@ impl<'a> NodeCtx<'a> { &self.scope_ref.cur_frame().bump } - fn text(&self, args: Arguments) -> VNode<'a> { + /// Create some text that's allocated along with the other vnodes + pub fn text(&self, args: Arguments) -> VNode<'a> { text3(self.bump(), args) } - fn element<'b, Listeners, Attributes, Children>( + /// Create an element builder + pub fn element<'b, Listeners, Attributes, Children>( &'b self, tag_name: &'static str, ) -> ElementBuilder< diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index cf245785..ba98d8e0 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -13,7 +13,7 @@ use bumpalo::Bump; use std::{ any::Any, cell::{Cell, RefCell}, - fmt::{Arguments, Debug}, + fmt::{Arguments, Debug, Formatter}, marker::PhantomData, rc::Rc, }; @@ -127,8 +127,8 @@ impl<'a> VNode<'a> { pub fn get_mounted_id(&self) -> Option { match self { - VNode::Element(_) => todo!(), - VNode::Text(_) => todo!(), + VNode::Element(el) => Some(el.dom_id.get()), + VNode::Text(te) => Some(te.dom_id.get()), VNode::Fragment(_) => todo!(), VNode::Suspended => todo!(), VNode::Component(_) => todo!(), @@ -136,6 +136,18 @@ impl<'a> VNode<'a> { } } +impl Debug for VNode<'_> { + fn fmt(&self, s: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + match self { + VNode::Element(el) => write!(s, "element, {}", el.tag_name), + VNode::Text(t) => write!(s, "text, {}", t.text), + VNode::Fragment(_) => write!(s, "fragment"), + VNode::Suspended => write!(s, "suspended"), + VNode::Component(_) => write!(s, "component"), + } + } +} + #[derive(Clone)] pub struct VText<'src> { pub text: &'src str, @@ -201,11 +213,14 @@ pub struct Listener<'bump> { /// The type of event to listen for. pub(crate) event: &'static str, + /// Which scope? + /// This might not actually be relevant pub scope: ScopeIdx, - pub id: usize, + + pub mounted_node: &'bump Cell, /// The callback to invoke when the event happens. - pub(crate) callback: &'bump (dyn Fn(VirtualEvent)), + pub(crate) callback: &'bump dyn Fn(VirtualEvent), } /// The key for keyed children. @@ -259,6 +274,7 @@ pub struct VComponent<'src> { pub key: NodeKey<'src>, pub mounted_root: Cell, + pub ass_scope: RefCell, // pub comparator: Rc bool + 'src>, diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index a36c3c20..55f80c4f 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -24,7 +24,7 @@ use bumpalo::Bump; use generational_arena::Arena; use std::{ any::{Any, TypeId}, - cell::RefCell, + cell::{Cell, RefCell}, collections::{HashMap, HashSet, VecDeque}, fmt::Debug, future::Future, @@ -63,7 +63,7 @@ pub struct VirtualDom { _root_prop_type: std::any::TypeId, } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug, PartialEq)] pub struct RealDomNode(pub u32); impl RealDomNode { pub fn new(id: u32) -> Self { @@ -109,7 +109,7 @@ impl VirtualDom { /// /// let dom = VirtualDom::new(Example); /// ``` - pub fn new(root: impl Fn(Context<()>) -> VNode + 'static) -> Self { + pub fn new(root: FC<()>) -> Self { Self::new_with_props(root, ()) } @@ -141,10 +141,7 @@ impl VirtualDom { /// /// let dom = VirtualDom::new(Example); /// ``` - pub fn new_with_props( - root: impl Fn(Context

) -> VNode + 'static, - root_props: P, - ) -> Self { + pub fn new_with_props(root: FC

, root_props: P) -> Self { let components = ScopeArena::new(Arena::new()); // Normally, a component would be passed as a child in the RSX macro which automatically produces OpaqueComponents @@ -260,11 +257,11 @@ impl VirtualDom { pub fn progress_with_event( &mut self, realdom: &mut Dom, - event: EventTrigger, + trigger: EventTrigger, ) -> Result<()> { - let id = event.component_id.clone(); + let id = trigger.component_id.clone(); - self.components.try_get_mut(id)?.call_listener(event)?; + self.components.try_get_mut(id)?.call_listener(trigger)?; let mut diff_machine = DiffMachine::new( realdom, @@ -397,7 +394,12 @@ pub struct Scope { // - is self-refenrential and therefore needs to point into the bump // Stores references into the listeners attached to the vnodes // NEEDS TO BE PRIVATE - pub(crate) listeners: RefCell>, + pub(crate) listeners: RefCell, *const dyn Fn(VirtualEvent))>>, + // pub(crate) listeners: RefCell>, + // pub(crate) listeners: RefCell>, + // pub(crate) listeners: RefCell>, + // NoHashMap + // pub(crate) listeners: RefCell>, } // We need to pin the hook so it doesn't move as we initialize the list of hooks @@ -450,7 +452,7 @@ impl Scope { let child_nodes = unsafe { std::mem::transmute(child_nodes) }; Self { - child_nodes: child_nodes, + child_nodes, caller, parent, arena_idx, @@ -490,12 +492,12 @@ impl Scope { self.frames.next().bump.reset(); // Remove all the outdated listeners - // - self.listeners - .try_borrow_mut() - .ok() - .ok_or(Error::FatalInternal("Borrowing listener failed"))? - .drain(..); + self.listeners.borrow_mut().clear(); + // self.listeners + // .try_borrow_mut() + // .ok() + // .ok_or(Error::FatalInternal("Borrowing listener failed"))? + // .drain(..); *self.hookidx.borrow_mut() = 0; @@ -529,26 +531,35 @@ impl Scope { // The listener list will be completely drained because the next frame will write over previous listeners pub fn call_listener(&mut self, trigger: EventTrigger) -> Result<()> { let EventTrigger { - listener_id, event, .. + real_node_id, + event, + .. } = trigger; // todo: implement scanning for outdated events - unsafe { - // Convert the raw ptr into an actual object - // This operation is assumed to be safe - let listener_fn = self - .listeners - .try_borrow() - .ok() - .ok_or(Error::FatalInternal("Borrowing listener failed"))? - .get(listener_id as usize) - .ok_or(Error::FatalInternal("Event should exist if triggered"))? - .as_ref() - .ok_or(Error::FatalInternal("Raw event ptr is invalid"))?; - // Run the callback with the user event + // Convert the raw ptr into an actual object + // This operation is assumed to be safe + + log::debug!("Calling listeners! {:?}", self.listeners.borrow().len()); + let listners = self.listeners.borrow(); + let (_, listener) = listners + .iter() + .find(|(domptr, _)| { + let p = unsafe { &**domptr }; + p.get() == real_node_id + }) + .expect(&format!( + "Failed to find real node with ID {:?}", + real_node_id + )); + + // TODO: Don'tdo a linear scan! Do a hashmap lookup! It'll be faster! + unsafe { + let listener_fn = &**listener; listener_fn(event); } + Ok(()) } diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 3242c43f..c0068273 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -27,8 +27,9 @@ atoms = { path="../atoms" } # futures = "0.3.12" # html-validation = { path = "../html-validation", version = "0.1.1" } -# async-channel = "1.6.1" +async-channel = "1.6.1" nohash-hasher = "0.2.0" +anyhow = "1.0.41" # futures-lite = "1.11.3" [dependencies.web-sys] diff --git a/packages/web/examples/demoday.rs b/packages/web/examples/demoday.rs index 50a295b9..7a9a3438 100644 --- a/packages/web/examples/demoday.rs +++ b/packages/web/examples/demoday.rs @@ -11,7 +11,7 @@ fn App(ctx: Context<()>) -> VNode { ctx.render(rsx! { main { class: "dark:bg-gray-800 bg-white relative h-screen" NavBar {} - {(0..10).map(|f| rsx!{ Landing {} })} + {(0..10).map(|f| rsx!(Landing { key: "{f}" }))} } }) } @@ -62,7 +62,7 @@ fn Landing(ctx: Context<()>) -> VNode { div { class: "container mx-auto px-6 flex flex-col justify-between items-center relative py-8" div { class: "flex flex-col" h1 { class: "font-light w-full uppercase text-center text-4xl sm:text-5xl dark:text-white text-gray-800" - "The React Framework for Production" + "The Dioxus Framework for Production" } h2{ class: "font-light max-w-2xl mx-auto w-full text-xl dark:text-white text-gray-500 text-center py-8" "Next.js gives you the best developer experience with all the features you need for production: \n diff --git a/packages/web/examples/hello.rs b/packages/web/examples/hello.rs index 274379aa..d6a2c198 100644 --- a/packages/web/examples/hello.rs +++ b/packages/web/examples/hello.rs @@ -11,11 +11,7 @@ fn main() { } static Example: FC<()> = |ctx| { - let nodes = (0..5).map(|f| { - rsx! { - li {"{f}"} - } - }); + let nodes = (0..15).map(|f| rsx! (li { key: "{f}", "{f}"})); ctx.render(rsx! { div { span { diff --git a/packages/web/examples/jackjill.rs b/packages/web/examples/jackjill.rs index da5958bb..da58dc59 100644 --- a/packages/web/examples/jackjill.rs +++ b/packages/web/examples/jackjill.rs @@ -33,12 +33,11 @@ static Example: FC<()> = |ctx| { diff --git a/packages/web/examples/listy.rs b/packages/web/examples/listy.rs new file mode 100644 index 00000000..82e6b3f5 --- /dev/null +++ b/packages/web/examples/listy.rs @@ -0,0 +1,28 @@ +use dioxus_core as dioxus; +use dioxus_core::prelude::*; +use dioxus_web::WebsysRenderer; + +fn main() { + wasm_logger::init(wasm_logger::Config::new(log::Level::Debug)); + console_error_panic_hook::set_once(); + + log::info!("hello world"); + wasm_bindgen_futures::spawn_local(WebsysRenderer::start(JonsFavoriteCustomApp)); +} + +fn JonsFavoriteCustomApp(cx: Context<()>) -> VNode { + let items = (0..20).map(|f| { + rsx! { + li {"{f}"} + } + }); + + cx.render(rsx! { + div { + "list" + ul { + {items} + } + } + }) +} diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 420199f6..411e8b9a 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -12,7 +12,7 @@ pub use dioxus_core as dioxus; use dioxus_core::{events::EventTrigger, prelude::FC}; pub use dioxus_core::prelude; -pub mod interpreter; +// pub mod interpreter; pub mod new; /// The `WebsysRenderer` provides a way of rendering a Dioxus Virtual DOM to the browser's DOM. @@ -34,7 +34,7 @@ impl WebsysRenderer { /// /// Run the app to completion, panicing if any error occurs while rendering. /// Pairs well with the wasm_bindgen async handler - pub async fn start(root: impl for<'a> Fn(Context<'a, ()>) -> VNode + 'static) { + pub async fn start(root: FC<()>) { Self::new(root).run().await.expect("Virtual DOM failed :("); } @@ -42,7 +42,7 @@ impl WebsysRenderer { /// /// This means that the root component must either consumes its own context, or statics are used to generate the page. /// The root component can access things like routing in its context. - pub fn new(root: impl for<'a> Fn(Context<'a, ()>) -> VNode + 'static) -> Self { + pub fn new(root: FC<()>) -> Self { Self::new_with_props(root, ()) } @@ -50,10 +50,7 @@ impl WebsysRenderer { /// 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_with_props( - root: impl for<'a> Fn(Context<'a, T>) -> VNode + 'static, - root_props: T, - ) -> Self { + pub fn new_with_props(root: FC, root_props: T) -> Self { Self::from_vdom(VirtualDom::new_with_props(root, root_props)) } @@ -63,28 +60,23 @@ impl WebsysRenderer { } pub async fn run(&mut self) -> dioxus_core::error::Result<()> { - // let (sender, mut receiver) = async_channel::unbounded::(); - let body_element = prepare_websys_dom(); - // let mut patch_machine = interpreter::PatchMachine::new(body_element.clone(), move |ev| { - // log::debug!("Event trigger! {:#?}", ev); - // let mut c = sender.clone(); - // wasm_bindgen_futures::spawn_local(async move { - // c.send(ev).await.unwrap(); - // }); - // }); - let root_node = body_element.first_child().unwrap(); - let mut websys_dom = crate::new::WebsysDom::new(body_element); + let mut websys_dom = crate::new::WebsysDom::new(body_element.clone()); websys_dom.stack.push(root_node); - // patch_machine.stack.push(root_node.clone()); - - // todo: initialize the event registry properly on the root self.internal_dom.rebuild(&mut websys_dom)?; + + while let Some(trigger) = websys_dom.wait_for_event().await { + let root_node = body_element.first_child().unwrap(); + websys_dom.stack.push(root_node.clone()); + self.internal_dom + .progress_with_event(&mut websys_dom, trigger)?; + } + // let edits = self.internal_dom.rebuild()?; // log::debug!("Received edits: {:#?}", edits); // edits.iter().for_each(|edit| { diff --git a/packages/web/src/new.rs b/packages/web/src/new.rs index f43cdc42..5c79dea2 100644 --- a/packages/web/src/new.rs +++ b/packages/web/src/new.rs @@ -1,19 +1,35 @@ -use std::collections::HashMap; +use std::{collections::HashMap, rc::Rc, sync::Arc}; -use dioxus_core::virtual_dom::RealDomNode; +use dioxus_core::{ + events::{EventTrigger, VirtualEvent}, + prelude::ScopeIdx, + virtual_dom::RealDomNode, +}; +use fxhash::FxHashMap; use nohash_hasher::IntMap; use wasm_bindgen::{closure::Closure, JsCast}; use web_sys::{ window, Document, Element, Event, HtmlElement, HtmlInputElement, HtmlOptionElement, Node, }; -use crate::interpreter::Stack; pub struct WebsysDom { pub stack: Stack, nodes: IntMap, document: Document, root: Element, + event_receiver: async_channel::Receiver, + trigger: Arc, + + // every callback gets a monotomically increasing callback ID + callback_id: usize, + + // map of listener types to number of those listeners + listeners: FxHashMap)>, + + // Map of callback_id to component index and listener id + callback_map: FxHashMap, + // We need to make sure to add comments between text nodes // We ensure that the text siblings are patched by preventing the browser from merging // neighboring text nodes. Originally inspired by some of React's work from 2016. @@ -33,18 +49,39 @@ impl WebsysDom { .document() .expect("must have access to the Document"); + let (sender, mut receiver) = async_channel::unbounded::(); + + let sender_callback = Arc::new(move |ev| { + let mut c = sender.clone(); + wasm_bindgen_futures::spawn_local(async move { + c.send(ev).await.unwrap(); + }); + }); + + let mut nodes = + HashMap::with_capacity_and_hasher(1000, nohash_hasher::BuildNoHashHasher::default()); + + nodes.insert(0_u32, root.clone().dyn_into::().unwrap()); Self { stack: Stack::with_capacity(10), - nodes: HashMap::with_capacity_and_hasher( - 1000, - nohash_hasher::BuildNoHashHasher::default(), - ), + nodes, + + callback_id: 0, + listeners: FxHashMap::default(), + callback_map: FxHashMap::default(), document, + event_receiver: receiver, + trigger: sender_callback, root, last_node_was_text: false, node_counter: Counter(0), } } + + pub async fn wait_for_event(&mut self) -> Option { + let v = self.event_receiver.recv().await.unwrap(); + Some(v) + } } struct Counter(u32); @@ -56,11 +93,13 @@ impl Counter { } impl dioxus_core::diff::RealDom for WebsysDom { fn push_root(&mut self, root: dioxus_core::virtual_dom::RealDomNode) { + log::debug!("Called `[`push_root] {:?}", root); let domnode = self.nodes.get(&root.0).expect("Failed to pop know root"); self.stack.push(domnode.clone()); } fn append_child(&mut self) { + log::debug!("Called [`append_child`]"); let child = self.stack.pop(); if child.dyn_ref::().is_some() { @@ -81,6 +120,7 @@ impl dioxus_core::diff::RealDom for WebsysDom { } fn replace_with(&mut self) { + log::debug!("Called [`replace_with`]"); let new_node = self.stack.pop(); let old_node = self.stack.pop(); @@ -117,10 +157,12 @@ impl dioxus_core::diff::RealDom for WebsysDom { } fn remove(&mut self) { + log::debug!("Called [`remove`]"); todo!() } fn remove_all_children(&mut self) { + log::debug!("Called [`remove_all_children`]"); todo!() } @@ -134,6 +176,8 @@ impl dioxus_core::diff::RealDom for WebsysDom { self.stack.push(textnode.clone()); self.nodes.insert(nid, textnode); + log::debug!("Called [`create_text_node`]: {}, {}", text, nid); + RealDomNode::new(nid) } @@ -148,6 +192,7 @@ impl dioxus_core::diff::RealDom for WebsysDom { self.stack.push(el.clone()); let nid = self.node_counter.next(); self.nodes.insert(nid, el); + log::debug!("Called [`create_element`]: {}, {:?}", tag, nid); RealDomNode::new(nid) } @@ -166,6 +211,7 @@ impl dioxus_core::diff::RealDom for WebsysDom { self.stack.push(el.clone()); let nid = self.node_counter.next(); self.nodes.insert(nid, el); + log::debug!("Called [`create_element_ns`]: {:}", nid); RealDomNode::new(nid) } @@ -173,81 +219,133 @@ impl dioxus_core::diff::RealDom for WebsysDom { &mut self, event: &str, scope: dioxus_core::prelude::ScopeIdx, - id: usize, + el_id: usize, + real_id: RealDomNode, ) { - // if let Some(entry) = self.listeners.get_mut(event) { - // entry.0 += 1; - // } else { - // let trigger = self.trigger.clone(); - // let handler = Closure::wrap(Box::new(move |event: &web_sys::Event| { - // log::debug!("Handling event!"); + log::debug!( + "Called [`new_event_listener`]: {}, {:?}, {}, {:?}", + event, + scope, + el_id, + real_id + ); + // attach the correct attributes to the element + // these will be used by accessing the event's target + // This ensures we only ever have one handler attached to the root, but decide + // dynamically when we want to call a listener. - // let target = event - // .target() - // .expect("missing target") - // .dyn_into::() - // .expect("not a valid element"); + let el = self.stack.top(); - // let typ = event.type_(); + let el = el + .dyn_ref::() + .expect(&format!("not an element: {:?}", el)); - // let gi_id: Option = target - // .get_attribute(&format!("dioxus-giid-{}", typ)) - // .and_then(|v| v.parse().ok()); + let (gi_id, gi_gen) = (&scope).into_raw_parts(); + el.set_attribute( + &format!("dioxus-event-{}", event), + &format!("{}.{}.{}.{}", gi_id, gi_gen, el_id, real_id.0), + ) + .unwrap(); - // let gi_gen: Option = target - // .get_attribute(&format!("dioxus-gigen-{}", typ)) - // .and_then(|v| v.parse().ok()); + // Register the callback to decode - // let li_idx: Option = target - // .get_attribute(&format!("dioxus-lidx-{}", typ)) - // .and_then(|v| v.parse().ok()); + if let Some(entry) = self.listeners.get_mut(event) { + entry.0 += 1; + } else { + let trigger = self.trigger.clone(); + let handler = Closure::wrap(Box::new(move |event: &web_sys::Event| { + // "Result" cannot be received from JS + // Instead, we just build and immediately execute a closure that returns result + let res = || -> anyhow::Result { + log::debug!("Handling event!"); - // if let (Some(gi_id), Some(gi_gen), Some(li_idx)) = (gi_id, gi_gen, li_idx) { - // // Call the trigger - // log::debug!( - // "decoded gi_id: {}, gi_gen: {}, li_idx: {}", - // gi_id, - // gi_gen, - // li_idx - // ); + let target = event + .target() + .expect("missing target") + .dyn_into::() + .expect("not a valid element"); - // let triggered_scope = ScopeIdx::from_raw_parts(gi_id, gi_gen); - // trigger.0.as_ref()(EventTrigger::new( - // virtual_event_from_websys_event(event), - // triggered_scope, - // // scope, - // li_idx, - // )); - // } - // }) as Box); + let typ = event.type_(); + use anyhow::Context; + let val: String = target + .get_attribute(&format!("dioxus-event-{}", typ)) + .context("")?; - // self.root - // .add_event_listener_with_callback(event, (&handler).as_ref().unchecked_ref()) - // .unwrap(); + let mut fields = val.splitn(4, "."); - // // Increment the listeners - // self.listeners.insert(event.into(), (1, handler)); - // } + let gi_id = fields + .next() + .and_then(|f| f.parse::().ok()) + .context("")?; + let gi_gen = fields + .next() + .and_then(|f| f.parse::().ok()) + .context("")?; + let el_id = fields + .next() + .and_then(|f| f.parse::().ok()) + .context("")?; + let real_id = fields + .next() + .and_then(|f| f.parse::().ok().map(RealDomNode::new)) + .context("")?; + + // Call the trigger + log::debug!( + "decoded gi_id: {}, gi_gen: {}, li_idx: {}", + gi_id, + gi_gen, + el_id + ); + + let triggered_scope = ScopeIdx::from_raw_parts(gi_id, gi_gen); + Ok(EventTrigger::new( + virtual_event_from_websys_event(event), + triggered_scope, + real_id, + )) + }; + + match res() { + Ok(synthetic_event) => trigger.as_ref()(synthetic_event), + Err(_) => log::error!("Error decoding Dioxus event attribute."), + }; + }) as Box); + + self.root + .add_event_listener_with_callback(event, (&handler).as_ref().unchecked_ref()) + .unwrap(); + + // Increment the listeners + self.listeners.insert(event.into(), (1, handler)); + } } fn remove_event_listener(&mut self, event: &str) { + log::debug!("Called [`remove_event_listener`]: {}", event); todo!() } fn set_text(&mut self, text: &str) { + log::debug!("Called [`set_text`]: {}", text); self.stack.top().set_text_content(Some(text)) } fn set_attribute(&mut self, name: &str, value: &str, is_namespaced: bool) { + log::debug!("Called [`set_attribute`]: {}, {}", name, value); if name == "class" { if let Some(el) = self.stack.top().dyn_ref::() { el.set_class_name(value); } } else { + if let Some(el) = self.stack.top().dyn_ref::() { + el.set_attribute(name, value).unwrap(); + } } } fn remove_attribute(&mut self, name: &str) { + log::debug!("Called [`remove_attribute`]: {}", name); let node = self.stack.top(); if let Some(node) = node.dyn_ref::() { node.remove_attribute(name).unwrap(); @@ -270,6 +368,234 @@ impl dioxus_core::diff::RealDom for WebsysDom { } fn raw_node_as_any_mut(&self) -> &mut dyn std::any::Any { + log::debug!("Called [`raw_node_as_any_mut`]"); todo!() } } + +#[derive(Debug, Default)] +pub struct Stack { + list: Vec, +} + +impl Stack { + pub fn with_capacity(cap: usize) -> Self { + Stack { + list: Vec::with_capacity(cap), + } + } + + pub fn push(&mut self, node: Node) { + // debug!("stack-push: {:?}", node); + self.list.push(node); + } + + pub fn pop(&mut self) -> Node { + let res = self.list.pop().unwrap(); + res + } + + pub fn clear(&mut self) { + self.list.clear(); + } + + pub fn top(&self) -> &Node { + match self.list.last() { + Some(a) => a, + None => panic!("Called 'top' of an empty stack, make sure to push the root first"), + } + } +} + +fn virtual_event_from_websys_event(event: &web_sys::Event) -> VirtualEvent { + use dioxus_core::events::on::*; + match event.type_().as_str() { + "copy" | "cut" | "paste" => { + // let evt: web_sys::ClipboardEvent = event.clone().dyn_into().unwrap(); + + todo!() + } + + "compositionend" | "compositionstart" | "compositionupdate" => { + let evt: web_sys::CompositionEvent = event.clone().dyn_into().unwrap(); + todo!() + } + + "keydown" | "keypress" | "keyup" => { + let evt: web_sys::KeyboardEvent = event.clone().dyn_into().unwrap(); + todo!() + } + + "focus" | "blur" => { + let evt: web_sys::FocusEvent = event.clone().dyn_into().unwrap(); + todo!() + } + + "change" => { + let evt: web_sys::Event = event.clone().dyn_into().expect("wrong error typ"); + todo!() + // VirtualEvent::FormEvent(FormEvent {value:}) + } + + "input" | "invalid" | "reset" | "submit" => { + // is a special react events + let evt: web_sys::InputEvent = event.clone().dyn_into().expect("wrong event type"); + let this: web_sys::EventTarget = evt.target().unwrap(); + + let value = (&this) + .dyn_ref() + .map(|input: &web_sys::HtmlInputElement| input.value()) + .or_else(|| { + (&this) + .dyn_ref() + .map(|input: &web_sys::HtmlTextAreaElement| input.value()) + }) + .or_else(|| { + (&this) + .dyn_ref::() + .unwrap() + .text_content() + }) + .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener"); + + // let p2 = evt.data_transfer(); + + // let value: Option = (&evt).data(); + // let value = val; + // let value = value.unwrap_or_default(); + // let value = (&evt).data().expect("No data to unwrap"); + + // todo - this needs to be a "controlled" event + // these events won't carry the right data with them + todo!() + // VirtualEvent::FormEvent(FormEvent { value }) + } + + "click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit" + | "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter" + | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => { + let evt: web_sys::MouseEvent = event.clone().dyn_into().unwrap(); + + #[derive(Debug)] + pub struct CustomMouseEvent(web_sys::MouseEvent); + impl dioxus_core::events::on::MouseEvent for CustomMouseEvent { + fn alt_key(&self) -> bool { + todo!() + } + fn button(&self) -> i32 { + todo!() + } + fn buttons(&self) -> i32 { + todo!() + } + fn client_x(&self) -> i32 { + todo!() + } + fn client_y(&self) -> i32 { + todo!() + } + fn ctrl_key(&self) -> bool { + todo!() + } + fn meta_key(&self) -> bool { + todo!() + } + fn page_x(&self) -> i32 { + todo!() + } + fn page_y(&self) -> i32 { + todo!() + } + fn screen_x(&self) -> i32 { + todo!() + } + fn screen_y(&self) -> i32 { + todo!() + } + fn shift_key(&self) -> bool { + todo!() + } + fn get_modifier_state(&self, key_code: usize) -> bool { + todo!() + } + } + VirtualEvent::MouseEvent(Rc::new(CustomMouseEvent(evt))) + // MouseEvent(Box::new(RawMouseEvent { + // alt_key: evt.alt_key(), + // button: evt.button() as i32, + // buttons: evt.buttons() as i32, + // client_x: evt.client_x(), + // client_y: evt.client_y(), + // ctrl_key: evt.ctrl_key(), + // meta_key: evt.meta_key(), + // page_x: evt.page_x(), + // page_y: evt.page_y(), + // screen_x: evt.screen_x(), + // screen_y: evt.screen_y(), + // shift_key: evt.shift_key(), + // get_modifier_state: GetModifierKey(Box::new(|f| { + // // evt.get_modifier_state(f) + // todo!("This is not yet implemented properly, sorry :("); + // })), + // })) + // todo!() + // VirtualEvent::MouseEvent() + } + + "pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture" + | "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => { + let evt: web_sys::PointerEvent = event.clone().dyn_into().unwrap(); + todo!() + } + + "select" => { + // let evt: web_sys::SelectionEvent = event.clone().dyn_into().unwrap(); + // not required to construct anything special beyond standard event stuff + todo!() + } + + "touchcancel" | "touchend" | "touchmove" | "touchstart" => { + let evt: web_sys::TouchEvent = event.clone().dyn_into().unwrap(); + todo!() + } + + "scroll" => { + // let evt: web_sys::UIEvent = event.clone().dyn_into().unwrap(); + todo!() + } + + "wheel" => { + let evt: web_sys::WheelEvent = event.clone().dyn_into().unwrap(); + todo!() + } + + "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted" + | "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play" + | "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend" + | "timeupdate" | "volumechange" | "waiting" => { + // not required to construct anything special beyond standard event stuff + + // let evt: web_sys::MediaEvent = event.clone().dyn_into().unwrap(); + // let evt: web_sys::MediaEvent = event.clone().dyn_into().unwrap(); + todo!() + } + + "animationstart" | "animationend" | "animationiteration" => { + let evt: web_sys::AnimationEvent = event.clone().dyn_into().unwrap(); + todo!() + } + + "transitionend" => { + let evt: web_sys::TransitionEvent = event.clone().dyn_into().unwrap(); + todo!() + } + + "toggle" => { + // not required to construct anything special beyond standard event stuff (target) + + // let evt: web_sys::ToggleEvent = event.clone().dyn_into().unwrap(); + todo!() + } + _ => VirtualEvent::OtherEvent, + } +} diff --git a/packages/web/src/interpreter.rs b/packages/web/src/old/interpreter.rs similarity index 99% rename from packages/web/src/interpreter.rs rename to packages/web/src/old/interpreter.rs index f494f376..c6410922 100644 --- a/packages/web/src/interpreter.rs +++ b/packages/web/src/old/interpreter.rs @@ -2,7 +2,6 @@ use std::{borrow::Borrow, convert::TryInto, default, fmt::Debug, sync::Arc}; use dioxus_core::{ events::{EventTrigger, VirtualEvent}, - patch::Edit, prelude::ScopeIdx, }; use fxhash::FxHashMap;