diff --git a/packages/core-types/src/event.rs b/packages/core-types/src/event.rs deleted file mode 100644 index c8a5ff68a..000000000 --- a/packages/core-types/src/event.rs +++ /dev/null @@ -1,203 +0,0 @@ -use std::{any::Any, cell::RefCell, rc::Rc}; - -/// A wrapper around some generic data that handles the event's state -/// -/// -/// Prevent this event from continuing to bubble up the tree to parent elements. -/// -/// # Example -/// -/// ```rust, no_run -/// # use dioxus::prelude::*; -/// rsx! { -/// button { -/// onclick: move |evt: Event| { -/// evt.stop_propagation(); -/// } -/// } -/// }; -/// ``` -pub struct Event { - /// The data associated with this event - pub data: Rc, - pub(crate) metadata: Rc>, -} - -#[derive(Clone, Copy)] -pub(crate) struct EventMetadata { - pub(crate) propagates: bool, - pub(crate) prevent_default: bool, -} - -impl Event { - /// Create a new event from the inner data - pub fn new(data: Rc, propagates: bool) -> Self { - Self { - data, - metadata: Rc::new(RefCell::new(EventMetadata { - propagates, - prevent_default: false, - })), - } - } -} - -impl Event { - /// Map the event data to a new type - /// - /// # Example - /// - /// ```rust, no_run - /// # use dioxus::prelude::*; - /// rsx! { - /// button { - /// onclick: move |evt: MouseEvent| { - /// let data = evt.map(|data| data.client_coordinates()); - /// println!("{:?}", data.data()); - /// } - /// } - /// }; - /// ``` - pub fn map U>(&self, f: F) -> Event { - Event { - data: Rc::new(f(&self.data)), - metadata: self.metadata.clone(), - } - } - - /// Prevent this event from continuing to bubble up the tree to parent elements. - /// - /// # Example - /// - /// ```rust, no_run - /// # use dioxus::prelude::*; - /// rsx! { - /// button { - /// onclick: move |evt: Event| { - /// # #[allow(deprecated)] - /// evt.cancel_bubble(); - /// } - /// } - /// }; - /// ``` - #[deprecated = "use stop_propagation instead"] - pub fn cancel_bubble(&self) { - self.metadata.borrow_mut().propagates = false; - } - - /// Check if the event propagates up the tree to parent elements - pub fn propagates(&self) -> bool { - self.metadata.borrow().propagates - } - - /// Prevent this event from continuing to bubble up the tree to parent elements. - /// - /// # Example - /// - /// ```rust, no_run - /// # use dioxus::prelude::*; - /// rsx! { - /// button { - /// onclick: move |evt: Event| { - /// evt.stop_propagation(); - /// } - /// } - /// }; - /// ``` - pub fn stop_propagation(&self) { - self.metadata.borrow_mut().propagates = false; - } - - /// Get a reference to the inner data from this event - /// - /// ```rust, no_run - /// # use dioxus::prelude::*; - /// rsx! { - /// button { - /// onclick: move |evt: Event| { - /// let data = evt.data(); - /// async move { - /// println!("{:?}", data); - /// } - /// } - /// } - /// }; - /// ``` - pub fn data(&self) -> Rc { - self.data.clone() - } - - /// Prevent the default action of the event. - /// - /// # Example - /// - /// ```rust - /// # use dioxus::prelude::*; - /// fn App() -> Element { - /// rsx! { - /// a { - /// // You can prevent the default action of the event with `prevent_default` - /// onclick: move |event| { - /// event.prevent_default(); - /// }, - /// href: "https://dioxuslabs.com", - /// "don't go to the link" - /// } - /// } - /// } - /// ``` - /// - /// Note: This must be called synchronously when handling the event. Calling it after the event has been handled will have no effect. - /// - ///
- /// - /// This method is not available on the LiveView renderer because LiveView handles all events over a websocket which cannot block. - /// - ///
- #[track_caller] - pub fn prevent_default(&self) { - self.metadata.borrow_mut().prevent_default = true; - } - - /// Check if the default action of the event is enabled. - pub fn default_action_enabled(&self) -> bool { - !self.metadata.borrow().prevent_default - } -} - -impl Event { - /// Downcast this `Event`` to a concrete type - pub fn downcast(self) -> Option> { - let data = self.data.downcast::().ok()?; - Some(Event { - metadata: self.metadata.clone(), - data, - }) - } -} - -impl Clone for Event { - fn clone(&self) -> Self { - Self { - metadata: self.metadata.clone(), - data: self.data.clone(), - } - } -} - -impl std::ops::Deref for Event { - type Target = Rc; - fn deref(&self) -> &Self::Target { - &self.data - } -} - -impl std::fmt::Debug for Event { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("UiEvent") - .field("bubble_state", &self.propagates()) - .field("prevent_default", &!self.default_action_enabled()) - .field("data", &self.data) - .finish() - } -} diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 316d15d7d..91789b5ba 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -1,6 +1,8 @@ -use crate::{properties::SuperFrom, runtime::RuntimeGuard, Runtime, ScopeId}; +use crate::{ + prelude::current_scope_id, properties::SuperFrom, runtime::RuntimeGuard, Runtime, ScopeId, +}; use generational_box::GenerationalBox; -use std::{cell::RefCell, marker::PhantomData, panic::Location, rc::Rc}; +use std::{any::Any, cell::RefCell, marker::PhantomData, panic::Location, rc::Rc}; /// A wrapper around some generic data that handles the event's state /// @@ -67,6 +69,17 @@ impl Event { } } + /// Convert this event into a boxed event with a dynamic type + pub fn into_any(self) -> Event + where + T: Sized, + { + Event { + data: self.data as Rc, + metadata: self.metadata, + } + } + /// Prevent this event from continuing to bubble up the tree to parent elements. /// /// # Example @@ -376,6 +389,27 @@ impl< } } +impl< + Function: FnMut(Event) -> Spawn + 'static, + T: 'static, + Spawn: SpawnIfAsync + 'static, + Marker, + > SuperFrom> for ListenerCallback +{ + fn super_from(input: Function) -> Self { + ListenerCallback::new(input) + } +} + +// ListenerCallback can be created from Callback> +impl SuperFrom>> for ListenerCallback { + fn super_from(input: Callback>) -> Self { + // https://github.com/rust-lang/rust-clippy/issues/15072 + #[allow(clippy::redundant_closure)] + ListenerCallback::new(move |event| input(event)) + } +} + #[doc(hidden)] pub struct UnitClosure(PhantomData); @@ -471,14 +505,6 @@ impl Callback { Self { callback, origin } } - /// Leak a new reference to the [`Callback`] that will not be dropped unless the callback is dropped manually - pub(crate) fn leak_reference(&self) -> generational_box::BorrowResult> { - Ok(Callback { - callback: self.callback.leak_reference()?, - origin: self.origin, - }) - } - /// Call this callback with the appropriate argument type /// /// This borrows the callback using a RefCell. Recursively calling a callback will cause a panic. @@ -559,3 +585,87 @@ impl std::ops::Deref for Callback { reference_to_closure as &_ } } + +type AnyEventHandler = Rc)>>; + +/// An owned callback type used in [`AttributeValue::Listener`](crate::AttributeValue::Listener). +/// +/// This is the type that powers the `on` attributes in the `rsx!` macro, allowing you to pass event +/// handlers to elements. +/// +/// ```rust, ignore +/// rsx! { +/// button { +/// onclick: AttributeValue::Listener(ListenerCallback::new(move |evt: Event| { +/// // ... +/// } +/// } +/// } +/// ``` +pub struct ListenerCallback { + pub(crate) origin: ScopeId, + callback: AnyEventHandler, + _marker: PhantomData, +} + +impl Clone for ListenerCallback { + fn clone(&self) -> Self { + Self { + origin: self.origin, + callback: self.callback.clone(), + _marker: PhantomData, + } + } +} + +impl PartialEq for ListenerCallback { + fn eq(&self, other: &Self) -> bool { + // We compare the pointers of the callbacks, since they are unique + Rc::ptr_eq(&self.callback, &other.callback) && self.origin == other.origin + } +} + +impl ListenerCallback { + /// Create a new [`ListenerCallback`] from a callback + /// + /// This is expected to be called within a runtime scope. Make sure a runtime is current before + /// calling this method. + pub fn new(mut f: impl FnMut(Event) -> MaybeAsync + 'static) -> Self + where + T: 'static, + MaybeAsync: SpawnIfAsync, + { + Self { + origin: current_scope_id().expect("ListenerCallback must be created within a scope"), + callback: Rc::new(RefCell::new(move |event: Event| { + let data = event.data.downcast::().unwrap(); + f(Event { + metadata: event.metadata.clone(), + data, + }) + .spawn(); + })), + _marker: PhantomData, + } + } + + /// Call the callback with an event + /// + /// This is expected to be called within a runtime scope. Make sure a runtime is current before + /// calling this method. + pub fn call(&self, event: Event) { + let runtime = Runtime::current().expect("ListenerCallback must be called within a runtime"); + runtime.with_scope_on_stack(self.origin, || { + (self.callback.borrow_mut())(event); + }); + } + + /// Erase the type of the callback, allowing it to be used with any type of event + pub fn erase(self) -> ListenerCallback { + ListenerCallback { + origin: self.origin, + callback: self.callback, + _marker: PhantomData, + } + } +} diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index b289f4798..17a3ca3a5 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -79,10 +79,10 @@ pub(crate) mod innerlude { pub use crate::innerlude::{ fc_to_builder, generation, schedule_update, schedule_update_any, use_hook, vdom_is_rendering, AnyValue, Attribute, AttributeValue, CapturedError, Component, ComponentFunction, DynamicNode, - Element, ElementId, Event, Fragment, HasAttributes, IntoDynNode, LaunchConfig, MarkerWrapper, - Mutation, Mutations, NoOpMutations, Ok, Properties, Result, Runtime, ScopeId, ScopeState, - SpawnIfAsync, Task, Template, TemplateAttribute, TemplateNode, VComponent, VNode, VNodeInner, - VPlaceholder, VText, VirtualDom, WriteMutations, + Element, ElementId, Event, Fragment, HasAttributes, IntoDynNode, LaunchConfig, + ListenerCallback, MarkerWrapper, Mutation, Mutations, NoOpMutations, Ok, Properties, Result, + Runtime, ScopeId, ScopeState, SpawnIfAsync, Task, Template, TemplateAttribute, TemplateNode, + VComponent, VNode, VNodeInner, VPlaceholder, VText, VirtualDom, WriteMutations, }; /// The purpose of this module is to alleviate imports of many common types diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 8f37a7a95..fea7d7762 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -1,11 +1,12 @@ use dioxus_core_types::DioxusFormattable; +use crate::events::ListenerCallback; use crate::innerlude::VProps; use crate::prelude::RenderError; use crate::{any_props::BoxedAnyProps, innerlude::ScopeState}; use crate::{arena::ElementId, Element, Event}; use crate::{ - innerlude::{ElementRef, EventHandler, MountId}, + innerlude::{ElementRef, MountId}, properties::ComponentFunction, }; use crate::{Properties, ScopeId, VirtualDom}; @@ -157,29 +158,6 @@ impl Default for VNode { } } -impl Drop for VNode { - fn drop(&mut self) { - // FIXME: - // TODO: - // - // We have to add this drop *here* because we can't add a drop impl to AttributeValue and - // keep semver compatibility. Adding a drop impl means you can't destructure the value, which - // we need to do for enums. - // - // if dropping this will drop the last vnode (rc count is 1), then we need to drop the listeners - // in this template - if Rc::strong_count(&self.vnode) == 1 { - for attrs in self.vnode.dynamic_attrs.iter() { - for attr in attrs.iter() { - if let AttributeValue::Listener(listener) = &attr.value { - listener.callback.manually_drop(); - } - } - } - } - } -} - impl PartialEq for VNode { fn eq(&self, other: &Self) -> bool { Rc::ptr_eq(&self.vnode, &other.vnode) @@ -810,12 +788,7 @@ impl Attribute { name: self.name, namespace: self.namespace, volatile: self.volatile, - value: match &self.value { - AttributeValue::Listener(listener) => { - AttributeValue::Listener(listener.leak_reference().unwrap()) - } - value => value.clone(), - }, + value: self.value.clone(), } } } @@ -839,7 +812,7 @@ pub enum AttributeValue { Bool(bool), /// A listener, like "onclick" - Listener(ListenerCb), + Listener(ListenerCallback), /// An arbitrary value that implements PartialEq and is static Any(Rc), @@ -852,16 +825,8 @@ impl AttributeValue { /// Create a new [`AttributeValue`] with the listener variant from a callback /// /// The callback must be confined to the lifetime of the ScopeState - pub fn listener(mut callback: impl FnMut(Event) + 'static) -> AttributeValue { - // TODO: maybe don't use the copy-variant of EventHandler here? - // Maybe, create an Owned variant so we are less likely to run into leaks - AttributeValue::Listener(EventHandler::leak(move |event: Event| { - let data = event.data.downcast::().unwrap(); - callback(Event { - metadata: event.metadata.clone(), - data, - }); - })) + pub fn listener(callback: impl FnMut(Event) + 'static) -> AttributeValue { + AttributeValue::Listener(ListenerCallback::new(callback).erase()) } /// Create a new [`AttributeValue`] with a value that implements [`AnyValue`] @@ -870,8 +835,6 @@ impl AttributeValue { } } -pub type ListenerCb = EventHandler>; - impl std::fmt::Debug for AttributeValue { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -879,7 +842,7 @@ impl std::fmt::Debug for AttributeValue { Self::Float(arg0) => f.debug_tuple("Float").field(arg0).finish(), Self::Int(arg0) => f.debug_tuple("Int").field(arg0).finish(), Self::Bool(arg0) => f.debug_tuple("Bool").field(arg0).finish(), - Self::Listener(listener) => f.debug_tuple("Listener").field(listener).finish(), + Self::Listener(_) => f.debug_tuple("Listener").finish(), Self::Any(_) => f.debug_tuple("Any").finish(), Self::None => write!(f, "None"), } @@ -1197,6 +1160,12 @@ impl IntoAttributeValue for Rc { } } +impl IntoAttributeValue for ListenerCallback { + fn into_value(self) -> AttributeValue { + AttributeValue::Listener(self.erase()) + } +} + impl IntoAttributeValue for Option { fn into_value(self) -> AttributeValue { match self { diff --git a/packages/html/src/events/mod.rs b/packages/html/src/events/mod.rs index de290d346..802ff2ede 100644 --- a/packages/html/src/events/mod.rs +++ b/packages/html/src/events/mod.rs @@ -25,18 +25,15 @@ macro_rules! impl_event { #[doc(alias = $js_name)] )? #[inline] - pub fn $name<__Marker>(mut _f: impl ::dioxus_core::prelude::SuperInto<::dioxus_core::prelude::EventHandler<::dioxus_core::Event<$data>>, __Marker>) -> ::dioxus_core::Attribute { - // super into will make a closure that is owned by the current owner (either the child component or the parent component). - // We can't change that behavior in a minor version because it would cause issues with Components that accept event handlers. - // Instead we run super into with an owner that is moved into the listener closure so it will be dropped when the closure is dropped. - let owner = <::generational_box::UnsyncStorage as ::generational_box::AnyStorage>::owner(); - let event_handler = ::dioxus_core::prelude::with_owner(owner.clone(), || _f.super_into()); + pub fn $name<__Marker>(mut _f: impl ::dioxus_core::prelude::SuperInto<::dioxus_core::ListenerCallback<$data>, __Marker>) -> ::dioxus_core::Attribute { + let event_handler = _f.super_into(); ::dioxus_core::Attribute::new( impl_event!(@name $name $($js_name)?), ::dioxus_core::AttributeValue::listener(move |e: ::dioxus_core::Event| { - // Force the owner to be moved into the event handler - _ = &owner; - event_handler.call(e.map(|e| e.into())); + let event: ::dioxus_core::Event<$data> = e.map(|data| { + data.into() + }); + event_handler.call(event.into_any()); }), None, false,