From d84fc0538670b2a3bda9ae41878896793b74e8ee Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 3 Jan 2022 02:06:42 -0500 Subject: [PATCH] feat: plug in bubbling --- packages/core/src/events.rs | 159 +++++++++++ packages/core/src/lib.rs | 22 +- packages/core/src/nodes.rs | 8 +- packages/core/src/properties.rs | 164 +++++++++++ packages/core/src/scopes.rs | 17 +- packages/core/src/util.rs | 78 ++++++ packages/core/src/virtual_dom.rs | 455 +++---------------------------- packages/desktop/src/events.rs | 28 +- packages/hooks/src/lib.rs | 78 +++--- packages/html/src/events.rs | 92 ++++--- 10 files changed, 579 insertions(+), 522 deletions(-) create mode 100644 packages/core/src/events.rs create mode 100644 packages/core/src/properties.rs create mode 100644 packages/core/src/util.rs diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs new file mode 100644 index 000000000..c7295c56d --- /dev/null +++ b/packages/core/src/events.rs @@ -0,0 +1,159 @@ +//! Internal and external event system +//! +//! +//! This is all kinda WIP, but the bones are there. + +use crate::{ElementId, ScopeId}; +use std::{any::Any, cell::Cell, rc::Rc, sync::Arc}; + +pub(crate) struct BubbleState { + pub canceled: Cell, +} + +impl BubbleState { + pub fn new() -> Self { + Self { + canceled: Cell::new(false), + } + } +} + +/// User Events are events that are shuttled from the renderer into the VirtualDom trhough the scheduler channel. +/// +/// These events will be passed to the appropriate Element given by `mounted_dom_id` and then bubbled up through the tree +/// where each listener is checked and fired if the event name matches. +/// +/// It is the expectation that the event name matches the corresponding event listener, otherwise Dioxus will panic in +/// attempting to downcast the event data. +/// +/// Because Event Data is sent across threads, it must be `Send + Sync`. We are hoping to lift the `Sync` restriction but +/// `Send` will not be lifted. The entire `UserEvent` must also be `Send + Sync` due to its use in the scheduler channel. +/// +/// # Example +/// ```rust, ignore +/// fn App(cx: Scope) -> Element { +/// rsx!(cx, div { +/// onclick: move |_| println!("Clicked!") +/// }) +/// } +/// +/// let mut dom = VirtualDom::new(App); +/// let mut scheduler = dom.get_scheduler_channel(); +/// scheduler.unbounded_send(SchedulerMsg::UiEvent( +/// UserEvent { +/// scope_id: None, +/// priority: EventPriority::Medium, +/// name: "click", +/// element: Some(ElementId(0)), +/// data: Arc::new(ClickEvent { .. }) +/// } +/// )).unwrap(); +/// ``` +#[derive(Debug)] +pub struct UserEvent { + /// The originator of the event trigger if available + pub scope_id: Option, + + /// The priority of the event to be scheduled around ongoing work + pub priority: EventPriority, + + /// The optional real node associated with the trigger + pub element: Option, + + /// The event type IE "onclick" or "onmouseover" + pub name: &'static str, + + /// The event data to be passed onto the event handler + pub data: Arc, +} + +/// Priority of Event Triggers. +/// +/// Internally, Dioxus will abort work that's taking too long if new, more important work arrives. Unlike React, Dioxus +/// won't be afraid to pause work or flush changes to the RealDOM. This is called "cooperative scheduling". Some Renderers +/// 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. +/// +/// React has a 5-tier priority system. However, they break things into "Continuous" and "Discrete" priority. For now, +/// we keep it simple, and just use a 3-tier priority system. +/// +/// - NoPriority = 0 +/// - LowPriority = 1 +/// - NormalPriority = 2 +/// - UserBlocking = 3 +/// - HighPriority = 4 +/// - ImmediatePriority = 5 +/// +/// We still have a concept of discrete vs continuous though - discrete events won't be batched, but continuous events will. +/// This means that multiple "scroll" events will be processed in a single frame, but multiple "click" events will be +/// flushed before proceeding. Multiple discrete events is highly unlikely, though. +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)] +pub enum EventPriority { + /// Work that must be completed during the EventHandler phase. + /// + /// Currently this is reserved for controlled inputs. + Immediate = 3, + + /// "High Priority" work will not interrupt other high priority work, but will interrupt medium and low priority work. + /// + /// This is typically reserved for things like user interaction. + /// + /// React calls these "discrete" events, but with an extra category of "user-blocking" (Immediate). + High = 2, + + /// "Medium priority" work is generated by page events not triggered by the user. These types of events are less important + /// than "High Priority" events and will take precedence over low priority events. + /// + /// This is typically reserved for VirtualEvents that are not related to keyboard or mouse input. + /// + /// React calls these "continuous" events (e.g. mouse move, mouse wheel, touch move, etc). + Medium = 1, + + /// "Low Priority" work will always be preempted unless the work is significantly delayed, in which case it will be + /// advanced to the front of the work queue until completed. + /// + /// The primary user of Low Priority work is the asynchronous work system (Suspense). + /// + /// This is considered "idle" work or "background" work. + Low = 0, +} + +pub struct AnyEvent { + pub(crate) bubble_state: Rc, + pub(crate) data: Arc, +} + +impl AnyEvent { + pub fn downcast(self) -> Option> { + let AnyEvent { data, bubble_state } = self; + + if let Ok(data) = data.downcast::() { + Some(UiEvent { bubble_state, data }) + } else { + None + } + } +} + +pub struct UiEvent { + pub data: Arc, + + #[allow(unused)] + bubble_state: Rc, +} + +impl std::ops::Deref for UiEvent { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.data.as_ref() + } +} + +impl UiEvent { + /// Prevent this event from bubbling up the tree. + pub fn cancel_bubble(&self) { + self.bubble_state.canceled.set(true); + } +} diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 494c772a1..dba4effbb 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -2,18 +2,24 @@ #![doc = include_str!("../README.md")] pub(crate) mod diff; +pub(crate) mod events; pub(crate) mod lazynodes; pub(crate) mod mutations; pub(crate) mod nodes; +pub(crate) mod properties; pub(crate) mod scopes; +pub(crate) mod util; pub(crate) mod virtual_dom; pub(crate) mod innerlude { pub(crate) use crate::diff::*; + pub use crate::events::*; pub use crate::lazynodes::*; pub use crate::mutations::*; pub use crate::nodes::*; + pub use crate::properties::*; pub use crate::scopes::*; + pub use crate::util::*; pub use crate::virtual_dom::*; /// An [`Element`] is a possibly-none [`VNode`] created by calling `render` on [`Scope`] or [`ScopeState`]. @@ -65,21 +71,17 @@ pub(crate) mod innerlude { } pub use crate::innerlude::{ - Attribute, Component, DioxusElement, DomEdit, Element, ElementId, ElementIdIterator, Event, + AnyEvent, Attribute, Component, DioxusElement, DomEdit, Element, ElementId, ElementIdIterator, EventHandler, EventPriority, IntoVNode, LazyNodes, Listener, Mutations, NodeFactory, - Properties, SchedulerMsg, Scope, ScopeId, ScopeState, TaskId, UserEvent, VComponent, VElement, - VFragment, VNode, VPlaceholder, VText, VirtualDom, + Properties, SchedulerMsg, Scope, ScopeId, ScopeState, TaskId, UiEvent, UserEvent, VComponent, + VElement, VFragment, VNode, VPlaceholder, VText, VirtualDom, }; pub mod prelude { - pub use crate::innerlude::Scope; pub use crate::innerlude::{ - Attributes, Component, DioxusElement, Element, EventHandler, LazyNodes, NodeFactory, - ScopeState, + fc_to_builder, Attributes, Component, DioxusElement, Element, EventHandler, Fragment, + LazyNodes, NodeFactory, Properties, Scope, ScopeState, VNode, VirtualDom, }; - pub use crate::nodes::VNode; - pub use crate::virtual_dom::{fc_to_builder, Fragment, Properties}; - pub use crate::VirtualDom; } pub mod exports { @@ -93,7 +95,7 @@ pub mod exports { pub(crate) mod unsafe_utils { use crate::VNode; - pub unsafe fn extend_vnode<'a, 'b>(node: &'a VNode<'a>) -> &'b VNode<'b> { + pub(crate) unsafe fn extend_vnode<'a, 'b>(node: &'a VNode<'a>) -> &'b VNode<'b> { std::mem::transmute(node) } } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 36cca7f0c..6c2b3f0b7 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -6,14 +6,12 @@ use crate::{ innerlude::{Element, Properties, Scope, ScopeId, ScopeState}, lazynodes::LazyNodes, - Component, + AnyEvent, Component, }; use bumpalo::{boxed::Box as BumpBox, Bump}; use std::{ - any::Any, cell::{Cell, RefCell}, fmt::{Arguments, Debug, Formatter}, - sync::Arc, }; /// A composable "VirtualNode" to declare a User Interface in the Dioxus VirtualDOM. @@ -330,7 +328,7 @@ pub struct EventHandler<'bump> { } impl EventHandler<'_> { - pub fn call(&self, event: Arc) { + pub fn call(&self, event: AnyEvent) { if let Some(callback) = self.callback.borrow_mut().as_mut() { callback(event); } @@ -340,7 +338,7 @@ impl EventHandler<'_> { } } -type ListenerCallback<'bump> = BumpBox<'bump, dyn FnMut(Arc) + 'bump>; +type ListenerCallback<'bump> = BumpBox<'bump, dyn FnMut(AnyEvent) + 'bump>; impl Copy for EventHandler<'_> {} impl Clone for EventHandler<'_> { diff --git a/packages/core/src/properties.rs b/packages/core/src/properties.rs new file mode 100644 index 000000000..f42bc36ba --- /dev/null +++ b/packages/core/src/properties.rs @@ -0,0 +1,164 @@ +use crate::innerlude::*; + +pub struct FragmentProps<'a>(Element<'a>); +pub struct FragmentBuilder<'a, const BUILT: bool>(Element<'a>); +impl<'a> FragmentBuilder<'a, false> { + pub fn children(self, children: Element<'a>) -> FragmentBuilder<'a, true> { + FragmentBuilder(children) + } +} +impl<'a, const A: bool> FragmentBuilder<'a, A> { + pub fn build(self) -> FragmentProps<'a> { + FragmentProps(self.0) + } +} + +/// Access the children elements passed into the component +/// +/// This enables patterns where a component is passed children from its parent. +/// +/// ## Details +/// +/// Unlike React, Dioxus allows *only* lists of children to be passed from parent to child - not arbitrary functions +/// or classes. If you want to generate nodes instead of accepting them as a list, consider declaring a closure +/// on the props that takes Context. +/// +/// If a parent passes children into a component, the child will always re-render when the parent re-renders. In other +/// words, a component cannot be automatically memoized if it borrows nodes from its parent, even if the component's +/// props are valid for the static lifetime. +/// +/// ## Example +/// +/// ```rust, ignore +/// fn App(cx: Scope) -> Element { +/// cx.render(rsx!{ +/// CustomCard { +/// h1 {}2 +/// p {} +/// } +/// }) +/// } +/// +/// #[derive(PartialEq, Props)] +/// struct CardProps { +/// children: Element +/// } +/// +/// fn CustomCard(cx: Scope) -> Element { +/// cx.render(rsx!{ +/// div { +/// h1 {"Title card"} +/// {cx.props.children} +/// } +/// }) +/// } +/// ``` +impl<'a> Properties for FragmentProps<'a> { + type Builder = FragmentBuilder<'a, false>; + const IS_STATIC: bool = false; + fn builder() -> Self::Builder { + FragmentBuilder(None) + } + unsafe fn memoize(&self, _other: &Self) -> bool { + false + } +} + +/// Create inline fragments using Component syntax. +/// +/// ## Details +/// +/// Fragments capture a series of children without rendering extra nodes. +/// +/// Creating fragments explicitly with the Fragment component is particularly useful when rendering lists or tables and +/// a key is needed to identify each item. +/// +/// ## Example +/// +/// ```rust, ignore +/// rsx!{ +/// Fragment { key: "abc" } +/// } +/// ``` +/// +/// ## Usage +/// +/// Fragments are incredibly useful when necessary, but *do* add cost in the diffing phase. +/// Try to avoid highly nested fragments if you can. Unlike React, there is no protection against infinitely nested fragments. +/// +/// This function defines a dedicated `Fragment` component that can be used to create inline fragments in the RSX macro. +/// +/// You want to use this free-function when your fragment needs a key and simply returning multiple nodes from rsx! won't cut it. +#[allow(non_upper_case_globals, non_snake_case)] +pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element { + let i = cx.props.0.as_ref().map(|f| f.decouple()); + cx.render(LazyNodes::new(|f| f.fragment_from_iter(i))) +} + +/// Every "Props" used for a component must implement the `Properties` trait. This trait gives some hints to Dioxus +/// on how to memoize the props and some additional optimizations that can be made. We strongly encourage using the +/// derive macro to implement the `Properties` trait automatically as guarantee that your memoization strategy is safe. +/// +/// If your props are 'static, then Dioxus will require that they also be PartialEq for the derived memoize strategy. However, +/// if your props borrow data, then the memoization strategy will simply default to "false" and the PartialEq will be ignored. +/// This tends to be useful when props borrow something that simply cannot be compared (IE a reference to a closure); +/// +/// By default, the memoization strategy is very conservative, but can be tuned to be more aggressive manually. However, +/// this is only safe if the props are 'static - otherwise you might borrow references after-free. +/// +/// We strongly suggest that any changes to memoization be done at the "PartialEq" level for 'static props. Additionally, +/// we advise the use of smart pointers in cases where memoization is important. +/// +/// ## Example +/// +/// For props that are 'static: +/// ```rust, ignore +/// #[derive(Props, PartialEq)] +/// struct MyProps { +/// data: String +/// } +/// ``` +/// +/// For props that borrow: +/// +/// ```rust, ignore +/// #[derive(Props)] +/// struct MyProps<'a >{ +/// data: &'a str +/// } +/// ``` +pub trait Properties: Sized { + type Builder; + const IS_STATIC: bool; + fn builder() -> Self::Builder; + + /// Memoization can only happen if the props are valid for the 'static lifetime + /// + /// # Safety + /// The user must know if their props are static, but if they make a mistake, UB happens + /// Therefore it's unsafe to memoize. + unsafe fn memoize(&self, other: &Self) -> bool; +} + +impl Properties for () { + type Builder = EmptyBuilder; + const IS_STATIC: bool = true; + fn builder() -> Self::Builder { + EmptyBuilder {} + } + unsafe fn memoize(&self, _other: &Self) -> bool { + true + } +} +// We allow components to use the () generic parameter if they have no props. This impl enables the "build" method +// that the macros use to anonymously complete prop construction. +pub struct EmptyBuilder; +impl EmptyBuilder { + pub fn build(self) {} +} + +/// This utility function launches the builder method so rsx! and html! macros can use the typed-builder pattern +/// to initialize a component's props. +pub fn fc_to_builder<'a, T: Properties + 'a>(_: fn(Scope<'a, T>) -> Element) -> T::Builder { + T::builder() +} diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 61c60ec02..8f35a3ec0 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -278,19 +278,29 @@ impl ScopeArena { let nodes = self.nodes.borrow(); let mut cur_el = Some(element); + let state = Rc::new(BubbleState::new()); + while let Some(id) = cur_el.take() { if let Some(el) = nodes.get(id.0) { let real_el = unsafe { &**el }; if let VNode::Element(real_el) = real_el { for listener in real_el.listeners.borrow().iter() { if listener.event == event.name { + if state.canceled.get() { + // stop bubbling if canceled + break; + } + let mut cb = listener.callback.callback.borrow_mut(); if let Some(cb) = cb.as_mut() { // todo: arcs are pretty heavy to clone // we really want to convert arc to rc // unfortunately, the SchedulerMsg must be send/sync to be sent across threads // we could convert arc to rc internally or something - (cb)(event.data.clone()); + (cb)(AnyEvent { + bubble_state: state.clone(), + data: event.data.clone(), + }); } } } @@ -702,10 +712,9 @@ impl ScopeState { /// } ///``` pub fn render<'src>(&'src self, rsx: LazyNodes<'src, '_>) -> Option> { - let fac = NodeFactory { + Some(rsx.call(NodeFactory { bump: &self.wip_frame().bump, - }; - Some(rsx.call(fac)) + })) } /// Store a value between renders diff --git a/packages/core/src/util.rs b/packages/core/src/util.rs new file mode 100644 index 000000000..f29a13e17 --- /dev/null +++ b/packages/core/src/util.rs @@ -0,0 +1,78 @@ +use crate::innerlude::*; + +pub struct ElementIdIterator<'a> { + vdom: &'a VirtualDom, + + // Heuristcally we should never bleed into 5 completely nested fragments/components + // Smallvec lets us stack allocate our little stack machine so the vast majority of cases are sane + stack: smallvec::SmallVec<[(u16, &'a VNode<'a>); 5]>, +} + +impl<'a> ElementIdIterator<'a> { + pub fn new(vdom: &'a VirtualDom, node: &'a VNode<'a>) -> Self { + Self { + vdom, + stack: smallvec::smallvec![(0, node)], + } + } +} + +impl<'a> Iterator for ElementIdIterator<'a> { + type Item = &'a VNode<'a>; + + fn next(&mut self) -> Option<&'a VNode<'a>> { + let mut should_pop = false; + let mut returned_node = None; + let mut should_push = None; + + while returned_node.is_none() { + if let Some((count, node)) = self.stack.last_mut() { + match node { + // We can only exit our looping when we get "real" nodes + VNode::Element(_) | VNode::Text(_) | VNode::Placeholder(_) => { + // We've recursed INTO an element/text + // We need to recurse *out* of it and move forward to the next + // println!("Found element! Returning it!"); + should_pop = true; + returned_node = Some(&**node); + } + + // If we get a fragment we push the next child + VNode::Fragment(frag) => { + let _count = *count as usize; + if _count >= frag.children.len() { + should_pop = true; + } else { + should_push = Some(&frag.children[_count]); + } + } + + // For components, we load their root and push them onto the stack + VNode::Component(sc) => { + let scope = self.vdom.get_scope(sc.scope.get().unwrap()).unwrap(); + // Simply swap the current node on the stack with the root of the component + *node = scope.root_node(); + } + } + } else { + // If there's no more items on the stack, we're done! + return None; + } + + if should_pop { + self.stack.pop(); + if let Some((id, _)) = self.stack.last_mut() { + *id += 1; + } + should_pop = false; + } + + if let Some(push) = should_push { + self.stack.push((0, push)); + should_push = None; + } + } + + returned_node + } +} diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 6e6aea94a..3cbb3fe5d 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -4,10 +4,10 @@ use crate::innerlude::*; use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender}; -use futures_util::{Future, StreamExt}; +use futures_util::{future::poll_fn, StreamExt}; use fxhash::FxHashSet; use indexmap::IndexSet; -use std::{any::Any, collections::VecDeque, iter::FromIterator, pin::Pin, sync::Arc, task::Poll}; +use std::{collections::VecDeque, iter::FromIterator, task::Poll}; /// A virtual node s ystem that progresses user events and diffs UI trees. /// @@ -113,6 +113,18 @@ pub struct VirtualDom { ), } +#[derive(Debug)] +pub enum SchedulerMsg { + // events from the host + Event(UserEvent), + + // setstate + Immediate(ScopeId), + + // an async task pushed from an event handler (or just spawned) + NewTask(ScopeId), +} + // Methods to create the VirtualDom impl VirtualDom { /// Create a new VirtualDom with a component that does not have special props. @@ -260,6 +272,7 @@ impl VirtualDom { self.channel.0.clone() } + /// Try to get an element from its ElementId pub fn get_element(&self, id: ElementId) -> Option<&VNode> { self.scopes.get_element(id) } @@ -318,7 +331,35 @@ impl VirtualDom { if self.scopes.tasks.has_tasks() { use futures_util::future::{select, Either}; - match select(PollTasks(&mut self.scopes), self.channel.1.next()).await { + let scopes = &mut self.scopes; + let task_poll = poll_fn(|cx| { + // + let mut any_pending = false; + + let mut tasks = scopes.tasks.tasks.borrow_mut(); + let mut to_remove = vec![]; + + // this would be better served by retain + for (id, task) in tasks.iter_mut() { + if task.as_mut().poll(cx).is_ready() { + to_remove.push(*id); + } else { + any_pending = true; + } + } + + for id in to_remove { + tasks.remove(&id); + } + + // Resolve the future if any singular task is ready + match any_pending { + true => Poll::Pending, + false => Poll::Ready(()), + } + }); + + match select(task_poll, self.channel.1.next()).await { Either::Left((_, _)) => {} Either::Right((msg, _)) => self.pending_messages.push_front(msg.unwrap()), } @@ -699,411 +740,3 @@ impl Drop for VirtualDom { } } } - -struct PollTasks<'a>(&'a mut ScopeArena); - -impl<'a> Future for PollTasks<'a> { - type Output = (); - - fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { - let mut any_pending = false; - - let mut tasks = self.0.tasks.tasks.borrow_mut(); - let mut to_remove = vec![]; - - // this would be better served by retain - for (id, task) in tasks.iter_mut() { - if task.as_mut().poll(cx).is_ready() { - to_remove.push(*id); - } else { - any_pending = true; - } - } - - for id in to_remove { - tasks.remove(&id); - } - - // Resolve the future if any singular task is ready - match any_pending { - true => Poll::Pending, - false => Poll::Ready(()), - } - } -} - -#[derive(Debug)] -pub enum SchedulerMsg { - // events from the host - Event(UserEvent), - - // setstate - Immediate(ScopeId), - - // an async task pushed from an event handler (or just spawned) - NewTask(ScopeId), -} - -/// User Events are events that are shuttled from the renderer into the VirtualDom trhough the scheduler channel. -/// -/// These events will be passed to the appropriate Element given by `mounted_dom_id` and then bubbled up through the tree -/// where each listener is checked and fired if the event name matches. -/// -/// It is the expectation that the event name matches the corresponding event listener, otherwise Dioxus will panic in -/// attempting to downcast the event data. -/// -/// Because Event Data is sent across threads, it must be `Send + Sync`. We are hoping to lift the `Sync` restriction but -/// `Send` will not be lifted. The entire `UserEvent` must also be `Send + Sync` due to its use in the scheduler channel. -/// -/// # Example -/// ```rust, ignore -/// fn App(cx: Scope) -> Element { -/// rsx!(cx, div { -/// onclick: move |_| println!("Clicked!") -/// }) -/// } -/// -/// let mut dom = VirtualDom::new(App); -/// let mut scheduler = dom.get_scheduler_channel(); -/// scheduler.unbounded_send(SchedulerMsg::UiEvent( -/// UserEvent { -/// scope_id: None, -/// priority: EventPriority::Medium, -/// name: "click", -/// element: Some(ElementId(0)), -/// data: Arc::new(ClickEvent { .. }) -/// } -/// )).unwrap(); -/// ``` -#[derive(Debug)] -pub struct UserEvent { - /// The originator of the event trigger if available - pub scope_id: Option, - - /// The priority of the event to be scheduled around ongoing work - pub priority: EventPriority, - - /// The optional real node associated with the trigger - pub element: Option, - - /// The event type IE "onclick" or "onmouseover" - pub name: &'static str, - - /// The event data to be passed onto the event handler - pub data: Arc, -} - -/// Priority of Event Triggers. -/// -/// Internally, Dioxus will abort work that's taking too long if new, more important work arrives. Unlike React, Dioxus -/// won't be afraid to pause work or flush changes to the RealDOM. This is called "cooperative scheduling". Some Renderers -/// 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. -/// -/// React has a 5-tier priority system. However, they break things into "Continuous" and "Discrete" priority. For now, -/// we keep it simple, and just use a 3-tier priority system. -/// -/// - NoPriority = 0 -/// - LowPriority = 1 -/// - NormalPriority = 2 -/// - UserBlocking = 3 -/// - HighPriority = 4 -/// - ImmediatePriority = 5 -/// -/// We still have a concept of discrete vs continuous though - discrete events won't be batched, but continuous events will. -/// This means that multiple "scroll" events will be processed in a single frame, but multiple "click" events will be -/// flushed before proceeding. Multiple discrete events is highly unlikely, though. -#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)] -pub enum EventPriority { - /// Work that must be completed during the EventHandler phase. - /// - /// Currently this is reserved for controlled inputs. - Immediate = 3, - - /// "High Priority" work will not interrupt other high priority work, but will interrupt medium and low priority work. - /// - /// This is typically reserved for things like user interaction. - /// - /// React calls these "discrete" events, but with an extra category of "user-blocking" (Immediate). - High = 2, - - /// "Medium priority" work is generated by page events not triggered by the user. These types of events are less important - /// than "High Priority" events and will take precedence over low priority events. - /// - /// This is typically reserved for VirtualEvents that are not related to keyboard or mouse input. - /// - /// React calls these "continuous" events (e.g. mouse move, mouse wheel, touch move, etc). - Medium = 1, - - /// "Low Priority" work will always be preempted unless the work is significantly delayed, in which case it will be - /// advanced to the front of the work queue until completed. - /// - /// The primary user of Low Priority work is the asynchronous work system (Suspense). - /// - /// This is considered "idle" work or "background" work. - Low = 0, -} - -pub struct FragmentProps<'a>(Element<'a>); -pub struct FragmentBuilder<'a, const BUILT: bool>(Element<'a>); -impl<'a> FragmentBuilder<'a, false> { - pub fn children(self, children: Element<'a>) -> FragmentBuilder<'a, true> { - FragmentBuilder(children) - } -} -impl<'a, const A: bool> FragmentBuilder<'a, A> { - pub fn build(self) -> FragmentProps<'a> { - FragmentProps(self.0) - } -} - -/// Access the children elements passed into the component -/// -/// This enables patterns where a component is passed children from its parent. -/// -/// ## Details -/// -/// Unlike React, Dioxus allows *only* lists of children to be passed from parent to child - not arbitrary functions -/// or classes. If you want to generate nodes instead of accepting them as a list, consider declaring a closure -/// on the props that takes Context. -/// -/// If a parent passes children into a component, the child will always re-render when the parent re-renders. In other -/// words, a component cannot be automatically memoized if it borrows nodes from its parent, even if the component's -/// props are valid for the static lifetime. -/// -/// ## Example -/// -/// ```rust, ignore -/// fn App(cx: Scope) -> Element { -/// cx.render(rsx!{ -/// CustomCard { -/// h1 {}2 -/// p {} -/// } -/// }) -/// } -/// -/// #[derive(PartialEq, Props)] -/// struct CardProps { -/// children: Element -/// } -/// -/// fn CustomCard(cx: Scope) -> Element { -/// cx.render(rsx!{ -/// div { -/// h1 {"Title card"} -/// {cx.props.children} -/// } -/// }) -/// } -/// ``` -impl<'a> Properties for FragmentProps<'a> { - type Builder = FragmentBuilder<'a, false>; - const IS_STATIC: bool = false; - fn builder() -> Self::Builder { - FragmentBuilder(None) - } - unsafe fn memoize(&self, _other: &Self) -> bool { - false - } -} - -/// Create inline fragments using Component syntax. -/// -/// ## Details -/// -/// Fragments capture a series of children without rendering extra nodes. -/// -/// Creating fragments explicitly with the Fragment component is particularly useful when rendering lists or tables and -/// a key is needed to identify each item. -/// -/// ## Example -/// -/// ```rust, ignore -/// rsx!{ -/// Fragment { key: "abc" } -/// } -/// ``` -/// -/// ## Usage -/// -/// Fragments are incredibly useful when necessary, but *do* add cost in the diffing phase. -/// Try to avoid highly nested fragments if you can. Unlike React, there is no protection against infinitely nested fragments. -/// -/// This function defines a dedicated `Fragment` component that can be used to create inline fragments in the RSX macro. -/// -/// You want to use this free-function when your fragment needs a key and simply returning multiple nodes from rsx! won't cut it. -#[allow(non_upper_case_globals, non_snake_case)] -pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element { - let i = cx.props.0.as_ref().map(|f| f.decouple()); - cx.render(LazyNodes::new(|f| f.fragment_from_iter(i))) -} - -/// Every "Props" used for a component must implement the `Properties` trait. This trait gives some hints to Dioxus -/// on how to memoize the props and some additional optimizations that can be made. We strongly encourage using the -/// derive macro to implement the `Properties` trait automatically as guarantee that your memoization strategy is safe. -/// -/// If your props are 'static, then Dioxus will require that they also be PartialEq for the derived memoize strategy. However, -/// if your props borrow data, then the memoization strategy will simply default to "false" and the PartialEq will be ignored. -/// This tends to be useful when props borrow something that simply cannot be compared (IE a reference to a closure); -/// -/// By default, the memoization strategy is very conservative, but can be tuned to be more aggressive manually. However, -/// this is only safe if the props are 'static - otherwise you might borrow references after-free. -/// -/// We strongly suggest that any changes to memoization be done at the "PartialEq" level for 'static props. Additionally, -/// we advise the use of smart pointers in cases where memoization is important. -/// -/// ## Example -/// -/// For props that are 'static: -/// ```rust, ignore -/// #[derive(Props, PartialEq)] -/// struct MyProps { -/// data: String -/// } -/// ``` -/// -/// For props that borrow: -/// -/// ```rust, ignore -/// #[derive(Props)] -/// struct MyProps<'a >{ -/// data: &'a str -/// } -/// ``` -pub trait Properties: Sized { - type Builder; - const IS_STATIC: bool; - fn builder() -> Self::Builder; - - /// Memoization can only happen if the props are valid for the 'static lifetime - /// - /// # Safety - /// The user must know if their props are static, but if they make a mistake, UB happens - /// Therefore it's unsafe to memoize. - unsafe fn memoize(&self, other: &Self) -> bool; -} - -impl Properties for () { - type Builder = EmptyBuilder; - const IS_STATIC: bool = true; - fn builder() -> Self::Builder { - EmptyBuilder {} - } - unsafe fn memoize(&self, _other: &Self) -> bool { - true - } -} -// We allow components to use the () generic parameter if they have no props. This impl enables the "build" method -// that the macros use to anonymously complete prop construction. -pub struct EmptyBuilder; -impl EmptyBuilder { - pub fn build(self) {} -} - -/// This utility function launches the builder method so rsx! and html! macros can use the typed-builder pattern -/// to initialize a component's props. -pub fn fc_to_builder<'a, T: Properties + 'a>(_: fn(Scope<'a, T>) -> Element) -> T::Builder { - T::builder() -} - -pub struct Event { - data: Arc, - _cancel: std::rc::Rc>, -} - -impl std::ops::Deref for Event { - type Target = T; - - fn deref(&self) -> &Self::Target { - self.data.as_ref() - } -} - -impl Event { - pub fn cancel(&self) {} - pub fn timestamp(&self) {} - pub fn triggered_element(&self) -> Option { - None - } -} - -pub struct ElementIdIterator<'a> { - vdom: &'a VirtualDom, - - // Heuristcally we should never bleed into 5 completely nested fragments/components - // Smallvec lets us stack allocate our little stack machine so the vast majority of cases are sane - stack: smallvec::SmallVec<[(u16, &'a VNode<'a>); 5]>, -} - -impl<'a> ElementIdIterator<'a> { - pub fn new(vdom: &'a VirtualDom, node: &'a VNode<'a>) -> Self { - Self { - vdom, - stack: smallvec::smallvec![(0, node)], - } - } -} - -impl<'a> Iterator for ElementIdIterator<'a> { - type Item = &'a VNode<'a>; - - fn next(&mut self) -> Option<&'a VNode<'a>> { - let mut should_pop = false; - let mut returned_node = None; - let mut should_push = None; - - while returned_node.is_none() { - if let Some((count, node)) = self.stack.last_mut() { - match node { - // We can only exit our looping when we get "real" nodes - VNode::Element(_) | VNode::Text(_) | VNode::Placeholder(_) => { - // We've recursed INTO an element/text - // We need to recurse *out* of it and move forward to the next - // println!("Found element! Returning it!"); - should_pop = true; - returned_node = Some(&**node); - } - - // If we get a fragment we push the next child - VNode::Fragment(frag) => { - let _count = *count as usize; - if _count >= frag.children.len() { - should_pop = true; - } else { - should_push = Some(&frag.children[_count]); - } - } - - // For components, we load their root and push them onto the stack - VNode::Component(sc) => { - // todo!(); - let scope = self.vdom.get_scope(sc.scope.get().unwrap()).unwrap(); - - // Simply swap the current node on the stack with the root of the component - *node = scope.root_node(); - } - } - } else { - // If there's no more items on the stack, we're done! - return None; - } - - if should_pop { - self.stack.pop(); - if let Some((id, _)) = self.stack.last_mut() { - *id += 1; - } - should_pop = false; - } - - if let Some(push) = should_push { - self.stack.push((0, push)); - should_push = None; - } - } - - returned_node - } -} diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index 8f86c3270..026b543c8 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -43,63 +43,63 @@ fn make_synthetic_event(name: &str, val: serde_json::Value) -> Arc { // - Arc::new(ClipboardEvent {}) + Arc::new(ClipboardData {}) } "compositionend" | "compositionstart" | "compositionupdate" => { - Arc::new(serde_json::from_value::(val).unwrap()) + Arc::new(serde_json::from_value::(val).unwrap()) } "keydown" | "keypress" | "keyup" => { - let evt = serde_json::from_value::(val).unwrap(); + let evt = serde_json::from_value::(val).unwrap(); Arc::new(evt) } "focus" | "blur" => { // - Arc::new(FocusEvent {}) + Arc::new(FocusData {}) } // todo: these handlers might get really slow if the input box gets large and allocation pressure is heavy // don't have a good solution with the serialized event problem "change" | "input" | "invalid" | "reset" | "submit" => { - Arc::new(serde_json::from_value::(val).unwrap()) + Arc::new(serde_json::from_value::(val).unwrap()) } "click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit" | "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter" | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => { - Arc::new(serde_json::from_value::(val).unwrap()) + Arc::new(serde_json::from_value::(val).unwrap()) } "pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture" | "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => { - Arc::new(serde_json::from_value::(val).unwrap()) + Arc::new(serde_json::from_value::(val).unwrap()) } "select" => { // - Arc::new(serde_json::from_value::(val).unwrap()) + Arc::new(serde_json::from_value::(val).unwrap()) } "touchcancel" | "touchend" | "touchmove" | "touchstart" => { - Arc::new(serde_json::from_value::(val).unwrap()) + Arc::new(serde_json::from_value::(val).unwrap()) } "scroll" => Arc::new(()), - "wheel" => Arc::new(serde_json::from_value::(val).unwrap()), + "wheel" => Arc::new(serde_json::from_value::(val).unwrap()), "animationstart" | "animationend" | "animationiteration" => { - Arc::new(serde_json::from_value::(val).unwrap()) + Arc::new(serde_json::from_value::(val).unwrap()) } - "transitionend" => Arc::new(serde_json::from_value::(val).unwrap()), + "transitionend" => Arc::new(serde_json::from_value::(val).unwrap()), "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted" | "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play" | "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend" | "timeupdate" | "volumechange" | "waiting" => { // - Arc::new(MediaEvent {}) + Arc::new(MediaData {}) } - "toggle" => Arc::new(ToggleEvent {}), + "toggle" => Arc::new(ToggleData {}), _ => Arc::new(()), } diff --git a/packages/hooks/src/lib.rs b/packages/hooks/src/lib.rs index 0050d1779..139e05245 100644 --- a/packages/hooks/src/lib.rs +++ b/packages/hooks/src/lib.rs @@ -16,45 +16,45 @@ pub use usefuture::*; mod usesuspense; pub use usesuspense::*; -#[macro_export] -macro_rules! to_owned { - ($($es:ident),+) => {$( - #[allow(unused_mut)] - let mut $es = $es.to_owned(); - )*} -} +// #[macro_export] +// macro_rules! to_owned { +// ($($es:ident),+) => {$( +// #[allow(unused_mut)] +// let mut $es = $es.to_owned(); +// )*} +// } -/// Calls `for_async` on the series of paramters. -/// -/// If the type is Clone, then it will be cloned. However, if the type is not `clone` -/// then it must have a `for_async` method for Rust to lower down into. -/// -/// See: how use_state implements `for_async` but *not* through the trait. -#[macro_export] -macro_rules! for_async { - ($($es:ident),+) => {$( - #[allow(unused_mut)] - let mut $es = $es.for_async(); - )*} -} +// /// Calls `for_async` on the series of paramters. +// /// +// /// If the type is Clone, then it will be cloned. However, if the type is not `clone` +// /// then it must have a `for_async` method for Rust to lower down into. +// /// +// /// See: how use_state implements `for_async` but *not* through the trait. +// #[macro_export] +// macro_rules! for_async { +// ($($es:ident),+) => {$( +// #[allow(unused_mut)] +// let mut $es = $es.for_async(); +// )*} +// } -/// This is a marker trait that uses decoherence. -/// -/// It is *not* meant for hooks to actually implement, but rather defer to their -/// underlying implementation if they *don't* implement the trait. -/// -/// -pub trait AsyncHook { - type Output; - fn for_async(self) -> Self::Output; -} +// /// This is a marker trait that uses decoherence. +// /// +// /// It is *not* meant for hooks to actually implement, but rather defer to their +// /// underlying implementation if they *don't* implement the trait. +// /// +// /// +// pub trait AsyncHook { +// type Output; +// fn for_async(self) -> Self::Output; +// } -impl AsyncHook for T -where - T: ToOwned, -{ - type Output = T; - fn for_async(self) -> Self::Output { - self - } -} +// impl AsyncHook for T +// where +// T: ToOwned, +// { +// type Output = T; +// fn for_async(self) -> Self::Output { +// self +// } +// } diff --git a/packages/html/src/events.rs b/packages/html/src/events.rs index 52c5b906e..59f605cd5 100644 --- a/packages/html/src/events.rs +++ b/packages/html/src/events.rs @@ -1,16 +1,13 @@ use bumpalo::boxed::Box as BumpBox; use dioxus_core::exports::bumpalo; use dioxus_core::*; -use std::any::Any; pub mod on { use super::*; - use std::sync::Arc; macro_rules! event_directory { ( $( $( #[$attr:meta] )* - $wrapper:ident: [ - // $eventdata:ident($wrapper:ident): [ + $wrapper:ident($data:ident): [ $( $( #[$method_attr:meta] )* $name:ident @@ -24,19 +21,21 @@ pub mod on { factory: NodeFactory<'a>, mut callback: F, ) -> Listener<'a> - where F: FnMut(Arc<$wrapper>) + 'a + where F: FnMut($wrapper) + 'a { let bump = &factory.bump(); + + use dioxus_core::{AnyEvent}; // we can't allocate unsized in bumpalo's box, so we need to craft the box manually // safety: this is essentially the same as calling Box::new() but manually // The box is attached to the lifetime of the bumpalo allocator - let cb: &mut dyn FnMut(Arc) = bump.alloc(move |evt: Arc| { - let event = evt.downcast::<$wrapper>().unwrap(); + let cb: &mut dyn FnMut(AnyEvent) = bump.alloc(move |evt: AnyEvent| { + let event = evt.downcast::<$data>().unwrap(); callback(event) }); - let callback: BumpBox) + 'a> = unsafe { BumpBox::from_raw(cb) }; + let callback: BumpBox = unsafe { BumpBox::from_raw(cb) }; // ie oncopy let event_name = stringify!($name); @@ -58,7 +57,7 @@ pub mod on { // The Dioxus Synthetic event system // todo: move these into the html event system. dioxus accepts *any* event, so having these here doesn't make sense. event_directory! { - ClipboardEvent: [ + ClipboardEvent(ClipboardData): [ /// Called when "copy" oncopy @@ -69,7 +68,7 @@ pub mod on { onpaste ]; - CompositionEvent: [ + CompositionEvent(CompositionData): [ /// oncompositionend oncompositionend @@ -80,7 +79,7 @@ pub mod on { oncompositionupdate ]; - KeyboardEvent: [ + KeyboardEvent(KeyboardData): [ /// onkeydown onkeydown @@ -91,7 +90,7 @@ pub mod on { onkeyup ]; - FocusEvent: [ + FocusEvent(FocusData): [ /// onfocus onfocus @@ -99,7 +98,7 @@ pub mod on { onblur ]; - FormEvent: [ + FormEvent(FormData): [ /// onchange onchange @@ -157,7 +156,7 @@ pub mod on { /// - [`onmouseout`] /// - [`onmouseover`] /// - [`onmouseup`] - MouseEvent: [ + MouseEvent(MouseData): [ /// Execute a callback when a button is clicked. /// /// ## Description @@ -167,7 +166,7 @@ pub mod on { /// /// - Bubbles: Yes /// - Cancelable: Yes - /// - Interface: [`MouseEvent`] + /// - Interface(InteData): [`MouseEvent`] /// /// If the button is pressed on one element and the pointer is moved outside the element before the button /// is released, the event is fired on the most specific ancestor element that contained both elements. @@ -240,7 +239,7 @@ pub mod on { onmouseup ]; - PointerEvent: [ + PointerEvent(PointerData): [ /// pointerdown onpointerdown @@ -272,12 +271,12 @@ pub mod on { onpointerout ]; - SelectionEvent: [ + SelectionEvent(SelectionData): [ /// onselect onselect ]; - TouchEvent: [ + TouchEvent(TouchData): [ /// ontouchcancel ontouchcancel @@ -291,12 +290,12 @@ pub mod on { ontouchstart ]; - WheelEvent: [ + WheelEvent(WheelData): [ /// onwheel ]; - MediaEvent: [ + MediaEvent(MediaData): [ ///abort onabort @@ -367,7 +366,7 @@ pub mod on { onwaiting ]; - AnimationEvent: [ + AnimationEvent(AnimationData): [ /// onanimationstart onanimationstart @@ -378,32 +377,35 @@ pub mod on { onanimationiteration ]; - TransitionEvent: [ + TransitionEvent(TransitionData): [ /// ontransitionend ]; - ToggleEvent: [ + ToggleEvent(ToggleData): [ /// ontoggle ]; } + pub type ClipboardEvent = UiEvent; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug)] - pub struct ClipboardEvent( + pub struct ClipboardData { // DOMDataTransfer clipboardData - ); + } + pub type CompositionEvent = UiEvent; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug)] - pub struct CompositionEvent { + pub struct CompositionData { pub data: String, } + pub type KeyboardEvent = UiEvent; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug)] - pub struct KeyboardEvent { + pub struct KeyboardData { pub char_code: u32, /// Identify which "key" was entered. @@ -469,20 +471,23 @@ pub mod on { // get_modifier_state: bool, } + pub type FocusEvent = UiEvent; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug)] - pub struct FocusEvent {/* DOMEventInner: Send + SyncTarget relatedTarget */} + pub struct FocusData {/* DOMEventInner: Send + SyncTarget relatedTarget */} + pub type FormEvent = UiEvent; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug)] - pub struct FormEvent { + pub struct FormData { pub value: String, /* DOMEvent: Send + SyncTarget relatedTarget */ } + pub type MouseEvent = UiEvent; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug)] - pub struct MouseEvent { + pub struct MouseData { pub alt_key: bool, pub button: i16, pub buttons: u16, @@ -498,9 +503,10 @@ pub mod on { // fn get_modifier_state(&self, key_code: &str) -> bool; } + pub type PointerEvent = UiEvent; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug)] - pub struct PointerEvent { + pub struct PointerData { // Mouse only pub alt_key: bool, pub button: i16, @@ -527,13 +533,15 @@ pub mod on { // pub get_modifier_state: bool, } + pub type SelectionEvent = UiEvent; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug)] - pub struct SelectionEvent {} + pub struct SelectionData {} + pub type TouchEvent = UiEvent; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug)] - pub struct TouchEvent { + pub struct TouchData { pub alt_key: bool, pub ctrl_key: bool, pub meta_key: bool, @@ -544,44 +552,50 @@ pub mod on { // touches: DOMTouchList, } + pub type WheelEvent = UiEvent; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug)] - pub struct WheelEvent { + pub struct WheelData { pub delta_mode: u32, pub delta_x: f64, pub delta_y: f64, pub delta_z: f64, } + pub type MediaEvent = UiEvent; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug)] - pub struct MediaEvent {} + pub struct MediaData {} + pub type ImageEvent = UiEvent; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug)] - pub struct ImageEvent { + pub struct ImageData { pub load_error: bool, } + pub type AnimationEvent = UiEvent; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug)] - pub struct AnimationEvent { + pub struct AnimationData { pub animation_name: String, pub pseudo_element: String, pub elapsed_time: f32, } + pub type TransitionEvent = UiEvent; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug)] - pub struct TransitionEvent { + pub struct TransitionData { pub property_name: String, pub pseudo_element: String, pub elapsed_time: f32, } + pub type ToggleEvent = UiEvent; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug)] - pub struct ToggleEvent {} + pub struct ToggleData {} } #[cfg_attr(