diff --git a/packages/core/src/context.rs b/packages/core/src/context.rs index dd35ce4b9..c0eca029f 100644 --- a/packages/core/src/context.rs +++ b/packages/core/src/context.rs @@ -102,6 +102,10 @@ impl<'src> Context<'src> { self.scope.memoized_updater.clone() } + pub fn needs_update(&self) { + (self.scope.memoized_updater)() + } + /// Schedule an update for any component given its ScopeId. /// /// A component's ScopeId can be obtained from `use_hook` or the [`Context::scope_id`] method. @@ -178,65 +182,33 @@ impl<'src> Context<'src> { /// struct SharedState(&'static str); /// /// static App: FC<()> = |cx, props|{ - /// cx.use_provide_state(|| SharedState("world")); + /// cx.provide_state(SharedState("world")); /// rsx!(cx, Child {}) /// } /// /// static Child: FC<()> = |cx, props|{ - /// let state = cx.use_consume_state::(); + /// let state = cx.consume_state::(); /// rsx!(cx, div { "hello {state.0}" }) /// } /// ``` - pub fn use_provide_state(self, init: F) -> &'src Rc + pub fn provide_state(self, value: T) -> Option> where T: 'static, - F: FnOnce() -> T, { - let is_initialized = self.use_hook( - |_| false, - |s| { - let i = *s; - *s = true; - i - }, - |_| {}, - ); - - if !is_initialized { - let existing = self - .scope - .shared_contexts - .borrow_mut() - .insert(TypeId::of::(), Rc::new(init())); - - if existing.is_some() { - log::warn!( - "A shared state was replaced with itself. \ - This is does not result in a panic, but is probably not what you are trying to do" - ); - } - } - - self.use_consume_state().unwrap() + self.scope + .shared_contexts + .borrow_mut() + .insert(TypeId::of::(), Rc::new(value)) + .map(|f| f.downcast::().ok()) + .flatten() } - /// Uses a context, storing the cached value around - /// - /// If a context is not found on the first search, then this call will be "dud", always returning "None" even if a - /// context was added later. This allows using another hook as a fallback - /// - pub fn use_consume_state(self) -> Option<&'src Rc> { - struct UseContextHook(Option>); - self.use_hook( - move |_| { - let getter = &self.scope.shared.get_shared_context; - let ty = TypeId::of::(); - let idx = self.scope.our_arena_idx; - UseContextHook(getter(idx, ty).map(|f| f.downcast().unwrap())) - }, - move |hook| hook.0.as_ref(), - |_| {}, - ) + /// Try to retrive a SharedState with type T from the any parent Scope. + pub fn consume_state(self) -> Option> { + let getter = &self.scope.shared.get_shared_context; + let ty = TypeId::of::(); + let idx = self.scope.our_arena_idx; + getter(idx, ty).map(|f| f.downcast().unwrap()) } /// Create a new subtree with this scope as the root of the subtree. @@ -250,15 +222,11 @@ impl<'src> Context<'src> { /// /// ```rust /// static App: FC<()> = |cx, props| { - /// let id = cx.get_current_subtree(); - /// let id = cx.use_create_subtree(); - /// subtree { - /// - /// } + /// todo!(); /// rsx!(cx, div { "Subtree {id}"}) /// }; /// ``` - pub fn use_create_subtree(self) -> Option { + pub fn create_subtree(self) -> Option { self.scope.new_subtree() } diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 69584d749..062eb744d 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -326,7 +326,7 @@ impl<'bump> DiffMachine<'bump> { } for attr in *attributes { - self.mutations.set_attribute(attr); + self.mutations.set_attribute(attr, real_id.as_u64()); } if !children.is_empty() { @@ -428,9 +428,7 @@ impl<'bump> DiffMachine<'bump> { fn diff_text_nodes(&mut self, old: &'bump VText<'bump>, new: &'bump VText<'bump>) { if let Some(root) = old.dom_id.get() { if old.text != new.text { - self.mutations.push_root(root); - self.mutations.set_text(new.text); - self.mutations.pop(); + self.mutations.set_text(new.text, root.as_u64()); } new.dom_id.set(Some(root)); @@ -443,7 +441,7 @@ impl<'bump> DiffMachine<'bump> { new: &'bump VElement<'bump>, new_node: &'bump VNode<'bump>, ) { - let root = old.dom_id.get(); + let root = old.dom_id.get().unwrap(); // If the element type is completely different, the element needs to be re-rendered completely // This is an optimization React makes due to how users structure their code @@ -462,24 +460,13 @@ impl<'bump> DiffMachine<'bump> { return; } - new.dom_id.set(root); + new.dom_id.set(Some(root)); // todo: attributes currently rely on the element on top of the stack, but in theory, we only need the id of the // element to modify its attributes. // it would result in fewer instructions if we just set the id directly. // it would also clean up this code some, but that's not very important anyways - // Don't push the root if we don't have to - let mut has_comitted = false; - let mut please_commit = |edits: &mut Vec| { - if !has_comitted { - has_comitted = true; - edits.push(PushRoot { - root: root.unwrap().as_u64(), - }); - } - }; - // Diff Attributes // // It's extraordinarily rare to have the number/order of attributes change @@ -489,20 +476,17 @@ impl<'bump> DiffMachine<'bump> { if old.attributes.len() == new.attributes.len() { for (old_attr, new_attr) in old.attributes.iter().zip(new.attributes.iter()) { if old_attr.value != new_attr.value { - please_commit(&mut self.mutations.edits); - self.mutations.set_attribute(new_attr); + self.mutations.set_attribute(new_attr, root.as_u64()); } else if new_attr.is_volatile { - please_commit(&mut self.mutations.edits); - self.mutations.set_attribute(new_attr); + self.mutations.set_attribute(new_attr, root.as_u64()); } } } else { - please_commit(&mut self.mutations.edits); for attribute in old.attributes { - self.mutations.remove_attribute(attribute); + self.mutations.remove_attribute(attribute, root.as_u64()); } for attribute in new.attributes { - self.mutations.set_attribute(attribute) + self.mutations.set_attribute(attribute, root.as_u64()) } } @@ -520,20 +504,20 @@ impl<'bump> DiffMachine<'bump> { if old.listeners.len() == new.listeners.len() { for (old_l, new_l) in old.listeners.iter().zip(new.listeners.iter()) { if old_l.event != new_l.event { - please_commit(&mut self.mutations.edits); - self.mutations.remove_event_listener(old_l.event); + self.mutations + .remove_event_listener(old_l.event, root.as_u64()); self.mutations.new_event_listener(new_l, cur_scope_id); } new_l.mounted_node.set(old_l.mounted_node.get()); self.attach_listener_to_scope(new_l, scope); } } else { - please_commit(&mut self.mutations.edits); for listener in old.listeners { - self.mutations.remove_event_listener(listener.event); + self.mutations + .remove_event_listener(listener.event, root.as_u64()); } for listener in new.listeners { - listener.mounted_node.set(root); + listener.mounted_node.set(Some(root)); self.mutations.new_event_listener(listener, cur_scope_id); self.attach_listener_to_scope(listener, scope); } @@ -541,13 +525,12 @@ impl<'bump> DiffMachine<'bump> { } if old.children.len() == 0 && new.children.len() != 0 { - please_commit(&mut self.mutations.edits); + self.mutations.edits.push(PushRoot { + root: root.as_u64(), + }); self.stack.create_children(new.children, MountType::Append); } else { self.diff_children(old.children, new.children); - if has_comitted { - self.mutations.pop(); - } } } diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index 474af1699..a26ed0c6e 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -90,16 +90,16 @@ impl<'a> Mutations<'a> { root: element_id, }); } - pub(crate) fn remove_event_listener(&mut self, event: &'static str) { - self.edits.push(RemoveEventListener { event }); + pub(crate) fn remove_event_listener(&mut self, event: &'static str, root: u64) { + self.edits.push(RemoveEventListener { event, root }); } // modify - pub(crate) fn set_text(&mut self, text: &'a str) { - self.edits.push(SetText { text }); + pub(crate) fn set_text(&mut self, text: &'a str, root: u64) { + self.edits.push(SetText { text, root }); } - pub(crate) fn set_attribute(&mut self, attribute: &'a Attribute) { + pub(crate) fn set_attribute(&mut self, attribute: &'a Attribute, root: u64) { let Attribute { name, value, @@ -111,12 +111,13 @@ impl<'a> Mutations<'a> { field: name, value, ns: *namespace, + root, }); } - pub(crate) fn remove_attribute(&mut self, attribute: &Attribute) { + pub(crate) fn remove_attribute(&mut self, attribute: &Attribute, root: u64) { let name = attribute.name; - self.edits.push(RemoveAttribute { name }); + self.edits.push(RemoveAttribute { name, root }); } } @@ -206,17 +207,21 @@ pub enum DomEdit<'bump> { root: u64, }, RemoveEventListener { + root: u64, event: &'static str, }, SetText { + root: u64, text: &'bump str, }, SetAttribute { + root: u64, field: &'static str, value: &'bump str, ns: Option<&'bump str>, }, RemoveAttribute { + root: u64, name: &'static str, }, } diff --git a/packages/core/tests/diffing.rs b/packages/core/tests/diffing.rs index 5becd6a70..7ab0731a6 100644 --- a/packages/core/tests/diffing.rs +++ b/packages/core/tests/diffing.rs @@ -45,13 +45,10 @@ fn html_and_rsx_generate_the_same_output() { assert_eq!( change.edits, - [ - PushRoot { root: 1 }, - SetText { - text: "Goodbye world" - }, - PopRoot - ] + [SetText { + text: "Goodbye world", + root: 1 + },] ); } diff --git a/packages/core/tests/sharedstate.rs b/packages/core/tests/sharedstate.rs index 59a541c75..b82893373 100644 --- a/packages/core/tests/sharedstate.rs +++ b/packages/core/tests/sharedstate.rs @@ -18,12 +18,12 @@ fn shared_state_test() { struct MySharedState(&'static str); static App: FC<()> = |cx, props| { - cx.use_provide_state(|| MySharedState("world!")); + cx.provide_state(MySharedState("world!")); rsx!(cx, Child {}) }; static Child: FC<()> = |cx, props| { - let shared = cx.use_consume_state::()?; + let shared = cx.consume_state::()?; rsx!(cx, "Hello, {shared.0}") }; diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index ac99be534..02242434b 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -54,7 +54,7 @@ pub struct WebviewWindowProps<'a> { /// /// pub fn WebviewWindow<'a>(cx: Context<'a>, props: &'a WebviewWindowProps) -> DomTree<'a> { - let dtcx = cx.use_consume_state::>()?; + let dtcx = cx.consume_state::>()?; cx.use_hook( |_| { diff --git a/packages/hooks/src/lib.rs b/packages/hooks/src/lib.rs index 6a0722d95..3c5de0f2b 100644 --- a/packages/hooks/src/lib.rs +++ b/packages/hooks/src/lib.rs @@ -3,3 +3,6 @@ pub use usestate::{use_state, AsyncUseState, UseState}; mod useref; pub use useref::*; + +mod use_shared_state; +pub use use_shared_state::*; diff --git a/packages/hooks/src/use_shared_state.rs b/packages/hooks/src/use_shared_state.rs new file mode 100644 index 000000000..e2a8f5399 --- /dev/null +++ b/packages/hooks/src/use_shared_state.rs @@ -0,0 +1,176 @@ +use dioxus_core::{prelude::Context, ScopeId}; +use std::{ + cell::{Cell, Ref, RefCell, RefMut}, + collections::HashSet, + rc::Rc, +}; + +type ProvidedState = RefCell>; + +// Tracks all the subscribers to a shared State +pub(crate) struct ProvidedStateInner { + value: Rc>, + notify_any: Rc, + consumers: HashSet, +} + +impl ProvidedStateInner { + pub(crate) fn notify_consumers(&mut self) { + for consumer in self.consumers.iter() { + (self.notify_any)(*consumer); + } + } +} + +/// This hook provides some relatively light ergonomics around shared state. +/// +/// It is not a substitute for a proper state management system, but it is capable enough to provide use_state - type +/// ergonimics in a pinch, with zero cost. +/// +/// # Example +/// +/// ## Provider +/// +/// ```rust +/// +/// +/// ``` +/// +/// ## Consumer +/// +/// ```rust +/// +/// +/// ``` +/// +/// # How it works +/// +/// Any time a component calls `write`, every consumer of the state will be notified - excluding the provider. +/// +/// Right now, there is not a distinction between read-only and write-only, so every consumer will be notified. +/// +/// +/// +pub fn use_shared_state<'a, T: 'static>(cx: Context<'a>) -> Option> { + cx.use_hook( + |_| { + let scope_id = cx.scope_id(); + let root = cx.consume_state::>(); + + if let Some(root) = root.as_ref() { + root.borrow_mut().consumers.insert(scope_id); + } + + let value = root.as_ref().map(|f| f.borrow().value.clone()); + SharedStateInner { + root, + value, + scope_id, + needs_notification: Cell::new(false), + } + }, + |f| { + // + f.needs_notification.set(false); + match (&f.value, &f.root) { + (Some(value), Some(root)) => Some(UseSharedState { + cx, + value, + root, + needs_notification: &f.needs_notification, + }), + _ => None, + } + }, + |f| { + // we need to unsubscribe when our component is unounted + if let Some(root) = &f.root { + let mut root = root.borrow_mut(); + root.consumers.remove(&f.scope_id); + } + }, + ) +} + +struct SharedStateInner { + root: Option>>, + value: Option>>, + scope_id: ScopeId, + needs_notification: Cell, +} + +pub struct UseSharedState<'a, T: 'static> { + pub(crate) cx: Context<'a>, + pub(crate) value: &'a Rc>, + pub(crate) root: &'a Rc>>, + pub(crate) needs_notification: &'a Cell, +} + +impl<'a, T: 'static> UseSharedState<'a, T> { + pub fn read(&self) -> Ref<'_, T> { + self.value.borrow() + } + + pub fn notify_consumers(self) { + if !self.needs_notification.get() { + self.needs_notification.set(true); + self.root.borrow_mut().notify_consumers(); + } + } + + pub fn read_write(&self) -> (Ref<'_, T>, &Self) { + (self.read(), self) + } + + /// Calling "write" will force the component to re-render + /// + /// + /// TODO: We prevent unncessary notifications only in the hook, but we should figure out some more global lock + pub fn write(&self) -> RefMut<'_, T> { + self.cx.needs_update(); + self.notify_consumers(); + self.value.borrow_mut() + } + + /// Allows the ability to write the value without forcing a re-render + pub fn write_silent(&self) -> RefMut<'_, T> { + self.value.borrow_mut() + } +} + +impl Copy for UseSharedState<'_, T> {} +impl<'a, T> Clone for UseSharedState<'a, T> +where + T: 'static, +{ + fn clone(&self) -> Self { + UseSharedState { + cx: self.cx, + value: self.value, + root: self.root, + needs_notification: self.needs_notification, + } + } +} + +/// Provide some state for components down the hierarchy to consume without having to drill props. +/// +/// +/// +/// +/// +/// +/// +pub fn use_provide_state<'a, T: 'static>(cx: Context<'a>, f: impl FnOnce() -> T) -> Option<()> { + cx.use_hook( + |_| { + cx.provide_state(ProvidedStateInner { + value: Rc::new(RefCell::new(f())), + notify_any: cx.schedule_update_any(), + consumers: HashSet::new(), + }) + }, + |inner| inner.as_ref().and_then(|_| Some(())), + |_| {}, + ) +} diff --git a/packages/hooks/src/usestate.rs b/packages/hooks/src/usestate.rs index 649f90ef8..be5ad9e94 100644 --- a/packages/hooks/src/usestate.rs +++ b/packages/hooks/src/usestate.rs @@ -2,7 +2,7 @@ use dioxus_core::prelude::Context; use std::{ cell::{Cell, Ref, RefCell, RefMut}, fmt::Display, - ops::{Deref, DerefMut, Not}, + ops::Not, rc::Rc, }; diff --git a/packages/html/src/lib.rs b/packages/html/src/lib.rs index 9aba10101..c2d824f22 100644 --- a/packages/html/src/lib.rs +++ b/packages/html/src/lib.rs @@ -518,6 +518,8 @@ pub trait GlobalAttributes { /// Specifies how an element is positioned. position: "position", + pointer_events: "pointer-events", + /// Specifies quotation marks for embedded quotations. quotes: "quotes", diff --git a/packages/web/examples/async_web.rs b/packages/web/examples/async_web.rs index 1e1f8b384..d3ff63b0f 100644 --- a/packages/web/examples/async_web.rs +++ b/packages/web/examples/async_web.rs @@ -1,9 +1,9 @@ //! Basic example that renders a simple VNode to the browser. use dioxus_core as dioxus; use dioxus_core::prelude::*; +use dioxus_core_macro::*; use dioxus_hooks::*; use dioxus_html as dioxus_elements; -use dioxus_core_macro::*; fn main() { console_error_panic_hook::set_once(); diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index ec6385e01..7cfae2bae 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -122,11 +122,18 @@ impl WebsysDom { root: mounted_node_id, } => self.new_event_listener(event_name, scope, mounted_node_id), - DomEdit::RemoveEventListener { event } => self.remove_event_listener(event), + DomEdit::RemoveEventListener { event, root } => { + self.remove_event_listener(event, root) + } - DomEdit::SetText { text } => self.set_text(text), - DomEdit::SetAttribute { field, value, ns } => self.set_attribute(field, value, ns), - DomEdit::RemoveAttribute { name } => self.remove_attribute(name), + DomEdit::SetText { text, root } => self.set_text(text, root), + DomEdit::SetAttribute { + field, + value, + ns, + root, + } => self.set_attribute(field, value, ns, root), + DomEdit::RemoveAttribute { name, root } => self.remove_attribute(name, root), DomEdit::InsertAfter { n, root } => self.insert_after(n, root), DomEdit::InsertBefore { n, root } => self.insert_before(n, root), @@ -302,16 +309,17 @@ impl WebsysDom { } } - fn remove_event_listener(&mut self, event: &str) { + fn remove_event_listener(&mut self, event: &str, root: u64) { // todo!() } - fn set_text(&mut self, text: &str) { - self.stack.top().set_text_content(Some(text)) + fn set_text(&mut self, text: &str, root: u64) { + let el = self.nodes[root as usize].as_ref().unwrap(); + el.set_text_content(Some(text)) } - fn set_attribute(&mut self, name: &str, value: &str, ns: Option<&str>) { - let node = self.stack.top(); + fn set_attribute(&mut self, name: &str, value: &str, ns: Option<&str>, root: u64) { + let node = self.nodes[root as usize].as_ref().unwrap(); if ns == Some("style") { if let Some(el) = node.dyn_ref::() { let el = el.dyn_ref::().unwrap(); @@ -345,7 +353,11 @@ impl WebsysDom { } "checked" => { if let Some(input) = node.dyn_ref::() { - input.set_checked(true); + match value { + "true" => input.set_checked(true), + "false" => input.set_checked(false), + _ => fallback(), + } } else { fallback(); } @@ -362,8 +374,8 @@ impl WebsysDom { } } - fn remove_attribute(&mut self, name: &str) { - let node = self.stack.top(); + fn remove_attribute(&mut self, name: &str, root: u64) { + let node = self.nodes[root as usize].as_ref().unwrap(); if let Some(node) = node.dyn_ref::() { node.remove_attribute(name).unwrap(); } @@ -510,17 +522,30 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> SyntheticEvent { FocusEventInner {}, DioxusWebsysEvent(event), ))), - "change" => SyntheticEvent::GenericEvent(DioxusEvent::new((), DioxusWebsysEvent(event))), + // "change" => SyntheticEvent::GenericEvent(DioxusEvent::new((), DioxusWebsysEvent(event))), // 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 - "input" | "invalid" | "reset" | "submit" => { + "change" | "input" | "invalid" | "reset" | "submit" => { let evt: &web_sys::Event = event.dyn_ref().unwrap(); let target: web_sys::EventTarget = evt.target().unwrap(); let value: String = (&target) .dyn_ref() - .map(|input: &web_sys::HtmlInputElement| input.value()) + .map(|input: &web_sys::HtmlInputElement| { + // todo: special case more input types + match input.type_().as_str() { + "checkbox" => { + match input.checked() { + true => "true".to_string(), + false => "false".to_string(), + } + }, + _ => { + input.value() + } + } + }) .or_else(|| { target .dyn_ref()