From f0c9dadf7718a3258793d2e868bf198ee9700471 Mon Sep 17 00:00:00 2001 From: marcerhans <121002093+marcerhans@users.noreply.github.com> Date: Wed, 15 Mar 2023 15:08:29 +0100 Subject: [PATCH 01/22] Added mention of StreamExt trait extension. (#886) --- docs/guide/src/en/async/use_coroutine.md | 8 ++++++++ 1 file changed, 8 insertions(+) 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), } From 46cc07e04827f8ea63bd210eaf6b72727441b6bf Mon Sep 17 00:00:00 2001 From: marcerhans <121002093+marcerhans@users.noreply.github.com> Date: Fri, 17 Mar 2023 13:15:18 +0100 Subject: [PATCH 02/22] Updated links in docs for custom use_hook. (#891) Found some more dead links. --- docs/guide/src/en/interactivity/custom_hooks.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From f0bcb3c2c602907e6d7a8e6e90efeff364876dd0 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sun, 19 Mar 2023 07:14:35 -0500 Subject: [PATCH 03/22] make dioxus intigration generic over custom value type --- packages/native-core/src/dioxus.rs | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) 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, From b19e7ce4550f714ce0c073a6390b87657f018002 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 22 Mar 2023 09:17:27 -0500 Subject: [PATCH 04/22] fix non-bubbling listener hydration --- packages/web/src/rehydrate.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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, ); } } From 468f684d7d8496fd20066f9839ff2a84c8289849 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 22 Mar 2023 10:10:18 -0500 Subject: [PATCH 05/22] fix style attributes in SSR --- packages/ssr/src/cache.rs | 41 ++++++++++++++++++++++++++--- packages/ssr/src/renderer.rs | 50 ++++++++++++++++++++++++++++++++---- packages/ssr/tests/styles.rs | 40 +++++++++++++++++++++++++++++ 3 files changed, 123 insertions(+), 8 deletions(-) create mode 100644 packages/ssr/tests/styles.rs 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#"
"# + ); +} From c8880fac36ac76774f4752f217452655b18ff949 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 22 Mar 2023 13:53:20 -0500 Subject: [PATCH 06/22] expose data directory in desktop config --- packages/desktop/src/cfg.rs | 10 ++++++++++ packages/desktop/src/webview.rs | 7 +++++-- 2 files changed, 15 insertions(+), 2 deletions(-) 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..c5d8b4b42 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -7,7 +7,7 @@ use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget}; pub use wry; pub use wry::application as tao; use wry::application::window::Window; -use wry::webview::{WebView, WebViewBuilder}; +use wry::webview::{WebContext, WebView, WebViewBuilder}; pub fn build( cfg: &mut Config, @@ -33,6 +33,8 @@ pub fn build( )); } + let mut web_context = WebContext::new(cfg.data_dir.clone()); + let mut webview = WebViewBuilder::new(window) .unwrap() .with_transparent(cfg.window.window.transparent) @@ -52,7 +54,8 @@ pub fn build( .as_ref() .map(|handler| handler(window, evet)) .unwrap_or_default() - }); + }) + .with_web_context(&mut web_context); for (name, handler) in cfg.protocols.drain(..) { webview = webview.with_custom_protocol(name, handler) From e0563bd63717d662556cc3d152c5ee799bf16cc0 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 23 Mar 2023 13:15:47 -0500 Subject: [PATCH 07/22] allow paths in format expressions --- packages/rsx/src/ifmt.rs | 5 +++++ 1 file changed, 5 insertions(+) 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 == '}' { From d658f501415aefb9061a95aab653b24e9a603e86 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 24 Mar 2023 09:34:40 -0700 Subject: [PATCH 08/22] publish autofmt --- packages/autofmt/Cargo.toml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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", ] } From b06228be7047b02b39562f9b96a1ce08cb8a60b3 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 24 Mar 2023 09:39:52 -0700 Subject: [PATCH 09/22] publish rosetta --- packages/rsx-rosetta/Cargo.toml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) 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" From 6bc9bc6ef95d054e92c8accca4d52e2554c39a0f Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 24 Mar 2023 12:57:19 -0500 Subject: [PATCH 10/22] fix nodes not being marked as dirty with multiple mutations --- packages/native-core/src/node_ref.rs | 2 +- packages/native-core/src/real_dom.rs | 22 +++++++++++++++------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/native-core/src/node_ref.rs b/packages/native-core/src/node_ref.rs index 22ab5d8f1..218348c90 100644 --- a/packages/native-core/src/node_ref.rs +++ b/packages/native-core/src/node_ref.rs @@ -126,7 +126,7 @@ impl AttributeMask { pub fn union(&self, other: &Self) -> Self { match (self, other) { (AttributeMask::Some(s), AttributeMask::Some(o)) => { - AttributeMask::Some(s.intersection(o).cloned().collect()) + AttributeMask::Some(s.union(o).cloned().collect()) } _ => AttributeMask::All, } diff --git a/packages/native-core/src/real_dom.rs b/packages/native-core/src/real_dom.rs index 1deec7d64..25d1ed9fe 100644 --- a/packages/native-core/src/real_dom.rs +++ b/packages/native-core/src/real_dom.rs @@ -186,6 +186,8 @@ impl RealDom { .entry(id) .or_default() .extend(self.dirty_nodes.passes.iter().map(|x| x.this_type_id)); + self.dirty_nodes + .mark_dirty(id, NodeMaskBuilder::ALL.build()); let watchers = self.node_watchers.clone(); for watcher in &*watchers.read().unwrap() { watcher.on_node_added(NodeMut::new(id, self)); @@ -216,16 +218,19 @@ impl RealDom { self.root_id } + /// Check if a node exists in the dom. + pub fn contains(&self, id: NodeId) -> bool { + self.tree_ref().contains(id) + } + /// Get a reference to a node. pub fn get(&self, id: NodeId) -> Option> { - self.tree_ref() - .contains(id) - .then_some(NodeRef { id, dom: self }) + self.contains(id).then_some(NodeRef { id, dom: self }) } /// Get a mutable reference to a node. pub fn get_mut(&mut self, id: NodeId) -> Option> { - let contains = self.tree_ref().contains(id); + let contains = self.contains(id); contains.then(|| NodeMut::new(id, self)) } @@ -399,7 +404,7 @@ impl<'a, V: Component + Send + Sync> DerefMut for ViewEntr } /// A immutable view of a node -pub trait NodeImmutable: Sized { +pub trait NodeImmutable: Sized { /// Get the real dom this node was created in fn real_dom(&self) -> &RealDom; @@ -573,7 +578,9 @@ impl<'a, V: FromAnyValue + Send + Sync> NodeMut<'a, V> { .or_default() .insert(TypeId::of::()); let view_mut: ViewMut = self.dom.borrow_raw().ok()?; - Some(ViewEntryMut::new(view_mut, self.id)) + view_mut + .contains(self.id) + .then_some(ViewEntryMut::new(view_mut, self.id)) } /// Insert a custom component into this node @@ -684,7 +691,8 @@ impl<'a, V: FromAnyValue + Send + Sync> NodeMut<'a, V> { for child in children_ids_vec { self.dom.get_mut(child).unwrap().remove(); } - self.dom.tree_mut().remove_single(id); + self.dom.tree_mut().remove(id); + self.real_dom_mut().raw_world_mut().delete_entity(id); } /// Replace this node with a different node From e4cb8c3aa0a4fc2a5915e7003a3e6bb7a05b58dd Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 24 Mar 2023 13:41:00 -0500 Subject: [PATCH 11/22] fix Dirty Nodes for large NodeIds --- packages/native-core/src/passes.rs | 31 +++++++----------------------- packages/native-core/src/tree.rs | 4 ++-- 2 files changed, 9 insertions(+), 26 deletions(-) diff --git a/packages/native-core/src/passes.rs b/packages/native-core/src/passes.rs index 70dfc0f08..a98f81600 100644 --- a/packages/native-core/src/passes.rs +++ b/packages/native-core/src/passes.rs @@ -16,34 +16,20 @@ use crate::{NodeId, NodeMask}; #[derive(Default)] struct DirtyNodes { - passes_dirty: Vec, + nodes_dirty: Vec, } 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.push(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.pop() } } @@ -76,7 +62,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 +75,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 +200,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 +212,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 +224,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 { From ec8b77ab3200fef11df718956ec21e81c7c187e4 Mon Sep 17 00:00:00 2001 From: Arniu Tseng Date: Sat, 25 Mar 2023 18:20:01 +0800 Subject: [PATCH 12/22] Remove unused simple-logger --- packages/router/Cargo.toml | 2 -- 1 file changed, 2 deletions(-) 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" From a9b5c92fbb608121f93db5c3458c6843efb464fb Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 28 Mar 2023 08:35:57 -0500 Subject: [PATCH 13/22] avoid duplicating dirty nodes in native core --- packages/native-core/src/passes.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/native-core/src/passes.rs b/packages/native-core/src/passes.rs index a98f81600..2a84e6df4 100644 --- a/packages/native-core/src/passes.rs +++ b/packages/native-core/src/passes.rs @@ -16,12 +16,12 @@ use crate::{NodeId, NodeMask}; #[derive(Default)] struct DirtyNodes { - nodes_dirty: Vec, + nodes_dirty: FxHashSet, } impl DirtyNodes { pub fn add_node(&mut self, node_id: NodeId) { - self.nodes_dirty.push(node_id); + self.nodes_dirty.insert(node_id); } pub fn is_empty(&self) -> bool { @@ -29,10 +29,14 @@ impl DirtyNodes { } pub fn pop(&mut self) -> Option { - self.nodes_dirty.pop() + 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>>>, From 204ac8b2ed33f92566c55b234e0dc86350f6c4ca Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 28 Mar 2023 11:58:50 -0500 Subject: [PATCH 14/22] clarify mutations docs --- packages/core/src/mutations.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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, }, From 3bb6c4bc300b4358f105c2d0d909dac793e2cad3 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 28 Mar 2023 13:34:37 -0500 Subject: [PATCH 15/22] don't add no op mutations --- packages/core/src/diff.rs | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index fdc1a03f1..6f84b21aa 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -628,10 +628,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 +654,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 +680,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, + }); + } } } From f9b33f12cbd5cb6509d12b43b32b54167bcb3a17 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 28 Mar 2023 15:39:52 -0500 Subject: [PATCH 16/22] fix more complex workspaces with hot reloading --- .../src/hot_reload/hot_reloading_file_map.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) 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) } } } From 1cc4260f70c4ed0388f2e8f15ff3bd22d4159328 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sat, 1 Apr 2023 20:15:10 -0500 Subject: [PATCH 17/22] more resilient keyboard event deserialization --- packages/html/src/events/keyboard.rs | 39 +++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 6 deletions(-) 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, } From 3aa7349db76127774dd4bcc0ec0b896391b13d8b Mon Sep 17 00:00:00 2001 From: Ivan Tham Date: Mon, 3 Apr 2023 09:03:33 +0800 Subject: [PATCH 18/22] Add more features to todomvc (#928) - toggle-all button - double click modify todo item - filter state show mouse pointer - individual todo item remove button - correct active item count --- examples/todomvc.rs | 125 ++++++++++++++++++++++++++++++-------------- 1 file changed, 86 insertions(+), 39 deletions(-) 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 { From c2dd963d86ef745a260d311978866f8f0bdcbfcb Mon Sep 17 00:00:00 2001 From: Antonio Curavalea <112291302+arqalite@users.noreply.github.com> Date: Tue, 4 Apr 2023 05:12:52 +0300 Subject: [PATCH 19/22] doc: fermi: add counter example to readme (#931) * doc: add counter example to readme * doc: fermi: revert use_read --- packages/fermi/README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) 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 From 297c9a822e22921577d29f099f7acd73498baa70 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 5 Apr 2023 12:34:17 -0500 Subject: [PATCH 20/22] always update root id node refs --- packages/core/src/create.rs | 1 + packages/core/src/diff.rs | 7 +++++++ 2 files changed, 8 insertions(+) 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 6f84b21aa..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( From dc0df321b1caee0061d4867a20d56466395309a2 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 5 Apr 2023 13:00:27 -0700 Subject: [PATCH 21/22] Fix wry segfaulting by commenting out webcontext --- packages/desktop/src/webview.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index c5d8b4b42..a8e1d4995 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -33,8 +33,6 @@ pub fn build( )); } - let mut web_context = WebContext::new(cfg.data_dir.clone()); - let mut webview = WebViewBuilder::new(window) .unwrap() .with_transparent(cfg.window.window.transparent) @@ -54,8 +52,11 @@ pub fn build( .as_ref() .map(|handler| handler(window, evet)) .unwrap_or_default() - }) - .with_web_context(&mut web_context); + }); + + // 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) From 44c1e2e033344d970d5a14bcd17e742c3ae99871 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 5 Apr 2023 13:06:25 -0700 Subject: [PATCH 22/22] clippy --- packages/desktop/src/webview.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index a8e1d4995..099a2bd94 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -7,7 +7,7 @@ use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget}; pub use wry; pub use wry::application as tao; use wry::application::window::Window; -use wry::webview::{WebContext, WebView, WebViewBuilder}; +use wry::webview::{WebView, WebViewBuilder}; pub fn build( cfg: &mut Config,