Ver Fonte

feat: shared state mechanisms

Jonathan Kelley há 3 anos atrás
pai
commit
4a4c7af

+ 21 - 53
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::<SharedState>();
+    ///     let state = cx.consume_state::<SharedState>();
     ///     rsx!(cx, div { "hello {state.0}" })
     /// }
     /// ```
-    pub fn use_provide_state<T, F>(self, init: F) -> &'src Rc<T>
+    pub fn provide_state<T>(self, value: T) -> Option<Rc<T>>
     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::<T>(), 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::<T>(), Rc::new(value))
+            .map(|f| f.downcast::<T>().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<T: 'static>(self) -> Option<&'src Rc<T>> {
-        struct UseContextHook<C>(Option<Rc<C>>);
-        self.use_hook(
-            move |_| {
-                let getter = &self.scope.shared.get_shared_context;
-                let ty = TypeId::of::<T>();
-                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<T: 'static>(self) -> Option<Rc<T>> {
+        let getter = &self.scope.shared.get_shared_context;
+        let ty = TypeId::of::<T>();
+        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<u32> {
+    pub fn create_subtree(self) -> Option<u32> {
         self.scope.new_subtree()
     }
 

+ 16 - 33
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<DomEdit>| {
-            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();
-            }
         }
     }
 

+ 12 - 7
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,
     },
 }

+ 4 - 7
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
+        },]
     );
 }
 

+ 2 - 2
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::<MySharedState>()?;
+        let shared = cx.consume_state::<MySharedState>()?;
         rsx!(cx, "Hello, {shared.0}")
     };
 

+ 1 - 1
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::<RefCell<DesktopContext>>()?;
+    let dtcx = cx.consume_state::<RefCell<DesktopContext>>()?;
 
     cx.use_hook(
         |_| {

+ 3 - 0
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::*;

+ 176 - 0
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<T> = RefCell<ProvidedStateInner<T>>;
+
+// Tracks all the subscribers to a shared State
+pub(crate) struct ProvidedStateInner<T> {
+    value: Rc<RefCell<T>>,
+    notify_any: Rc<dyn Fn(ScopeId)>,
+    consumers: HashSet<ScopeId>,
+}
+
+impl<T> ProvidedStateInner<T> {
+    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<UseSharedState<'a, T>> {
+    cx.use_hook(
+        |_| {
+            let scope_id = cx.scope_id();
+            let root = cx.consume_state::<ProvidedState<T>>();
+
+            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<T: 'static> {
+    root: Option<Rc<ProvidedState<T>>>,
+    value: Option<Rc<RefCell<T>>>,
+    scope_id: ScopeId,
+    needs_notification: Cell<bool>,
+}
+
+pub struct UseSharedState<'a, T: 'static> {
+    pub(crate) cx: Context<'a>,
+    pub(crate) value: &'a Rc<RefCell<T>>,
+    pub(crate) root: &'a Rc<RefCell<ProvidedStateInner<T>>>,
+    pub(crate) needs_notification: &'a Cell<bool>,
+}
+
+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<T> 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(())),
+        |_| {},
+    )
+}

+ 1 - 1
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,
 };
 

+ 2 - 0
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",
 

+ 1 - 1
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();

+ 40 - 15
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::<Element>() {
                 let el = el.dyn_ref::<HtmlElement>().unwrap();
@@ -345,7 +353,11 @@ impl WebsysDom {
                 }
                 "checked" => {
                     if let Some(input) = node.dyn_ref::<HtmlInputElement>() {
-                        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::<web_sys::Element>() {
             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()