Selaa lähdekoodia

Merge branch 'upstream' into make-widgets-cross-framework

Evan Almloff 2 vuotta sitten
vanhempi
commit
8b5f2cb02f

+ 8 - 0
docs/guide/src/en/async/use_coroutine.md

@@ -92,6 +92,8 @@ With Coroutines, we can centralize our async logic. The `rx` parameter is an Cha
 
 
 ```rust
+use futures_util::stream::StreamExt;
+
 enum ProfileUpdate {
     SetUsername(String),
     SetAge(i32)
@@ -117,6 +119,10 @@ cx.render(rsx!{
 })
 ```
 
+
+> Note: In order to use/run the `rx.next().await` statement you will need to extend the [`Stream`] trait (used by [`UnboundedReceiver`]) by adding 'futures_util' as a dependency to your project and adding the `use futures_util::stream::StreamExt;`.
+
+
 For sufficiently complex apps, we could build a bunch of different useful "services" that loop on channels to update the app.
 
 ```rust
@@ -164,6 +170,8 @@ fn Banner(cx: Scope) -> Element {
 Now, in our sync service, we can structure our state however we want. We only need to update the view values when ready.
 
 ```rust
+use futures_util::stream::StreamExt;
+
 enum SyncAction {
     SetUsername(String),
 }

+ 2 - 2
docs/guide/src/en/interactivity/custom_hooks.md

