diff --git a/docs/guide/src/en/async/use_coroutine.md b/docs/guide/src/en/async/use_coroutine.md index 95c932901..537e27ea4 100644 --- a/docs/guide/src/en/async/use_coroutine.md +++ b/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), } diff --git a/docs/guide/src/en/interactivity/custom_hooks.md b/docs/guide/src/en/interactivity/custom_hooks.md index ebf4f94db..07472bb95 100644 --- a/docs/guide/src/en/interactivity/custom_hooks.md +++ b/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 \ No newline at end of file +- 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 diff --git a/examples/todomvc.rs b/examples/todomvc.rs index b5277133e..4b25507a5 100644 --- a/examples/todomvc.rs +++ b/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::>(); 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 { diff --git a/packages/autofmt/Cargo.toml b/packages/autofmt/Cargo.toml index e409cfb2f..ccb6fed24 100644 --- a/packages/autofmt/Cargo.toml +++ b/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", ] } diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index 113fa7ae1..80561e937 100644 --- a/packages/core/src/create.rs +++ b/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]), diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index fdc1a03f1..56a2d05de 100644 --- a/packages/core/src/diff.rs +++ b/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, + }); + } } } diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index 551069c45..27976c157 100644 --- a/packages/core/src/mutations.rs +++ b/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, }, diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index 3127a7639..7b37b46d4 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -18,6 +18,7 @@ pub struct Config { pub(crate) pre_rendered: Option, pub(crate) disable_context_menu: bool, pub(crate) resource_dir: Option, + pub(crate) data_dir: Option, pub(crate) custom_head: Option, pub(crate) custom_index: Option, 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) -> 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; diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index a8f437614..099a2bd94 100644 --- a/packages/desktop/src/webview.rs +++ b/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) } diff --git a/packages/fermi/README.md b/packages/fermi/README.md index bd7d38ac4..f04ec5961 100644 --- a/packages/fermi/README.md +++ b/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 = |_| 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 diff --git a/packages/html/src/events/keyboard.rs b/packages/html/src/events/keyboard.rs index ee803d948..d00dcf8d2 100644 --- a/packages/html/src/events/keyboard.rs +++ b/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 +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; #[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(deserializer: D) -> Result + 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, } diff --git a/packages/native-core/src/dioxus.rs b/packages/native-core/src/dioxus.rs index d288883b0..3e9ff9ee0 100644 --- a/packages/native-core/src/dioxus.rs +++ b/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(rdom: &mut RealDom) -> 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( + &mut self, + mut node: NodeMut, + 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(&self, rdom: &RealDom, 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( + &mut self, + rdom: &mut RealDom, + 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( + rdom: &mut RealDom, + node: &TemplateNode, +) -> NodeId { match node { TemplateNode::Element { tag, diff --git a/packages/native-core/src/passes.rs b/packages/native-core/src/passes.rs index 70dfc0f08..2a84e6df4 100644 --- a/packages/native-core/src/passes.rs +++ b/packages/native-core/src/passes.rs @@ -16,37 +16,27 @@ use crate::{NodeId, NodeMask}; #[derive(Default)] struct DirtyNodes { - passes_dirty: Vec, + nodes_dirty: FxHashSet, } 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 { - 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 { + 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>>>, @@ -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( 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( } 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( } 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 { diff --git a/packages/native-core/src/tree.rs b/packages/native-core/src/tree.rs index 99ac07d16..df25deac3 100644 --- a/packages/native-core/src/tree.rs +++ b/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 { - self.get(id).unwrap().parent + self.get(id).ok()?.parent } fn children_ids(&self, id: NodeId) -> Vec { @@ -59,7 +59,7 @@ impl<'a> TreeRef for TreeRefView<'a> { } fn height(&self, id: NodeId) -> Option { - Some(self.get(id).unwrap().height) + Some(self.get(id).ok()?.height) } fn contains(&self, id: NodeId) -> bool { diff --git a/packages/router/Cargo.toml b/packages/router/Cargo.toml index d14d9ae19..e1f442ab4 100644 --- a/packages/router/Cargo.toml +++ b/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" diff --git a/packages/rsx-rosetta/Cargo.toml b/packages/rsx-rosetta/Cargo.toml index a0a9763e1..d160d2bf9 100644 --- a/packages/rsx-rosetta/Cargo.toml +++ b/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" diff --git a/packages/rsx/src/hot_reload/hot_reloading_file_map.rs b/packages/rsx/src/hot_reload/hot_reloading_file_map.rs index 2f0cc5246..0393142b4 100644 --- a/packages/rsx/src/hot_reload/hot_reloading_file_map.rs +++ b/packages/rsx/src/hot_reload/hot_reloading_file_map.rs @@ -29,7 +29,7 @@ pub struct FileMapBuildResult { pub struct FileMap { pub map: HashMap>)>, - in_workspace: HashMap, + in_workspace: HashMap>, phantom: std::marker::PhantomData, } @@ -112,11 +112,8 @@ impl FileMap { ) { // 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 FileMap { Ok(UpdateResult::NeedsRebuild) } - fn child_in_workspace(&mut self, crate_dir: &Path) -> io::Result { + fn child_in_workspace(&mut self, crate_dir: &Path) -> io::Result> { 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 FileMap { .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) } } } diff --git a/packages/rsx/src/ifmt.rs b/packages/rsx/src/ifmt.rs index c7a3c2e26..8146cea42 100644 --- a/packages/rsx/src/ifmt.rs +++ b/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 == '}' { diff --git a/packages/ssr/src/cache.rs b/packages/ssr/src/cache.rs index 8556682f1..e261d3b65 100644 --- a/packages/ssr/src/cache.rs +++ b/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 { diff --git a/packages/ssr/src/renderer.rs b/packages/ssr/src/renderer.rs index 5b6fa8fb2..be4025dba 100644 --- a/packages/ssr/src/renderer.rs +++ b/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("
Hello world 1 -->".into(),), Node(0,), PreRendered( diff --git a/packages/ssr/tests/styles.rs b/packages/ssr/tests/styles.rs new file mode 100644 index 000000000..de77e715c --- /dev/null +++ b/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#"
"# + ); +} + +#[test] +fn partially_dynamic_styles() { + let dynamic = 123; + + assert_eq!( + dioxus_ssr::render_lazy(rsx! { + div { width: "100px", height: "{dynamic}px" } + }), + r#"
"# + ); +} + +#[test] +fn dynamic_styles() { + let dynamic = 123; + + assert_eq!( + dioxus_ssr::render_lazy(rsx! { + div { width: "{dynamic}px" } + }), + r#"
"# + ); +} diff --git a/packages/web/src/rehydrate.rs b/packages/web/src/rehydrate.rs index d6afffea1..9af23534b 100644 --- a/packages/web/src/rehydrate.rs +++ b/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, ); } }