@@ -22,5 +22,5 @@ You can use [`cx.use_hook`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.
 
 Inside the initialization closure, you will typically make calls to other `cx` methods. For example:
 
-- The `use_state` hook tracks state in the hook value, and uses [`cx.schedule_update`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.Scope.html#method.schedule_update) to make Dioxus re-render the component whenever it changes.
-- The `use_context` hook calls [`cx.consume_context`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.Scope.html#method.consume_context) (which would be expensive to call on every render) to get some context from the scope
+- The `use_state` hook tracks state in the hook value, and uses [`cx.schedule_update`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.ScopeState.html#method.schedule_update) to make Dioxus re-render the component whenever it changes.
+- The `use_context` hook calls [`cx.consume_context`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.ScopeState.html#method.consume_context) (which would be expensive to call on every render) to get some context from the scope

+ 86 - 39
examples/todomvc.rs

@@ -7,7 +7,7 @@ fn main() {
     dioxus_desktop::launch(app);
 }
 
-#[derive(PartialEq, Eq)]
+#[derive(PartialEq, Eq, Clone, Copy)]
 pub enum FilterState {
     All,
     Active,
@@ -39,56 +39,99 @@ pub fn app(cx: Scope<()>) -> Element {
         .collect::<Vec<_>>();
     filtered_todos.sort_unstable();
 
-    let show_clear_completed = todos.values().any(|todo| todo.checked);
-    let items_left = filtered_todos.len();
-    let item_text = match items_left {
+    let active_todo_count = todos.values().filter(|item| !item.checked).count();
+    let active_todo_text = match active_todo_count {
         1 => "item",
         _ => "items",
     };
 
-    cx.render(rsx!{
+    let show_clear_completed = todos.values().any(|todo| todo.checked);
+
+    let selected = |state| {
+        if *filter == state {
+            "selected"
+        } else {
+            "false"
+        }
+    };
+
+    cx.render(rsx! {
         section { class: "todoapp",
             style { include_str!("./assets/todomvc.css") }
-            div {
-                header { class: "header",
-                    h1 {"todos"}
-                    input {
-                        class: "new-todo",
-                        placeholder: "What needs to be done?",
-                        value: "{draft}",
-                        autofocus: "true",
-                        oninput: move |evt| {
-                            draft.set(evt.value.clone());
-                        },
-                        onkeydown: move |evt| {
-                            if evt.key() == Key::Enter && !draft.is_empty() {
-                                todos.make_mut().insert(
-                                    **todo_id,
-                                    TodoItem {
-                                        id: **todo_id,
-                                        checked: false,
-                                        contents: draft.to_string(),
-                                    },
-                                );
-                                *todo_id.make_mut() += 1;
-                                draft.set("".to_string());
-                            }
+            header { class: "header",
+                h1 {"todos"}
+                input {
+                    class: "new-todo",
+                    placeholder: "What needs to be done?",
+                    value: "{draft}",
+                    autofocus: "true",
+                    oninput: move |evt| {
+                        draft.set(evt.value.clone());
+                    },
+                    onkeydown: move |evt| {
+                        if evt.key() == Key::Enter && !draft.is_empty() {
+                            todos.make_mut().insert(
+                                **todo_id,
+                                TodoItem {
+                                    id: **todo_id,
+                                    checked: false,
+                                    contents: draft.to_string(),
+                                },
+                            );
+                            *todo_id.make_mut() += 1;
+                            draft.set("".to_string());
+                        }
+                    }
+                }
+            }
+            section {
+                class: "main",
+                if !todos.is_empty() {
+                    rsx! {
+                        input {
+                            id: "toggle-all",
+                            class: "toggle-all",
+                            r#type: "checkbox",
+                            onchange: move |_| {
+                                let check = active_todo_count != 0;
+                                for (_, item) in todos.make_mut().iter_mut() {
+                                    item.checked = check;
+                                }
+                            },
+                            checked: if active_todo_count == 0 { "true" } else { "false" },
                         }
+                        label { r#for: "toggle-all" }
                     }
                 }
                 ul { class: "todo-list",
-                    filtered_todos.iter().map(|id| rsx!(TodoEntry { key: "{id}", id: *id, todos: todos }))
+                    filtered_todos.iter().map(|id| rsx!(TodoEntry {
+                        key: "{id}",
+                        id: *id,
+                        todos: todos,
+                    }))
                 }
                 (!todos.is_empty()).then(|| rsx!(
                     footer { class: "footer",
                         span { class: "todo-count",
-                            strong {"{items_left} "}
-                            span {"{item_text} left"}
+                            strong {"{active_todo_count} "}
+                            span {"{active_todo_text} left"}
                         }
                         ul { class: "filters",
-                            li { class: "All", a { onclick: move |_| filter.set(FilterState::All), "All" }}
-                            li { class: "Active", a { onclick: move |_| filter.set(FilterState::Active), "Active" }}
-                            li { class: "Completed", a { onclick: move |_| filter.set(FilterState::Completed), "Completed" }}
+                            for (state, state_text, url) in [
+                                (FilterState::All, "All", "#/"),
+                                (FilterState::Active, "Active", "#/active"),
+                                (FilterState::Completed, "Completed", "#/completed"),
+                            ] {
+                                li {
+                                    a {
+                                        href: url,
+                                        class: selected(state),
+                                        onclick: move |_| filter.set(state),
+                                        prevent_default: "onclick",
+                                        state_text
+                                    }
+                                }
+                            }
                         }
                         show_clear_completed.then(|| rsx!(
                             button {
@@ -102,8 +145,8 @@ pub fn app(cx: Scope<()>) -> Element {
             }
         }
         footer { class: "info",
-            p {"Double-click to edit a todo"}
-            p { "Created by ", a {  href: "http://github.com/jkelleyrtp/", "jkelleyrtp" }}
+            p { "Double-click to edit a todo" }
+            p { "Created by ", a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" }}
             p { "Part of ", a { href: "http://todomvc.com", "TodoMVC" }}
         }
     })
@@ -136,13 +179,17 @@ pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element {
                         cx.props.todos.make_mut()[&cx.props.id].checked = evt.value.parse().unwrap();
                     }
                 }
-
                 label {
                     r#for: "cbg-{todo.id}",
-                    onclick: move |_| is_editing.set(true),
+                    ondblclick: move |_| is_editing.set(true),
                     prevent_default: "onclick",
                     "{todo.contents}"
                 }
+                button {
+                    class: "destroy",
+                    onclick: move |_| { cx.props.todos.make_mut().remove(&todo.id); },
+                    prevent_default: "onclick",
+                }
             }
             is_editing.then(|| rsx!{
                 input {

+ 8 - 2
packages/autofmt/Cargo.toml

@@ -2,7 +2,13 @@
 name = "dioxus-autofmt"
 version = "0.3.0"
 edition = "2021"
-
+authors = ["Jonathan Kelley"]
+description = "Autofomatter for Dioxus RSX"
+license = "MIT/Apache-2.0"
+repository = "https://github.com/DioxusLabs/dioxus/"
+homepage = "https://dioxuslabs.com"
+documentation = "https://dioxuslabs.com"
+keywords = ["dom", "ui", "gui", "react"]
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
@@ -11,7 +17,7 @@ proc-macro2 = { version = "1.0.6", features = ["span-locations"] }
 quote = "1.0"
 syn = { version = "1.0.11", features = ["full", "extra-traits"] }
 serde = { version = "1.0.136", features = ["derive"] }
-prettyplease = { git = "https://github.com/DioxusLabs/prettyplease-macro-fmt.git", features = [
+prettyplease = { package = "prettier-please", version = "0.1.16",  features = [
     "verbatim",
 ] }
 

+ 1 - 0
packages/core/src/create.rs

@@ -35,6 +35,7 @@ fn sort_bfs(paths: &[&'static [u8]]) -> Vec<(usize, &'static [u8])> {
 }
 
 #[test]
+#[cfg(debug_assertions)]
 fn sorting() {
     let r: [(usize, &[u8]); 5] = [
         (0, &[0, 1]),

+ 25 - 12
packages/core/src/diff.rs

@@ -148,6 +148,13 @@ impl<'b> VirtualDom {
 
         // Make sure the roots get transferred over while we're here
         right_template.root_ids.transfer(&left_template.root_ids);
+
+        // Update the node refs
+        for i in 0..right_template.root_ids.len() {
+            if let Some(root_id) = right_template.root_ids.get(i) {
+                self.update_template(root_id, right_template);
+            }
+        }
     }
 
     fn diff_dynamic_node(
@@ -628,10 +635,12 @@ impl<'b> VirtualDom {
             }
 
             let id = self.find_last_element(&new[last]);
-            self.mutations.push(Mutation::InsertAfter {
-                id,
-                m: nodes_created,
-            });
+            if nodes_created > 0 {
+                self.mutations.push(Mutation::InsertAfter {
+                    id,
+                    m: nodes_created,
+                })
+            }
             nodes_created = 0;
         }
 
@@ -652,10 +661,12 @@ impl<'b> VirtualDom {
                 }
 
                 let id = self.find_first_element(&new[last]);
-                self.mutations.push(Mutation::InsertBefore {
-                    id,
-                    m: nodes_created,
-                });
+                if nodes_created > 0 {
+                    self.mutations.push(Mutation::InsertBefore {
+                        id,
+                        m: nodes_created,
+                    });
+                }
 
                 nodes_created = 0;
             }
@@ -676,10 +687,12 @@ impl<'b> VirtualDom {
             }
 
             let id = self.find_first_element(&new[first_lis]);
-            self.mutations.push(Mutation::InsertBefore {
-                id,
-                m: nodes_created,
-            });
+            if nodes_created > 0 {
+                self.mutations.push(Mutation::InsertBefore {
+                    id,
+                    m: nodes_created,
+                });
+            }
         }
     }
 

+ 5 - 5
packages/core/src/mutations.rs

@@ -68,7 +68,7 @@ pub enum Mutation<'a> {
         /// The ID of the element being mounted to
         id: ElementId,
 
-        /// The number of nodes on the stack
+        /// The number of nodes on the stack to append to the target element
         m: usize,
     },
 
@@ -155,7 +155,7 @@ pub enum Mutation<'a> {
         /// The ID of the node we're going to replace with
         id: ElementId,
 
-        /// The number of nodes on the stack to use to replace
+        /// The number of nodes on the stack to replace the target element with
         m: usize,
     },
 
@@ -167,7 +167,7 @@ pub enum Mutation<'a> {
         /// `[0,1,2]` represents 1st child's 2nd child's 3rd child.
         path: &'static [u8],
 
-        /// The number of nodes on the stack to use to replace
+        /// The number of nodes on the stack to replace the target element with
         m: usize,
     },
 
@@ -176,7 +176,7 @@ pub enum Mutation<'a> {
         /// The ID of the node to insert after.
         id: ElementId,
 
-        /// The ids of the nodes to insert after the target node.
+        /// The number of nodes on the stack to insert after the target node.
         m: usize,
     },
 
@@ -185,7 +185,7 @@ pub enum Mutation<'a> {
         /// The ID of the node to insert before.
         id: ElementId,
 
-        /// The ids of the nodes to insert before the target node.
+        /// The number of nodes on the stack to insert before the target node.
         m: usize,
     },
 

+ 10 - 0
packages/desktop/src/cfg.rs

@@ -18,6 +18,7 @@ pub struct Config {
     pub(crate) pre_rendered: Option<String>,
     pub(crate) disable_context_menu: bool,
     pub(crate) resource_dir: Option<PathBuf>,
+    pub(crate) data_dir: Option<PathBuf>,
     pub(crate) custom_head: Option<String>,
     pub(crate) custom_index: Option<String>,
     pub(crate) root_name: String,
@@ -44,6 +45,7 @@ impl Config {
             pre_rendered: None,
             disable_context_menu: !cfg!(debug_assertions),
             resource_dir: None,
+            data_dir: None,
             custom_head: None,
             custom_index: None,
             root_name: "main".to_string(),
@@ -56,6 +58,14 @@ impl Config {
         self
     }
 
+    /// set the directory where data will be stored in release mode.
+    ///
+    /// > Note: This **must** be set when bundling on Windows.
+    pub fn with_data_directory(mut self, path: impl Into<PathBuf>) -> Self {
+        self.data_dir = Some(path.into());
+        self
+    }
+
     /// Set whether or not the right-click context menu should be disabled.
     pub fn with_disable_context_menu(mut self, disable: bool) -> Self {
         self.disable_context_menu = disable;

+ 4 - 0
packages/desktop/src/webview.rs

@@ -54,6 +54,10 @@ pub fn build(
                 .unwrap_or_default()
         });
 
+    // These are commented out because wry is currently broken in wry
+    // let mut web_context = WebContext::new(cfg.data_dir.clone());
+    // .with_web_context(&mut web_context);
+
     for (name, handler) in cfg.protocols.drain(..) {
         webview = webview.with_custom_protocol(name, handler)
     }

+ 20 - 0
packages/fermi/README.md

@@ -63,6 +63,26 @@ fn NameCard(cx: Scope) -> Element {
 }
 ```
 
+If needed, we can update the atom's value, based on itself:
+
+```rust, ignore
+static COUNT: Atom<i32> = |_| 0;
+
+fn Counter(cx: Scope) -> Element {
+    let mut count = use_atom_state(cx, COUNT);
+
+    cx.render(rsx!{
+        p {
+          "{count}"
+        }
+        button {
+            onclick: move |_| count += 1,
+            "Increment counter"
+        }
+    })
+}
+```
+
 It's that simple!
 
 ## Installation

+ 33 - 6
packages/html/src/events/keyboard.rs

@@ -5,6 +5,16 @@ use std::convert::TryInto;
 use std::fmt::{Debug, Formatter};
 use std::str::FromStr;
 
+#[cfg(feature = "serialize")]
+fn resilient_deserialize_code<'de, D>(deserializer: D) -> Result<Code, D::Error>
+where
+    D: serde::Deserializer<'de>,
+{
+    use serde::Deserialize;
+    // If we fail to deserialize the code for any reason, just return Unidentified instead of failing.
+    Ok(Code::deserialize(deserializer).unwrap_or(Code::Unidentified))
+}
+
 pub type KeyboardEvent = Event<KeyboardData>;
 #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
 #[derive(Clone, PartialEq, Eq)]
@@ -27,6 +37,10 @@ pub struct KeyboardData {
     pub key_code: KeyCode,
 
     /// the physical key on the keyboard
+    #[cfg_attr(
+        feature = "serialize",
+        serde(deserialize_with = "resilient_deserialize_code")
+    )]
     code: Code,
 
     /// Indicate if the `alt` modifier key was pressed during this keyboard event
@@ -102,7 +116,7 @@ impl KeyboardData {
     /// The value of the key pressed by the user, taking into consideration the state of modifier keys such as Shift as well as the keyboard locale and layout.
     pub fn key(&self) -> Key {
         #[allow(deprecated)]
-        FromStr::from_str(&self.key).expect("could not parse")
+        FromStr::from_str(&self.key).unwrap_or(Key::Unidentified)
     }
 
     /// A physical key on the keyboard (as opposed to the character generated by pressing the key). In other words, this property returns a value that isn't altered by keyboard layout or the state of the modifier keys.
@@ -158,10 +172,24 @@ impl Debug for KeyboardData {
     }
 }
 
-#[cfg_attr(
-    feature = "serialize",
-    derive(serde_repr::Serialize_repr, serde_repr::Deserialize_repr)
-)]
+#[cfg(feature = "serialize")]
+impl<'de> serde::Deserialize<'de> for KeyCode {
+    fn deserialize<D>(deserializer: D) -> Result<KeyCode, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        // We could be deserializing a unicode character, so we need to use u64 even if the output only takes u8
+        let value = u64::deserialize(deserializer)?;
+
+        if let Ok(smaller_uint) = value.try_into() {
+            Ok(KeyCode::from_raw_code(smaller_uint))
+        } else {
+            Ok(KeyCode::Unknown)
+        }
+    }
+}
+
+#[cfg_attr(feature = "serialize", derive(serde_repr::Serialize_repr))]
 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
 #[repr(u8)]
 pub enum KeyCode {
@@ -525,7 +553,6 @@ pub enum KeyCode {
     // kanji, = 244
     // unlock trackpad (Chrome/Edge), = 251
     // toggle touchpad, = 255
-    #[cfg_attr(feature = "serialize", serde(other))]
     Unknown,
 }
 

+ 16 - 5
packages/native-core/src/dioxus.rs

@@ -27,7 +27,7 @@ pub struct DioxusState {
 
 impl DioxusState {
     /// Initialize the DioxusState in the RealDom
-    pub fn create(rdom: &mut RealDom) -> Self {
+    pub fn create<V: FromAnyValue + Send + Sync>(rdom: &mut RealDom<V>) -> Self {
         let root_id = rdom.root_id();
         let mut root = rdom.get_mut(root_id).unwrap();
         root.insert(ElementIdComponent(ElementId(0)));
@@ -48,7 +48,11 @@ impl DioxusState {
         self.node_id_mapping.get(element_id.0).copied().flatten()
     }
 
-    fn set_element_id(&mut self, mut node: NodeMut, element_id: ElementId) {
+    fn set_element_id<V: FromAnyValue + Send + Sync>(
+        &mut self,
+        mut node: NodeMut<V>,
+        element_id: ElementId,
+    ) {
         let node_id = node.id();
         node.insert(ElementIdComponent(element_id));
         if self.node_id_mapping.len() <= element_id.0 {
@@ -57,7 +61,7 @@ impl DioxusState {
         self.node_id_mapping[element_id.0] = Some(node_id);
     }
 
-    fn load_child(&self, rdom: &RealDom, path: &[u8]) -> NodeId {
+    fn load_child<V: FromAnyValue + Send + Sync>(&self, rdom: &RealDom<V>, path: &[u8]) -> NodeId {
         let mut current = rdom.get(*self.stack.last().unwrap()).unwrap();
         for i in path {
             let new_id = current.child_ids()[*i as usize];
@@ -67,7 +71,11 @@ impl DioxusState {
     }
 
     /// Updates the dom with some mutations and return a set of nodes that were updated. Pass the dirty nodes to update_state.
-    pub fn apply_mutations(&mut self, rdom: &mut RealDom, mutations: Mutations) {
+    pub fn apply_mutations<V: FromAnyValue + Send + Sync>(
+        &mut self,
+        rdom: &mut RealDom<V>,
+        mutations: Mutations,
+    ) {
         for template in mutations.templates {
             let mut template_root_ids = Vec::new();
             for root in template.roots {
@@ -222,7 +230,10 @@ impl DioxusState {
     }
 }
 
-fn create_template_node(rdom: &mut RealDom, node: &TemplateNode) -> NodeId {
+fn create_template_node<V: FromAnyValue + Send + Sync>(
+    rdom: &mut RealDom<V>,
+    node: &TemplateNode,
+) -> NodeId {
     match node {
         TemplateNode::Element {
             tag,

+ 11 - 24
packages/native-core/src/passes.rs

@@ -16,37 +16,27 @@ use crate::{NodeId, NodeMask};
 
 #[derive(Default)]
 struct DirtyNodes {
-    passes_dirty: Vec<u64>,
+    nodes_dirty: FxHashSet<NodeId>,
 }
 
 impl DirtyNodes {
     pub fn add_node(&mut self, node_id: NodeId) {
-        let node_id = node_id.uindex();
-        let index = node_id / 64;
-        let bit = node_id % 64;
-        let encoded = 1 << bit;
-        if let Some(passes) = self.passes_dirty.get_mut(index) {
-            *passes |= encoded;
-        } else {
-            self.passes_dirty.resize(index + 1, 0);
-            self.passes_dirty[index] |= encoded;
-        }
+        self.nodes_dirty.insert(node_id);
     }
 
     pub fn is_empty(&self) -> bool {
-        self.passes_dirty.iter().all(|dirty| *dirty == 0)
+        self.nodes_dirty.is_empty()
     }
 
-    pub fn pop(&mut self) -> Option<usize> {
-        let index = self.passes_dirty.iter().position(|dirty| *dirty != 0)?;
-        let passes = self.passes_dirty[index];
-        let node_id = passes.trailing_zeros();
-        let encoded = 1 << node_id;
-        self.passes_dirty[index] &= !encoded;
-        Some((index * 64) + node_id as usize)
+    pub fn pop(&mut self) -> Option<NodeId> {
+        self.nodes_dirty.iter().next().copied().map(|id| {
+            self.nodes_dirty.remove(&id);
+            id
+        })
     }
 }
 
+/// Tracks the dirty nodes sorted by height for each pass. We resolve passes based on the height of the node in order to avoid resolving any node twice in a pass.
 #[derive(Clone, Unique)]
 pub struct DirtyNodeStates {
     dirty: Arc<FxHashMap<TypeId, RwLock<BTreeMap<u16, DirtyNodes>>>>,
@@ -76,7 +66,7 @@ impl DirtyNodeStates {
         }
     }
 
-    fn pop_front(&self, pass_id: TypeId) -> Option<(u16, usize)> {
+    fn pop_front(&self, pass_id: TypeId) -> Option<(u16, NodeId)> {
         let mut values = self.dirty.get(&pass_id)?.write();
         let mut value = values.first_entry()?;
         let height = *value.key();
@@ -89,7 +79,7 @@ impl DirtyNodeStates {
         Some((height, id))
     }
 
-    fn pop_back(&self, pass_id: TypeId) -> Option<(u16, usize)> {
+    fn pop_back(&self, pass_id: TypeId) -> Option<(u16, NodeId)> {
         let mut values = self.dirty.get(&pass_id)?.write();
         let mut value = values.last_entry()?;
         let height = *value.key();
@@ -214,7 +204,6 @@ pub fn run_pass<V: FromAnyValue + Send + Sync>(
     match pass_direction {
         PassDirection::ParentToChild => {
             while let Some((height, id)) = dirty.pop_front(type_id) {
-                let id = tree.id_at(id).unwrap();
                 if (update_node)(id, ctx) {
                     nodes_updated.insert(id);
                     for id in tree.children_ids(id) {
@@ -227,7 +216,6 @@ pub fn run_pass<V: FromAnyValue + Send + Sync>(
         }
         PassDirection::ChildToParent => {
             while let Some((height, id)) = dirty.pop_back(type_id) {
-                let id = tree.id_at(id).unwrap();
                 if (update_node)(id, ctx) {
                     nodes_updated.insert(id);
                     if let Some(id) = tree.parent_id(id) {
@@ -240,7 +228,6 @@ pub fn run_pass<V: FromAnyValue + Send + Sync>(
         }
         PassDirection::AnyOrder => {
             while let Some((height, id)) = dirty.pop_back(type_id) {
-                let id = tree.id_at(id).unwrap();
                 if (update_node)(id, ctx) {
                     nodes_updated.insert(id);
                     for dependant in &dependants {

+ 2 - 2
packages/native-core/src/tree.rs

@@ -49,7 +49,7 @@ pub trait TreeMut: TreeRef {
 
 impl<'a> TreeRef for TreeRefView<'a> {
     fn parent_id(&self, id: NodeId) -> Option<NodeId> {
-        self.get(id).unwrap().parent
+        self.get(id).ok()?.parent
     }
 
     fn children_ids(&self, id: NodeId) -> Vec<NodeId> {
@@ -59,7 +59,7 @@ impl<'a> TreeRef for TreeRefView<'a> {
     }
 
     fn height(&self, id: NodeId) -> Option<u16> {
-        Some(self.get(id).unwrap().height)
+        Some(self.get(id).ok()?.height)
     }
 
     fn contains(&self, id: NodeId) -> bool {

+ 0 - 2
packages/router/Cargo.toml

@@ -36,7 +36,6 @@ thiserror = "1.0.30"
 futures-util = "0.3.21"
 serde = { version = "1", optional = true }
 serde_urlencoded = { version = "0.7.1", optional = true }
-simple_logger = "4.0.0"
 
 [features]
 default = ["query"]
@@ -46,7 +45,6 @@ wasm_test = []
 
 [dev-dependencies]
 console_error_panic_hook = "0.1.7"
-log = "0.4.14"
 wasm-logger = "0.2.0"
 wasm-bindgen-test = "0.3"
 gloo-utils = "0.1.2"

+ 10 - 3
packages/rsx-rosetta/Cargo.toml

@@ -1,12 +1,19 @@
 [package]
 name = "rsx-rosetta"
-version = "0.0.0"
-edition = "2018"
+version = "0.3.0"
+edition = "2021"
+authors = ["Jonathan Kelley"]
+description = "Autofomatter for Dioxus RSX"
+license = "MIT/Apache-2.0"
+repository = "https://github.com/DioxusLabs/dioxus/"
+homepage = "https://dioxuslabs.com"
+documentation = "https://dioxuslabs.com"
+keywords = ["dom", "ui", "gui", "react"]
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
-dioxus-autofmt = { path = "../autofmt" }
+dioxus-autofmt = { path = "../autofmt", version = "0.3.0" }
 dioxus-rsx = { path = "../rsx" , version = "^0.0.3" }
 html_parser = "0.6.3"
 proc-macro2 = "1.0.49"

+ 8 - 10
packages/rsx/src/hot_reload/hot_reloading_file_map.rs

@@ -29,7 +29,7 @@ pub struct FileMapBuildResult<Ctx: HotReloadingContext> {
 
 pub struct FileMap<Ctx: HotReloadingContext> {
     pub map: HashMap<PathBuf, (String, Option<Template<'static>>)>,
-    in_workspace: HashMap<PathBuf, bool>,
+    in_workspace: HashMap<PathBuf, Option<PathBuf>>,
     phantom: std::marker::PhantomData<Ctx>,
 }
 
@@ -112,11 +112,8 @@ impl<Ctx: HotReloadingContext> FileMap<Ctx> {
                                 ) {
                                     // if the file!() macro is invoked in a workspace, the path is relative to the workspace root, otherwise it's relative to the crate root
                                     // we need to check if the file is in a workspace or not and strip the prefix accordingly
-                                    let prefix = if in_workspace {
-                                        crate_dir.parent().ok_or(io::Error::new(
-                                            io::ErrorKind::Other,
-                                            "Could not load workspace",
-                                        ))?
+                                    let prefix = if let Some(workspace) = &in_workspace {
+                                        workspace
                                     } else {
                                         crate_dir
                                     };
@@ -173,9 +170,9 @@ impl<Ctx: HotReloadingContext> FileMap<Ctx> {
         Ok(UpdateResult::NeedsRebuild)
     }
 
-    fn child_in_workspace(&mut self, crate_dir: &Path) -> io::Result<bool> {
+    fn child_in_workspace(&mut self, crate_dir: &Path) -> io::Result<Option<PathBuf>> {
         if let Some(in_workspace) = self.in_workspace.get(crate_dir) {
-            Ok(*in_workspace)
+            Ok(in_workspace.clone())
         } else {
             let mut cmd = Cmd::new();
             let manafest_path = crate_dir.join("Cargo.toml");
@@ -186,9 +183,10 @@ impl<Ctx: HotReloadingContext> FileMap<Ctx> {
                 .map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
 
             let in_workspace = metadata.workspace_root != crate_dir;
+            let workspace_path = in_workspace.then(|| metadata.workspace_root.into());
             self.in_workspace
-                .insert(crate_dir.to_path_buf(), in_workspace);
-            Ok(in_workspace)
+                .insert(crate_dir.to_path_buf(), workspace_path.clone());
+            Ok(workspace_path)
         }
     }
 }

+ 5 - 0
packages/rsx/src/ifmt.rs

@@ -59,6 +59,11 @@ impl FromStr for IfmtInput {
                 let mut current_captured = String::new();
                 while let Some(c) = chars.next() {
                     if c == ':' {
+                        // two :s in a row is a path, not a format arg
+                        if chars.next_if(|c| *c == ':').is_some() {
+                            current_captured.push_str("::");
+                            continue;
+                        }
                         let mut current_format_args = String::new();
                         for c in chars.by_ref() {
                             if c == '}' {

+ 38 - 3
packages/ssr/src/cache.rs

@@ -17,6 +17,12 @@ pub enum Segment {
     Attr(usize),
     Node(usize),
     PreRendered(String),
+    /// A marker for where to insert a dynamic styles
+    StyleMarker {
+        // If the marker is inside a style tag or not
+        // This will be true if there are static styles
+        inside_style_tag: bool,
+    },
 }
 
 impl std::fmt::Write for StringChain {
@@ -61,16 +67,45 @@ impl StringCache {
             } => {
                 cur_path.push(root_idx);
                 write!(chain, "<{tag}")?;
+                // we need to collect the styles and write them at the end
+                let mut styles = Vec::new();
+                let mut has_dynamic_attrs = false;
                 for attr in *attrs {
                     match attr {
-                        TemplateAttribute::Static { name, value, .. } => {
-                            write!(chain, " {name}=\"{value}\"")?;
+                        TemplateAttribute::Static {
+                            name,
+                            value,
+                            namespace,
+                        } => {
+                            if let Some("style") = namespace {
+                                styles.push((name, value));
+                            } else {
+                                write!(chain, " {name}=\"{value}\"")?;
+                            }
                         }
                         TemplateAttribute::Dynamic { id: index } => {
-                            chain.segments.push(Segment::Attr(*index))
+                            chain.segments.push(Segment::Attr(*index));
+                            has_dynamic_attrs = true;
                         }
                     }
                 }
+
+                // write the styles
+                if !styles.is_empty() {
+                    write!(chain, " style=\"")?;
+                    for (name, value) in styles {
+                        write!(chain, "{name}:{value};")?;
+                    }
+                    chain.segments.push(Segment::StyleMarker {
+                        inside_style_tag: true,
+                    });
+                    write!(chain, "\"")?;
+                } else if has_dynamic_attrs {
+                    chain.segments.push(Segment::StyleMarker {
+                        inside_style_tag: false,
+                    });
+                }
+
                 if children.is_empty() && tag_is_self_closing(tag) {
                     write!(chain, "/>")?;
                 } else {

+ 45 - 5
packages/ssr/src/renderer.rs

@@ -70,15 +70,24 @@ impl Renderer {
             .or_insert_with(|| Rc::new(StringCache::from_template(template).unwrap()))
             .clone();
 
+        // We need to keep track of the dynamic styles so we can insert them into the right place
+        let mut accumulated_dynamic_styles = Vec::new();
+
         for segment in entry.segments.iter() {
             match segment {
                 Segment::Attr(idx) => {
                     let attr = &template.dynamic_attrs[*idx];
-                    match attr.value {
-                        AttributeValue::Text(value) => write!(buf, " {}=\"{}\"", attr.name, value)?,
-                        AttributeValue::Bool(value) => write!(buf, " {}={}", attr.name, value)?,
-                        _ => {}
-                    };
+                    if attr.namespace == Some("style") {
+                        accumulated_dynamic_styles.push(attr);
+                    } else {
+                        match attr.value {
+                            AttributeValue::Text(value) => {
+                                write!(buf, " {}=\"{}\"", attr.name, value)?
+                            }
+                            AttributeValue::Bool(value) => write!(buf, " {}={}", attr.name, value)?,
+                            _ => {}
+                        };
+                    }
                 }
                 Segment::Node(idx) => match &template.dynamic_nodes[*idx] {
                     DynamicNode::Component(node) => {
@@ -128,6 +137,34 @@ impl Renderer {
                 },
 
                 Segment::PreRendered(contents) => write!(buf, "{contents}")?,
+
+                Segment::StyleMarker { inside_style_tag } => {
+                    if !accumulated_dynamic_styles.is_empty() {
+                        // if we are inside a style tag, we don't need to write the style attribute
+                        if !*inside_style_tag {
+                            write!(buf, " style=\"")?;
+                        }
+                        for attr in &accumulated_dynamic_styles {
+                            match attr.value {
+                                AttributeValue::Text(value) => {
+                                    write!(buf, "{}:{};", attr.name, value)?
+                                }
+                                AttributeValue::Bool(value) => {
+                                    write!(buf, "{}:{};", attr.name, value)?
+                                }
+                                AttributeValue::Float(f) => write!(buf, "{}:{};", attr.name, f)?,
+                                AttributeValue::Int(i) => write!(buf, "{}:{};", attr.name, i)?,
+                                _ => {}
+                            };
+                        }
+                        if !*inside_style_tag {
+                            write!(buf, "\"")?;
+                        }
+
+                        // clear the accumulated styles
+                        accumulated_dynamic_styles.clear();
+                    }
+                }
             }
         }
 
@@ -168,6 +205,9 @@ fn to_string_works() {
                 vec![
                     PreRendered("<div class=\"asdasdasd\" class=\"asdasdasd\"".into(),),
                     Attr(0,),
+                    StyleMarker {
+                        inside_style_tag: false,
+                    },
                     PreRendered(">Hello world 1 --&gt;".into(),),
                     Node(0,),
                     PreRendered(

+ 40 - 0
packages/ssr/tests/styles.rs

@@ -0,0 +1,40 @@
+use dioxus::prelude::*;
+
+#[test]
+fn static_styles() {
+    fn app(cx: Scope) -> Element {
+        render! { div { width: "100px" } }
+    }
+
+    let mut dom = VirtualDom::new(app);
+    _ = dom.rebuild();
+
+    assert_eq!(
+        dioxus_ssr::render(&dom),
+        r#"<div style="width:100px;"></div>"#
+    );
+}
+
+#[test]
+fn partially_dynamic_styles() {
+    let dynamic = 123;
+
+    assert_eq!(
+        dioxus_ssr::render_lazy(rsx! {
+            div { width: "100px", height: "{dynamic}px" }
+        }),
+        r#"<div style="width:100px;height:123px;"></div>"#
+    );
+}
+
+#[test]
+fn dynamic_styles() {
+    let dynamic = 123;
+
+    assert_eq!(
+        dioxus_ssr::render_lazy(rsx! {
+            div { width: "{dynamic}px" }
+        }),
+        r#"<div style="width:123px;"></div>"#
+    );
+}

+ 3 - 2
packages/web/src/rehydrate.rs

@@ -128,10 +128,11 @@ impl WebsysDom {
                         mounted_id = Some(id);
                         let name = attribute.name;
                         if let AttributeValue::Listener(_) = value {
+                            let event_name = &name[2..];
                             self.interpreter.new_event_listener(
-                                &name[2..],
+                                event_name,
                                 id.0 as u32,
-                                event_bubbles(name) as u8,
+                                event_bubbles(event_name) as u8,
                             );
                         }
                     }