From 050add21d7b3d950524e3fc3e7e236f4274495eb Mon Sep 17 00:00:00 2001
From: Evan Almloff
Date: Tue, 17 Oct 2023 12:56:12 -0500
Subject: [PATCH 01/17] effect-outside-of-runtime
---
packages/signals/src/effect.rs | 28 +++++++++++++++++++++-------
packages/signals/src/selector.rs | 6 ++++--
packages/signals/src/signal.rs | 7 +++++--
3 files changed, 30 insertions(+), 11 deletions(-)
diff --git a/packages/signals/src/effect.rs b/packages/signals/src/effect.rs
index 9a54188d9..ad950b94b 100644
--- a/packages/signals/src/effect.rs
+++ b/packages/signals/src/effect.rs
@@ -1,16 +1,28 @@
use core::{self, fmt::Debug};
-use std::cell::RefCell;
use std::fmt::{self, Formatter};
-use std::rc::Rc;
//
use dioxus_core::prelude::*;
use crate::use_signal;
use crate::{dependency::Dependency, CopyValue};
-#[derive(Default, Clone)]
+#[derive(Copy, Clone, PartialEq)]
pub(crate) struct EffectStack {
- pub(crate) effects: Rc>>,
+ pub(crate) effects: CopyValue>,
+}
+
+impl Default for EffectStack {
+ fn default() -> Self {
+ Self {
+ effects: CopyValue::new_in_scope(Vec::new(), ScopeId::ROOT),
+ }
+ }
+}
+
+impl EffectStack {
+ pub(crate) fn current(&self) -> Option {
+ self.effects.read().last().copied()
+ }
}
pub(crate) fn get_effect_stack() -> EffectStack {
@@ -57,6 +69,7 @@ pub fn use_effect_with_dependencies(
pub struct Effect {
pub(crate) source: ScopeId,
pub(crate) callback: CopyValue>,
+ pub(crate) effect_stack: EffectStack,
}
impl Debug for Effect {
@@ -67,7 +80,7 @@ impl Debug for Effect {
impl Effect {
pub(crate) fn current() -> Option {
- get_effect_stack().effects.borrow().last().copied()
+ get_effect_stack().effects.read().last().copied()
}
/// Create a new effect. The effect will be run immediately and whenever any signal it reads changes.
@@ -77,6 +90,7 @@ impl Effect {
let myself = Self {
source: current_scope_id().expect("in a virtual dom"),
callback: CopyValue::new(Box::new(callback)),
+ effect_stack: get_effect_stack(),
};
myself.try_run();
@@ -88,11 +102,11 @@ impl Effect {
pub fn try_run(&self) {
if let Some(mut callback) = self.callback.try_write() {
{
- get_effect_stack().effects.borrow_mut().push(*self);
+ self.effect_stack.effects.write().push(*self);
}
callback();
{
- get_effect_stack().effects.borrow_mut().pop();
+ self.effect_stack.effects.write().pop();
}
}
}
diff --git a/packages/signals/src/selector.rs b/packages/signals/src/selector.rs
index 91ab920c8..556c8437c 100644
--- a/packages/signals/src/selector.rs
+++ b/packages/signals/src/selector.rs
@@ -78,19 +78,21 @@ pub fn selector(mut f: impl FnMut() -> R + 'static) -> ReadOnlySig
let effect = Effect {
source: current_scope_id().expect("in a virtual dom"),
callback: CopyValue::invalid(),
+ effect_stack: get_effect_stack(),
};
{
- get_effect_stack().effects.borrow_mut().push(effect);
+ get_effect_stack().effects.write().push(effect);
}
state.inner.value.set(SignalData {
subscribers: Default::default(),
effect_subscribers: Default::default(),
update_any: schedule_update_any().expect("in a virtual dom"),
value: f(),
+ effect_stack: get_effect_stack(),
});
{
- get_effect_stack().effects.borrow_mut().pop();
+ get_effect_stack().effects.write().pop();
}
effect.callback.value.set(Box::new(move || {
diff --git a/packages/signals/src/signal.rs b/packages/signals/src/signal.rs
index c35b04031..9b11572b4 100644
--- a/packages/signals/src/signal.rs
+++ b/packages/signals/src/signal.rs
@@ -11,7 +11,7 @@ use dioxus_core::{
ScopeId, ScopeState,
};
-use crate::{CopyValue, Effect};
+use crate::{get_effect_stack, CopyValue, Effect, EffectStack};
/// Creates a new Signal. Signals are a Copy state management solution with automatic dependency tracking.
///
@@ -82,6 +82,7 @@ pub(crate) struct SignalData {
pub(crate) subscribers: Rc>>,
pub(crate) effect_subscribers: Rc>>,
pub(crate) update_any: Arc,
+ pub(crate) effect_stack: EffectStack,
pub(crate) value: T,
}
@@ -144,6 +145,7 @@ impl Signal {
effect_subscribers: Default::default(),
update_any: schedule_update_any().expect("in a virtual dom"),
value,
+ effect_stack: get_effect_stack(),
}),
}
}
@@ -157,6 +159,7 @@ impl Signal {
effect_subscribers: Default::default(),
update_any: schedule_update_any().expect("in a virtual dom"),
value,
+ effect_stack: get_effect_stack(),
},
owner,
),
@@ -172,7 +175,7 @@ impl Signal {
/// If the signal has been dropped, this will panic.
pub fn read(&self) -> Ref {
let inner = self.inner.read();
- if let Some(effect) = Effect::current() {
+ if let Some(effect) = inner.effect_stack.current() {
let mut effect_subscribers = inner.effect_subscribers.borrow_mut();
if !effect_subscribers.contains(&effect) {
effect_subscribers.push(effect);
From ce86aabf262d53cbf8cbf0a5cbb74630725b8970 Mon Sep 17 00:00:00 2001
From: Evan Almloff
Date: Tue, 17 Oct 2023 13:02:51 -0500
Subject: [PATCH 02/17] fix clippy
---
packages/desktop/src/protocol.rs | 2 +-
packages/signals/src/effect.rs | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs
index 53652f82f..2f660b7a2 100644
--- a/packages/desktop/src/protocol.rs
+++ b/packages/desktop/src/protocol.rs
@@ -153,7 +153,7 @@ fn get_asset_root() -> Option {
/// Get the mime type from a path-like string
fn get_mime_from_path(trimmed: &Path) -> Result<&'static str> {
- if trimmed.ends_with(".svg") {
+ if trimmed.extension().is_some_and(|ext| ext == "svg") {
return Ok("image/svg+xml");
}
diff --git a/packages/signals/src/effect.rs b/packages/signals/src/effect.rs
index ad950b94b..307ee98ab 100644
--- a/packages/signals/src/effect.rs
+++ b/packages/signals/src/effect.rs
@@ -30,7 +30,7 @@ pub(crate) fn get_effect_stack() -> EffectStack {
Some(rt) => rt,
None => {
let store = EffectStack::default();
- provide_root_context(store.clone());
+ provide_root_context(store);
store
}
}
From ad9c59c2b4c49941d512aa126c07d96a9f7a3281 Mon Sep 17 00:00:00 2001
From: Evan Almloff
Date: Thu, 19 Oct 2023 13:23:58 -0500
Subject: [PATCH 03/17] add an readable error when you name a prop key
---
packages/core-macro/src/props/mod.rs | 15 +++++++++------
1 file changed, 9 insertions(+), 6 deletions(-)
diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs
index c1931e497..1fca9baf7 100644
--- a/packages/core-macro/src/props/mod.rs
+++ b/packages/core-macro/src/props/mod.rs
@@ -467,7 +467,7 @@ fn type_from_inside_option(ty: &syn::Type, check_option_name: bool) -> Option<&s
mod struct_info {
use proc_macro2::TokenStream;
- use quote::quote;
+ use quote::{quote, quote_spanned};
use syn::parse::Error;
use syn::punctuated::Punctuated;
use syn::Expr;
@@ -701,6 +701,14 @@ Finally, call `.build()` to create the instance of `{name}`.
}
pub fn field_impl(&self, field: &FieldInfo) -> Result {
+ let FieldInfo {
+ name: field_name,
+ ty: field_type,
+ ..
+ } = field;
+ if field_name.to_string() == "key" {
+ return Err(Error::new_spanned(field_name, "Naming a prop `key` is not allowed because the name can conflict with the built in key attribute. See https://dioxuslabs.com/learn/0.4/reference/dynamic_rendering#rendering-lists for more information about keys"));
+ }
let StructInfo {
ref builder_name, ..
} = *self;
@@ -715,11 +723,6 @@ Finally, call `.build()` to create the instance of `{name}`.
});
let reconstructing = self.included_fields().map(|f| f.name);
- let FieldInfo {
- name: field_name,
- ty: field_type,
- ..
- } = field;
let mut ty_generics: Vec = self
.generics
.params
From 42eb4b5098c3980171e4690d482d2e7a8021a3ef Mon Sep 17 00:00:00 2001
From: Evan Almloff
Date: Thu, 19 Oct 2023 15:48:06 -0500
Subject: [PATCH 04/17] fix clippy
---
packages/core-macro/src/props/mod.rs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs
index 1fca9baf7..605983afc 100644
--- a/packages/core-macro/src/props/mod.rs
+++ b/packages/core-macro/src/props/mod.rs
@@ -467,7 +467,7 @@ fn type_from_inside_option(ty: &syn::Type, check_option_name: bool) -> Option<&s
mod struct_info {
use proc_macro2::TokenStream;
- use quote::{quote, quote_spanned};
+ use quote::quote;
use syn::parse::Error;
use syn::punctuated::Punctuated;
use syn::Expr;
@@ -706,7 +706,7 @@ Finally, call `.build()` to create the instance of `{name}`.
ty: field_type,
..
} = field;
- if field_name.to_string() == "key" {
+ if *field_name == "key" {
return Err(Error::new_spanned(field_name, "Naming a prop `key` is not allowed because the name can conflict with the built in key attribute. See https://dioxuslabs.com/learn/0.4/reference/dynamic_rendering#rendering-lists for more information about keys"));
}
let StructInfo {
From e41278af4539f3a46ac8bd9cd9940ef0b56cfeec Mon Sep 17 00:00:00 2001
From: Leonard
Date: Fri, 20 Oct 2023 23:10:06 +0200
Subject: [PATCH 05/17] Add link to website section on contributing to root
README (#1559)
---
README.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/README.md b/README.md
index 5b6a99ad3..d1ec98019 100644
--- a/README.md
+++ b/README.md
@@ -159,6 +159,7 @@ So... Dioxus is great, but why won't it work for me?
## Contributing
+- Check out the website [section on contributing](https://dioxuslabs.com/learn/0.4/contributing).
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
- Join the discord and ask questions!
From 74952179db25e37ae6f9957bc7109ee276889d31 Mon Sep 17 00:00:00 2001
From: Leonard
Date: Sun, 22 Oct 2023 16:28:17 +0200
Subject: [PATCH 06/17] Append prop documentation to function when using
`#[inline_props]` or `#[component]` (#1563)
* Do the stuff
* Add deprecated warning and attribute list
Also move out methods that didn't take self as a parameter from `InlinePropsDeserializer`
---
.../inline_props.rs | 420 ++++++++++++------
1 file changed, 276 insertions(+), 144 deletions(-)
diff --git a/packages/core-macro/src/component_body_deserializers/inline_props.rs b/packages/core-macro/src/component_body_deserializers/inline_props.rs
index 8e4581c19..a6e0a07d2 100644
--- a/packages/core-macro/src/component_body_deserializers/inline_props.rs
+++ b/packages/core-macro/src/component_body_deserializers/inline_props.rs
@@ -30,166 +30,298 @@ impl ToTokens for InlinePropsDeserializerOutput {
impl DeserializerArgs for InlinePropsDeserializerArgs {
fn to_output(&self, component_body: &ComponentBody) -> Result {
Ok(InlinePropsDeserializerOutput {
- comp_fn: Self::get_function(component_body),
- props_struct: Self::get_props_struct(component_body),
+ comp_fn: get_function(component_body),
+ props_struct: get_props_struct(component_body),
})
}
}
-impl InlinePropsDeserializerArgs {
- fn get_props_struct(component_body: &ComponentBody) -> ItemStruct {
- let ComponentBody { item_fn, .. } = component_body;
- let ItemFn { vis, sig, .. } = item_fn;
- let Signature {
- inputs,
- ident: fn_ident,
- generics,
- ..
- } = sig;
+fn get_props_struct(component_body: &ComponentBody) -> ItemStruct {
+ let ComponentBody { item_fn, .. } = component_body;
+ let ItemFn { vis, sig, .. } = item_fn;
+ let Signature {
+ inputs,
+ ident: fn_ident,
+ generics,
+ ..
+ } = sig;
- // Skip first arg since that's the context
- let struct_fields = inputs.iter().skip(1).map(move |f| {
- match f {
- FnArg::Receiver(_) => unreachable!(), // Unreachable because of ComponentBody parsing
- FnArg::Typed(pt) => {
- let arg_pat = &pt.pat; // Pattern (identifier)
- let arg_colon = &pt.colon_token;
- let arg_ty = &pt.ty; // Type
- let arg_attrs = &pt.attrs; // Attributes
+ // Skip first arg since that's the context
+ let struct_fields = inputs.iter().skip(1).map(move |f| {
+ match f {
+ FnArg::Receiver(_) => unreachable!(), // Unreachable because of ComponentBody parsing
+ FnArg::Typed(pt) => {
+ let arg_pat = &pt.pat; // Pattern (identifier)
+ let arg_colon = &pt.colon_token;
+ let arg_ty = &pt.ty; // Type
+ let arg_attrs = &pt.attrs; // Attributes
- quote! {
- #(#arg_attrs)
- *
- #vis #arg_pat #arg_colon #arg_ty
- }
+ quote! {
+ #(#arg_attrs)
+ *
+ #vis #arg_pat #arg_colon #arg_ty
}
}
- });
-
- let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
-
- let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
- Some(lt)
- } else {
- None
- };
-
- let struct_attrs = if first_lifetime.is_some() {
- quote! { #[derive(Props)] }
- } else {
- quote! { #[derive(Props, PartialEq)] }
- };
-
- let struct_generics = if first_lifetime.is_some() {
- let struct_generics: Punctuated = component_body
- .item_fn
- .sig
- .generics
- .params
- .iter()
- .map(|it| match it {
- GenericParam::Type(tp) => {
- let mut tp = tp.clone();
- tp.bounds.push(parse_quote!( 'a ));
-
- GenericParam::Type(tp)
- }
- _ => it.clone(),
- })
- .collect();
-
- quote! { <#struct_generics> }
- } else {
- quote! { #generics }
- };
-
- parse_quote! {
- #struct_attrs
- #[allow(non_camel_case_types)]
- #vis struct #struct_ident #struct_generics
- {
- #(#struct_fields),*
- }
}
- }
+ });
- fn get_function(component_body: &ComponentBody) -> ItemFn {
- let ComponentBody {
- item_fn,
- cx_pat_type,
- ..
- } = component_body;
- let ItemFn {
- attrs: fn_attrs,
- vis,
- sig,
- block: fn_block,
- } = item_fn;
- let Signature {
- inputs,
- ident: fn_ident,
- generics,
- output: fn_output,
- asyncness,
- ..
- } = sig;
- let Generics { where_clause, .. } = generics;
+ let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
- let cx_pat = &cx_pat_type.pat;
- let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
+ let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
+ Some(lt)
+ } else {
+ None
+ };
- // Skip first arg since that's the context
- let struct_field_names = inputs.iter().skip(1).filter_map(|f| match f {
- FnArg::Receiver(_) => unreachable!(), // ComponentBody prohibits receiver parameters.
- FnArg::Typed(t) => Some(&t.pat),
- });
+ let struct_attrs = if first_lifetime.is_some() {
+ quote! { #[derive(Props)] }
+ } else {
+ quote! { #[derive(Props, PartialEq)] }
+ };
- let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
- Some(lt)
- } else {
- None
- };
+ let struct_generics = if first_lifetime.is_some() {
+ let struct_generics: Punctuated = component_body
+ .item_fn
+ .sig
+ .generics
+ .params
+ .iter()
+ .map(|it| match it {
+ GenericParam::Type(tp) => {
+ let mut tp = tp.clone();
+ tp.bounds.push(parse_quote!( 'a ));
- let (scope_lifetime, fn_generics) = if let Some(lt) = first_lifetime {
- (quote! { #lt, }, generics.clone())
- } else {
- let lifetime: LifetimeParam = parse_quote! { 'a };
+ GenericParam::Type(tp)
+ }
+ _ => it.clone(),
+ })
+ .collect();
- let mut fn_generics = generics.clone();
- fn_generics
- .params
- .insert(0, GenericParam::Lifetime(lifetime.clone()));
+ quote! { <#struct_generics> }
+ } else {
+ quote! { #generics }
+ };
- (quote! { #lifetime, }, fn_generics)
- };
-
- let generics_no_bounds = {
- let mut generics = generics.clone();
- generics.params = generics
- .params
- .iter()
- .map(|it| match it {
- GenericParam::Type(tp) => {
- let mut tp = tp.clone();
- tp.bounds.clear();
-
- GenericParam::Type(tp)
- }
- _ => it.clone(),
- })
- .collect();
-
- generics
- };
-
- parse_quote! {
- #(#fn_attrs)*
- #asyncness #vis fn #fn_ident #fn_generics (#cx_pat: Scope<#scope_lifetime #struct_ident #generics_no_bounds>) #fn_output
- #where_clause
- {
- let #struct_ident { #(#struct_field_names),* } = cx_pat.props;
- #fn_block
- }
+ parse_quote! {
+ #struct_attrs
+ #[allow(non_camel_case_types)]
+ #vis struct #struct_ident #struct_generics
+ {
+ #(#struct_fields),*
}
}
}
+
+fn get_props_docs(fn_ident: &Ident, inputs: Vec<&FnArg>) -> Vec {
+ if inputs.len() <= 1 {
+ return Vec::new();
+ }
+
+ let arg_docs = inputs
+ .iter()
+ .filter_map(|f| match f {
+ FnArg::Receiver(_) => unreachable!(), // ComponentBody prohibits receiver parameters.
+ FnArg::Typed(pt) => {
+ let arg_doc = pt
+ .attrs
+ .iter()
+ .filter_map(|attr| {
+ // TODO: Error reporting
+ // Check if the path of the attribute is "doc"
+ if !is_attr_doc(attr) {
+ return None;
+ };
+
+ let Meta::NameValue(meta_name_value) = &attr.meta else {
+ return None;
+ };
+
+ let Expr::Lit(doc_lit) = &meta_name_value.value else {
+ return None;
+ };
+
+ let Lit::Str(doc_lit_str) = &doc_lit.lit else {
+ return None;
+ };
+
+ Some(doc_lit_str.value())
+ })
+ .fold(String::new(), |mut doc, next_doc_line| {
+ doc.push('\n');
+ doc.push_str(&next_doc_line);
+ doc
+ });
+
+ Some((
+ pt.pat.to_token_stream().to_string(),
+ pt.ty.to_token_stream().to_string(),
+ pt.attrs.iter().filter(|a| !is_attr_doc(a)),
+ arg_doc,
+ ))
+ }
+ })
+ .collect::>();
+
+ let mut props_docs = Vec::with_capacity(5);
+ let props_def_link = fn_ident.to_string() + "Props";
+ let header =
+ format!("# Props\n*For details, see the [props struct definition]({props_def_link}).*");
+
+ props_docs.push(parse_quote! {
+ #[doc = #header]
+ });
+
+ for (arg_name, arg_type, attrs, input_arg_doc) in arg_docs {
+ let input_arg_doc =
+ keep_up_to_two_consecutive_chars(input_arg_doc.trim_matches('\n'), '\n')
+ .replace('\n', "
");
+ let prop_def_link = format!("{props_def_link}::{arg_name}");
+ let mut attribute_list = String::new();
+ let mut is_deprecated = false;
+
+ for attr in attrs {
+ if attr.path() == &parse_quote!(deprecated) {
+ is_deprecated = true;
+ continue;
+ }
+
+ attribute_list.push('`');
+ attribute_list.push_str("e!(#attr).into_token_stream().to_string());
+ attribute_list.push_str("`, ");
+ }
+
+ let mut arg_doc = format!("- [`{arg_name}`]({prop_def_link})");
+
+ if is_deprecated {
+ arg_doc.push_str(" π Deprecated");
+ }
+
+ arg_doc.push_str(&format!("
Type: `{arg_type}`"));
+
+ if !attribute_list.is_empty() {
+ // Truncate the last `, `
+ attribute_list.truncate(attribute_list.len() - 2);
+ arg_doc.push_str(&format!("
Attributes: {attribute_list}"));
+ }
+
+ arg_doc.push_str(&format!("
{input_arg_doc}"));
+
+ props_docs.push(parse_quote! {
+ #[doc = #arg_doc]
+ });
+ }
+
+ props_docs
+}
+
+fn get_function(component_body: &ComponentBody) -> ItemFn {
+ let ComponentBody {
+ item_fn,
+ cx_pat_type,
+ ..
+ } = component_body;
+ let ItemFn {
+ attrs: fn_attrs,
+ vis,
+ sig,
+ block: fn_block,
+ } = item_fn;
+ let Signature {
+ inputs,
+ ident: fn_ident,
+ generics,
+ output: fn_output,
+ asyncness,
+ ..
+ } = sig;
+ let Generics { where_clause, .. } = generics;
+
+ let cx_pat = &cx_pat_type.pat;
+ let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
+
+ // Skip first arg since that's the context
+ let struct_field_names = inputs.iter().skip(1).filter_map(|f| match f {
+ FnArg::Receiver(_) => unreachable!(), // ComponentBody prohibits receiver parameters.
+ FnArg::Typed(pt) => Some(&pt.pat),
+ });
+
+ let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
+ Some(lt)
+ } else {
+ None
+ };
+
+ let (scope_lifetime, fn_generics) = if let Some(lt) = first_lifetime {
+ (quote! { #lt, }, generics.clone())
+ } else {
+ let lifetime: LifetimeParam = parse_quote! { 'a };
+
+ let mut fn_generics = generics.clone();
+ fn_generics
+ .params
+ .insert(0, GenericParam::Lifetime(lifetime.clone()));
+
+ (quote! { #lifetime, }, fn_generics)
+ };
+
+ let generics_no_bounds = {
+ let mut generics = generics.clone();
+ generics.params = generics
+ .params
+ .iter()
+ .map(|it| match it {
+ GenericParam::Type(tp) => {
+ let mut tp = tp.clone();
+ tp.bounds.clear();
+
+ GenericParam::Type(tp)
+ }
+ _ => it.clone(),
+ })
+ .collect();
+
+ generics
+ };
+
+ let props_docs = get_props_docs(fn_ident, inputs.iter().skip(1).collect());
+
+ parse_quote! {
+ #(#fn_attrs)*
+ #(#props_docs)*
+ #asyncness #vis fn #fn_ident #fn_generics (#cx_pat: Scope<#scope_lifetime #struct_ident #generics_no_bounds>) #fn_output
+ #where_clause
+ {
+ let #struct_ident { #(#struct_field_names),* } = cx_pat.props;
+ #fn_block
+ }
+ }
+}
+
+/// Checks if the attribute is a `#[doc]` attribute.
+fn is_attr_doc(attr: &Attribute) -> bool {
+ attr.path() == &parse_quote!(doc)
+}
+
+fn keep_up_to_two_consecutive_chars(input: &str, target_char: char) -> String {
+ let mut output = String::new();
+ let mut prev_char: Option = None;
+ let mut consecutive_count = 0;
+
+ for c in input.chars() {
+ match prev_char {
+ Some(prev) if c == target_char && prev == target_char => {
+ if consecutive_count < 2 {
+ output.push(c);
+ consecutive_count += 1;
+ }
+ }
+ _ => {
+ output.push(c);
+ prev_char = Some(c);
+ consecutive_count = 1;
+ }
+ }
+ }
+
+ output
+}
From cea9563e259e51f45de40d8ac1078024aef29be6 Mon Sep 17 00:00:00 2001
From: Leonard
Date: Mon, 23 Oct 2023 15:41:26 +0200
Subject: [PATCH 07/17] Improve prop docs (#1565)
* Do the stuff
* More sanitizing + fix warnings and format
---
packages/core-macro/Cargo.toml | 1 +
.../inline_props.rs | 80 ++++++-----
packages/core-macro/src/lib.rs | 1 +
packages/core-macro/src/utils.rs | 129 ++++++++++++++++++
4 files changed, 177 insertions(+), 34 deletions(-)
create mode 100644 packages/core-macro/src/utils.rs
diff --git a/packages/core-macro/Cargo.toml b/packages/core-macro/Cargo.toml
index d928d5fec..1898d8bd1 100644
--- a/packages/core-macro/Cargo.toml
+++ b/packages/core-macro/Cargo.toml
@@ -19,6 +19,7 @@ syn = { version = "2.0", features = ["full", "extra-traits"] }
dioxus-rsx = { workspace = true }
dioxus-core = { workspace = true }
constcat = "0.3.0"
+prettyplease = "0.2.15"
# testing
[dev-dependencies]
diff --git a/packages/core-macro/src/component_body_deserializers/inline_props.rs b/packages/core-macro/src/component_body_deserializers/inline_props.rs
index a6e0a07d2..670a14d88 100644
--- a/packages/core-macro/src/component_body_deserializers/inline_props.rs
+++ b/packages/core-macro/src/component_body_deserializers/inline_props.rs
@@ -153,9 +153,20 @@ fn get_props_docs(fn_ident: &Ident, inputs: Vec<&FnArg>) -> Vec {
});
Some((
- pt.pat.to_token_stream().to_string(),
- pt.ty.to_token_stream().to_string(),
- pt.attrs.iter().filter(|a| !is_attr_doc(a)),
+ &pt.pat,
+ &pt.ty,
+ pt.attrs.iter().find_map(|attr| {
+ if attr.path() != &parse_quote!(deprecated) {
+ return None;
+ }
+
+ let res = crate::utils::DeprecatedAttribute::from_meta(&attr.meta);
+
+ match res {
+ Err(e) => panic!("{}", e.to_string()),
+ Ok(v) => Some(v),
+ }
+ }),
arg_doc,
))
}
@@ -171,40 +182,37 @@ fn get_props_docs(fn_ident: &Ident, inputs: Vec<&FnArg>) -> Vec {
#[doc = #header]
});
- for (arg_name, arg_type, attrs, input_arg_doc) in arg_docs {
- let input_arg_doc =
- keep_up_to_two_consecutive_chars(input_arg_doc.trim_matches('\n'), '\n')
- .replace('\n', "
");
- let prop_def_link = format!("{props_def_link}::{arg_name}");
- let mut attribute_list = String::new();
- let mut is_deprecated = false;
+ for (arg_name, arg_type, deprecation, input_arg_doc) in arg_docs {
+ let arg_name = arg_name.into_token_stream().to_string();
+ let arg_type = crate::utils::format_type_string(arg_type);
- for attr in attrs {
- if attr.path() == &parse_quote!(deprecated) {
- is_deprecated = true;
- continue;
+ let input_arg_doc =
+ keep_up_to_n_consecutive_chars(input_arg_doc.trim(), 2, '\n').replace('\n', "
");
+ let prop_def_link = format!("{props_def_link}::{arg_name}");
+ let mut arg_doc = format!("- [`{arg_name}`]({prop_def_link}) : `{arg_type}`");
+
+ if let Some(deprecation) = deprecation {
+ arg_doc.push_str("
π Deprecated");
+
+ if let Some(since) = deprecation.since {
+ arg_doc.push_str(&format!(" since {since}"));
}
- attribute_list.push('`');
- attribute_list.push_str("e!(#attr).into_token_stream().to_string());
- attribute_list.push_str("`, ");
+ if let Some(note) = deprecation.note {
+ let note = keep_up_to_n_consecutive_chars(¬e, 1, '\n').replace('\n', " ");
+ let note = keep_up_to_n_consecutive_chars(¬e, 1, '\t').replace('\t', " ");
+
+ arg_doc.push_str(&format!(": {note}"));
+ }
+
+ if !input_arg_doc.is_empty() {
+ arg_doc.push_str("
");
+ }
+ } else {
+ arg_doc.push_str("
");
}
- let mut arg_doc = format!("- [`{arg_name}`]({prop_def_link})");
-
- if is_deprecated {
- arg_doc.push_str(" π Deprecated");
- }
-
- arg_doc.push_str(&format!("
Type: `{arg_type}`"));
-
- if !attribute_list.is_empty() {
- // Truncate the last `, `
- attribute_list.truncate(attribute_list.len() - 2);
- arg_doc.push_str(&format!("
Attributes: {attribute_list}"));
- }
-
- arg_doc.push_str(&format!("
{input_arg_doc}"));
+ arg_doc.push_str(&input_arg_doc);
props_docs.push(parse_quote! {
#[doc = #arg_doc]
@@ -302,7 +310,11 @@ fn is_attr_doc(attr: &Attribute) -> bool {
attr.path() == &parse_quote!(doc)
}
-fn keep_up_to_two_consecutive_chars(input: &str, target_char: char) -> String {
+fn keep_up_to_n_consecutive_chars(
+ input: &str,
+ n_of_consecutive_chars_allowed: usize,
+ target_char: char,
+) -> String {
let mut output = String::new();
let mut prev_char: Option = None;
let mut consecutive_count = 0;
@@ -310,7 +322,7 @@ fn keep_up_to_two_consecutive_chars(input: &str, target_char: char) -> String {
for c in input.chars() {
match prev_char {
Some(prev) if c == target_char && prev == target_char => {
- if consecutive_count < 2 {
+ if consecutive_count < n_of_consecutive_chars_allowed {
output.push(c);
consecutive_count += 1;
}
diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs
index 477c05d76..4686bc797 100644
--- a/packages/core-macro/src/lib.rs
+++ b/packages/core-macro/src/lib.rs
@@ -12,6 +12,7 @@ use syn::{parse_macro_input, Path, Token};
mod component_body;
mod component_body_deserializers;
mod props;
+mod utils;
// mod rsx;
use crate::component_body::ComponentBody;
diff --git a/packages/core-macro/src/utils.rs b/packages/core-macro/src/utils.rs
new file mode 100644
index 000000000..7e066f773
--- /dev/null
+++ b/packages/core-macro/src/utils.rs
@@ -0,0 +1,129 @@
+use quote::ToTokens;
+use syn::parse::{Parse, ParseStream};
+use syn::spanned::Spanned;
+use syn::{parse_quote, Expr, Lit, Meta, Token, Type};
+
+const FORMATTED_TYPE_START: &str = "static TY_AFTER_HERE:";
+const FORMATTED_TYPE_END: &str = "= todo!();";
+
+/// Attempts to convert the given literal to a string.
+/// Converts ints and floats to their base 10 counterparts.
+///
+/// Returns `None` if the literal is [`Lit::Verbatim`] or if the literal is [`Lit::ByteStr`]
+/// and the byte string could not be converted to UTF-8.
+pub fn lit_to_string(lit: Lit) -> Option {
+ match lit {
+ Lit::Str(l) => Some(l.value()),
+ Lit::ByteStr(l) => String::from_utf8(l.value()).ok(),
+ Lit::Byte(l) => Some(String::from(l.value() as char)),
+ Lit::Char(l) => Some(l.value().to_string()),
+ Lit::Int(l) => Some(l.base10_digits().to_string()),
+ Lit::Float(l) => Some(l.base10_digits().to_string()),
+ Lit::Bool(l) => Some(l.value().to_string()),
+ Lit::Verbatim(_) => None,
+ _ => None,
+ }
+}
+
+pub fn format_type_string(ty: &Type) -> String {
+ let ty_unformatted = ty.into_token_stream().to_string();
+ let ty_unformatted = ty_unformatted.trim();
+
+ // This should always be valid syntax.
+ // Not Rust code, but syntax, which is the only thing that `syn` cares about.
+ let Ok(file_unformatted) = syn::parse_file(&format!(
+ "{FORMATTED_TYPE_START}{ty_unformatted}{FORMATTED_TYPE_END}"
+ )) else {
+ return ty_unformatted.to_string();
+ };
+
+ let file_formatted = prettyplease::unparse(&file_unformatted);
+
+ let file_trimmed = file_formatted.trim();
+ let start_removed = file_trimmed.trim_start_matches(FORMATTED_TYPE_START);
+ let end_removed = start_removed.trim_end_matches(FORMATTED_TYPE_END);
+ let ty_formatted = end_removed.trim();
+
+ ty_formatted.to_string()
+}
+
+/// Represents the `#[deprecated]` attribute.
+///
+/// You can use the [`DeprecatedAttribute::from_meta`] function to try to parse an attribute to this struct.
+#[derive(Default)]
+pub struct DeprecatedAttribute {
+ pub since: Option,
+ pub note: Option,
+}
+
+impl DeprecatedAttribute {
+ /// Returns `None` if the given attribute was not a valid form of the `#[deprecated]` attribute.
+ pub fn from_meta(meta: &Meta) -> syn::Result {
+ if meta.path() != &parse_quote!(deprecated) {
+ return Err(syn::Error::new(
+ meta.span(),
+ "attribute path is not `deprecated`",
+ ));
+ }
+
+ match &meta {
+ Meta::Path(_) => Ok(Self::default()),
+ Meta::NameValue(name_value) => {
+ let Expr::Lit(expr_lit) = &name_value.value else {
+ return Err(syn::Error::new(
+ name_value.span(),
+ "literal in `deprecated` value must be a string",
+ ));
+ };
+
+ Ok(Self {
+ since: None,
+ note: lit_to_string(expr_lit.lit.clone()).map(|s| s.trim().to_string()),
+ })
+ }
+ Meta::List(list) => {
+ let parsed = list.parse_args::()?;
+
+ Ok(Self {
+ since: parsed.since.map(|s| s.trim().to_string()),
+ note: parsed.note.map(|s| s.trim().to_string()),
+ })
+ }
+ }
+ }
+}
+
+mod kw {
+ use syn::custom_keyword;
+ custom_keyword!(since);
+ custom_keyword!(note);
+}
+
+struct DeprecatedAttributeArgsParser {
+ since: Option,
+ note: Option,
+}
+
+impl Parse for DeprecatedAttributeArgsParser {
+ fn parse(input: ParseStream) -> syn::Result {
+ let mut since: Option = None;
+ let mut note: Option = None;
+
+ if input.peek(kw::since) {
+ input.parse::()?;
+ input.parse::()?;
+
+ since = lit_to_string(input.parse()?);
+ }
+
+ if input.peek(Token![,]) && input.peek2(kw::note) {
+ input.parse::()?;
+ input.parse::()?;
+ input.parse::()?;
+
+ note = lit_to_string(input.parse()?);
+ }
+
+ Ok(Self { since, note })
+ }
+}
From d9b32afd64615e8c2a7eb247d20533286c38aaaa Mon Sep 17 00:00:00 2001
From: Evan Almloff
Date: Mon, 23 Oct 2023 12:45:10 -0500
Subject: [PATCH 08/17] add error message when overlapping fullstack features
are enabled
---
packages/fullstack/src/launch.rs | 11 +++++++++--
packages/fullstack/src/lib.rs | 7 +++++++
2 files changed, 16 insertions(+), 2 deletions(-)
diff --git a/packages/fullstack/src/launch.rs b/packages/fullstack/src/launch.rs
index c982a1c90..bd145f363 100644
--- a/packages/fullstack/src/launch.rs
+++ b/packages/fullstack/src/launch.rs
@@ -121,8 +121,15 @@ impl
Date: Tue, 24 Oct 2023 00:10:56 +0200
Subject: [PATCH 09/17] Fix
---
.../component_body_deserializers/inline_props.rs | 14 ++++++++------
1 file changed, 8 insertions(+), 6 deletions(-)
diff --git a/packages/core-macro/src/component_body_deserializers/inline_props.rs b/packages/core-macro/src/component_body_deserializers/inline_props.rs
index 670a14d88..d51132623 100644
--- a/packages/core-macro/src/component_body_deserializers/inline_props.rs
+++ b/packages/core-macro/src/component_body_deserializers/inline_props.rs
@@ -186,13 +186,13 @@ fn get_props_docs(fn_ident: &Ident, inputs: Vec<&FnArg>) -> Vec {
let arg_name = arg_name.into_token_stream().to_string();
let arg_type = crate::utils::format_type_string(arg_type);
- let input_arg_doc =
- keep_up_to_n_consecutive_chars(input_arg_doc.trim(), 2, '\n').replace('\n', "
");
+ let input_arg_doc = keep_up_to_n_consecutive_chars(input_arg_doc.trim(), 2, '\n')
+ .replace("\n\n", "
");
let prop_def_link = format!("{props_def_link}::{arg_name}");
let mut arg_doc = format!("- [`{arg_name}`]({prop_def_link}) : `{arg_type}`");
if let Some(deprecation) = deprecation {
- arg_doc.push_str("
π Deprecated");
+ arg_doc.push_str("
π Deprecated");
if let Some(since) = deprecation.since {
arg_doc.push_str(&format!(" since {since}"));
@@ -205,14 +205,16 @@ fn get_props_docs(fn_ident: &Ident, inputs: Vec<&FnArg>) -> Vec {
arg_doc.push_str(&format!(": {note}"));
}
+ arg_doc.push_str("
");
+
if !input_arg_doc.is_empty() {
arg_doc.push_str("
");
}
- } else {
- arg_doc.push_str("
");
}
- arg_doc.push_str(&input_arg_doc);
+ if !input_arg_doc.is_empty() {
+ arg_doc.push_str(&format!("{input_arg_doc}
"));
+ }
props_docs.push(parse_quote! {
#[doc = #arg_doc]
From 85839abe5668a34c87a2d325ef2dd5a115ec7978 Mon Sep 17 00:00:00 2001
From: Leonard
Date: Tue, 24 Oct 2023 01:35:59 +0200
Subject: [PATCH 10/17] Fix random clippy warning
---
packages/desktop/src/protocol.rs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs
index 53652f82f..3d51e3ac1 100644
--- a/packages/desktop/src/protocol.rs
+++ b/packages/desktop/src/protocol.rs
@@ -153,7 +153,7 @@ fn get_asset_root() -> Option {
/// Get the mime type from a path-like string
fn get_mime_from_path(trimmed: &Path) -> Result<&'static str> {
- if trimmed.ends_with(".svg") {
+ if trimmed.extension().is_some_and(|ext| ext == ".svg") {
return Ok("image/svg+xml");
}
From 613f2f9acaff5295395c7ce001cb41561203a6b6 Mon Sep 17 00:00:00 2001
From: Leonard
Date: Tue, 24 Oct 2023 01:41:46 +0200
Subject: [PATCH 11/17] Revert "Fix random clippy warning"
This reverts commit 85839abe5668a34c87a2d325ef2dd5a115ec7978.
---
packages/desktop/src/protocol.rs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs
index 3d51e3ac1..53652f82f 100644
--- a/packages/desktop/src/protocol.rs
+++ b/packages/desktop/src/protocol.rs
@@ -153,7 +153,7 @@ fn get_asset_root() -> Option {
/// Get the mime type from a path-like string
fn get_mime_from_path(trimmed: &Path) -> Result<&'static str> {
- if trimmed.extension().is_some_and(|ext| ext == ".svg") {
+ if trimmed.ends_with(".svg") {
return Ok("image/svg+xml");
}
From b25fada776c61013217540c0a3cd6f4294563c81 Mon Sep 17 00:00:00 2001
From: Stephen Andary
Date: Wed, 25 Oct 2023 16:36:38 -0400
Subject: [PATCH 12/17] Add Newtype so that Rust-url can be IntoRoutable
(#1579)
* add newtype so that rust-url can IntoRoutable
* add doc line
* implement From directly
---------
Co-authored-by: Evan Almloff
---
packages/router/src/components/link.rs | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/packages/router/src/components/link.rs b/packages/router/src/components/link.rs
index 787f9cdc9..639d99ae7 100644
--- a/packages/router/src/components/link.rs
+++ b/packages/router/src/components/link.rs
@@ -11,6 +11,8 @@ use crate::navigation::NavigationTarget;
use crate::prelude::Routable;
use crate::utils::use_router_internal::use_router_internal;
+use url::Url;
+
/// Something that can be converted into a [`NavigationTarget`].
#[derive(Clone)]
pub enum IntoRoutable {
@@ -53,6 +55,18 @@ impl From<&str> for IntoRoutable {
}
}
+impl From for IntoRoutable {
+ fn from(url: Url) -> Self {
+ IntoRoutable::FromStr(url.to_string())
+ }
+}
+
+impl From<&Url> for IntoRoutable {
+ fn from(url: &Url) -> Self {
+ IntoRoutable::FromStr(url.to_string())
+ }
+}
+
/// The properties for a [`Link`].
#[derive(Props)]
pub struct LinkProps<'a> {
From f5bc1a9856c0287f14526830df6c83d98f1116e8 Mon Sep 17 00:00:00 2001
From: Emil Boman
Date: Thu, 26 Oct 2023 21:19:51 +0200
Subject: [PATCH 13/17] Implement LiveView Router Integration (#1505)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Fix GoForwardButton calling can_go_back instead of can_go_forward
* Add first draft of LiveviewHistory
* Add external URL redirect
* Lock evaluator channel outside loop
* Add liveview to router examples
* fixup! Add liveview to router examples
* Communicate with liveview server on page load
* Add PopState event to Liveview routing
* Call updater callback from liveview history
* Add rudimentary PopState functionality to liveview router.
* Fix linter errors
* Refactor
* Fix navigator external redirection not working.
* Add go back and go forward buttons to router examples
* Finish functionality for timeline stack in liveview router
* Add docs to LiveviewHistory
* Replace Liveview history context attachment with constructor that takes context
* Fix go forward/backward history/future shuffle
* Support history across entire liveview session, if contiguous page jumps.
* Remove unnecessary bounds
* Add query and hash to location string
* Run rustfmt
* fix: Update server function docs link (#1489)
* liveview: Add `interpreter_glue_relative_uri (#1481)
* liveview: Add `interpreter_glue_relative_uri`
By utilizing `window.location.host` in the client-side JavaScript, we can easily derive the WebSocket URI from a relative path URI. This approach obviates the need for host address retrieval on the server side, unlike the method of serving glue code in liveview using `interpreter_glue`.
* liveview: Merge `.._relative_url` functionality
- Merged `.._relative_url` to current API `interpreter_glue`.
- Edit axum example to work with new feature.
* liveview: Fix clippy warning
* Rename modules to use snake_case (#1498)
* Change Scope into &ScopeState
* Move synchronization of state into router and make it opt-in
---------
Co-authored-by: Marc EspΓn
Co-authored-by: Seungwoo Kang
Co-authored-by: Leonard
Co-authored-by: Evan Almloff
---
packages/router/Cargo.toml | 5 +
packages/router/examples/simple_routes.rs | 72 ++-
.../router/src/components/history_buttons.rs | 2 +-
packages/router/src/contexts/router.rs | 2 +-
packages/router/src/history/liveview.rs | 441 ++++++++++++++++++
packages/router/src/history/mod.rs | 5 +
6 files changed, 517 insertions(+), 10 deletions(-)
create mode 100644 packages/router/src/history/liveview.rs
diff --git a/packages/router/Cargo.toml b/packages/router/Cargo.toml
index 80923d539..da9d3fe85 100644
--- a/packages/router/Cargo.toml
+++ b/packages/router/Cargo.toml
@@ -19,6 +19,7 @@ thiserror = { workspace = true }
futures-util = { workspace = true }
urlencoding = "2.1.3"
serde = { version = "1", features = ["derive"], optional = true }
+serde_json = { version = "1.0.91", optional = true }
url = "2.3.1"
wasm-bindgen = { workspace = true, optional = true }
web-sys = { version = "0.3.60", optional = true, features = [
@@ -26,18 +27,22 @@ web-sys = { version = "0.3.60", optional = true, features = [
] }
js-sys = { version = "0.3.63", optional = true }
gloo-utils = { version = "0.1.6", optional = true }
+dioxus-liveview = { workspace = true, optional = true }
dioxus-ssr = { workspace = true, optional = true }
tokio = { workspace = true, features = ["full"], optional = true }
[features]
default = ["web"]
ssr = ["dioxus-ssr", "tokio"]
+liveview = ["dioxus-liveview", "tokio", "dep:serde", "serde_json"]
wasm_test = []
serde = ["dep:serde", "gloo-utils/serde"]
web = ["gloo", "web-sys", "wasm-bindgen", "gloo-utils", "js-sys"]
[dev-dependencies]
+axum = { version = "0.6.1", features = ["ws"] }
dioxus = { path = "../dioxus" }
+dioxus-liveview = { workspace = true, features = ["axum"] }
dioxus-ssr = { path = "../ssr" }
criterion = { version = "0.5", features = ["async_tokio", "html_reports"] }
diff --git a/packages/router/examples/simple_routes.rs b/packages/router/examples/simple_routes.rs
index 3634f7df3..aac1e8658 100644
--- a/packages/router/examples/simple_routes.rs
+++ b/packages/router/examples/simple_routes.rs
@@ -2,6 +2,47 @@ use dioxus::prelude::*;
use dioxus_router::prelude::*;
use std::str::FromStr;
+#[cfg(feature = "liveview")]
+#[tokio::main]
+async fn main() {
+ use axum::{extract::ws::WebSocketUpgrade, response::Html, routing::get, Router};
+
+ let listen_address: std::net::SocketAddr = ([127, 0, 0, 1], 3030).into();
+ let view = dioxus_liveview::LiveViewPool::new();
+ let app = Router::new()
+ .fallback(get(move || async move {
+ Html(format!(
+ r#"
+
+
+
+
+ {glue}
+
+ "#,
+ glue = dioxus_liveview::interpreter_glue(&format!("ws://{listen_address}/ws"))
+ ))
+ }))
+ .route(
+ "/ws",
+ get(move |ws: WebSocketUpgrade| async move {
+ ws.on_upgrade(move |socket| async move {
+ _ = view
+ .launch(dioxus_liveview::axum_socket(socket), Root)
+ .await;
+ })
+ }),
+ );
+
+ println!("Listening on http://{listen_address}");
+
+ axum::Server::bind(&listen_address.to_string().parse().unwrap())
+ .serve(app.into_make_service())
+ .await
+ .unwrap();
+}
+
+#[cfg(not(feature = "liveview"))]
fn main() {
#[cfg(not(target_arch = "wasm32"))]
dioxus_desktop::launch(Root);
@@ -10,21 +51,26 @@ fn main() {
dioxus_web::launch(root);
}
+#[cfg(feature = "liveview")]
#[component]
fn Root(cx: Scope) -> Element {
- render! {
- Router:: {}
- }
+ let history = LiveviewHistory::new(cx);
+ render! { Router:: {
+ config: || RouterConfig::default().history(history),
+ } }
+}
+
+#[cfg(not(feature = "liveview"))]
+#[component]
+fn Root(cx: Scope) -> Element {
+ render! { Router:: {} }
}
#[component]
fn UserFrame(cx: Scope, user_id: usize) -> Element {
render! {
- pre {
- "UserFrame{{\n\tuser_id:{user_id}\n}}"
- }
- div {
- background_color: "rgba(0,0,0,50%)",
+ pre { "UserFrame{{\n\tuser_id:{user_id}\n}}" }
+ div { background_color: "rgba(0,0,0,50%)",
"children:"
Outlet:: {}
}
@@ -88,6 +134,16 @@ fn Route3(cx: Scope, dynamic: String) -> Element {
to: Route::Route2 { user_id: 8888 },
"hello world link"
}
+ button {
+ disabled: !navigator.can_go_back(),
+ onclick: move |_| { navigator.go_back(); },
+ "go back"
+ }
+ button {
+ disabled: !navigator.can_go_forward(),
+ onclick: move |_| { navigator.go_forward(); },
+ "go forward"
+ }
button {
onclick: move |_| { navigator.push("https://www.google.com"); },
"google link"
diff --git a/packages/router/src/components/history_buttons.rs b/packages/router/src/components/history_buttons.rs
index eb62e9285..ae11ad276 100644
--- a/packages/router/src/components/history_buttons.rs
+++ b/packages/router/src/components/history_buttons.rs
@@ -142,7 +142,7 @@ pub fn GoForwardButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
}
};
- let disabled = !router.can_go_back();
+ let disabled = !router.can_go_forward();
render! {
button {
diff --git a/packages/router/src/contexts/router.rs b/packages/router/src/contexts/router.rs
index b15956991..079640e61 100644
--- a/packages/router/src/contexts/router.rs
+++ b/packages/router/src/contexts/router.rs
@@ -232,7 +232,7 @@ impl RouterContext {
IntoRoutable::FromStr(url) => {
let parsed_route: NavigationTarget> = match self.route_from_str(&url) {
Ok(route) => NavigationTarget::Internal(route),
- Err(err) => NavigationTarget::External(err),
+ Err(_) => NavigationTarget::External(url),
};
parsed_route
}
diff --git a/packages/router/src/history/liveview.rs b/packages/router/src/history/liveview.rs
new file mode 100644
index 000000000..9ffb09c34
--- /dev/null
+++ b/packages/router/src/history/liveview.rs
@@ -0,0 +1,441 @@
+use super::HistoryProvider;
+use crate::routable::Routable;
+use dioxus::prelude::*;
+use serde::{Deserialize, Serialize};
+use std::sync::{Mutex, RwLock};
+use std::{collections::BTreeMap, rc::Rc, str::FromStr, sync::Arc};
+
+/// A [`HistoryProvider`] that evaluates history through JS.
+pub struct LiveviewHistory
+where
+ ::Err: std::fmt::Display,
+{
+ action_tx: tokio::sync::mpsc::UnboundedSender>,
+ timeline: Arc>>,
+ updater_callback: Arc>>,
+}
+
+struct Timeline
+where
+ ::Err: std::fmt::Display,
+{
+ current_index: usize,
+ routes: BTreeMap,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+struct State {
+ index: usize,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+struct Session
+where
+ ::Err: std::fmt::Display,
+{
+ #[serde(with = "routes")]
+ routes: BTreeMap,
+ last_visited: usize,
+}
+
+#[derive(Serialize, Deserialize)]
+struct SessionStorage {
+ liveview: Option,
+}
+
+enum Action {
+ GoBack,
+ GoForward,
+ Push(R),
+ Replace(R),
+ External(String),
+}
+
+impl Timeline
+where
+ ::Err: std::fmt::Display,
+{
+ fn new(initial_path: R) -> Self {
+ Self {
+ current_index: 0,
+ routes: BTreeMap::from([(0, initial_path)]),
+ }
+ }
+
+ fn init(
+ &mut self,
+ route: R,
+ state: Option,
+ session: Option>,
+ depth: usize,
+ ) -> State {
+ if let Some(session) = session {
+ self.routes = session.routes;
+ if state.is_none() {
+ // top of stack
+ let last_visited = session.last_visited;
+ self.routes.retain(|&lhs, _| lhs <= last_visited);
+ }
+ };
+ let state = match state {
+ Some(state) => {
+ self.current_index = state.index;
+ state
+ }
+ None => {
+ let index = depth - 1;
+ self.current_index = index;
+ State { index }
+ }
+ };
+ self.routes.insert(state.index, route);
+ state
+ }
+
+ fn update(&mut self, route: R, state: Option) -> State {
+ if let Some(state) = state {
+ self.current_index = state.index;
+ self.routes.insert(self.current_index, route);
+ state
+ } else {
+ self.push(route)
+ }
+ }
+
+ fn push(&mut self, route: R) -> State {
+ // top of stack
+ let index = self.current_index + 1;
+ self.current_index = index;
+ self.routes.insert(index, route);
+ self.routes.retain(|&rhs, _| index >= rhs);
+ State {
+ index: self.current_index,
+ }
+ }
+
+ fn replace(&mut self, route: R) -> State {
+ self.routes.insert(self.current_index, route);
+ State {
+ index: self.current_index,
+ }
+ }
+
+ fn current_route(&self) -> &R {
+ &self.routes[&self.current_index]
+ }
+
+ fn session(&self) -> Session {
+ Session {
+ routes: self.routes.clone(),
+ last_visited: self.current_index,
+ }
+ }
+}
+
+impl LiveviewHistory
+where
+ ::Err: std::fmt::Display,
+{
+ /// Create a [`LiveviewHistory`] in the given scope.
+ /// When using a [`LiveviewHistory`] in combination with use_eval, history must be untampered with.
+ ///
+ /// # Panics
+ ///
+ /// Panics if not in a Liveview context.
+ pub fn new(cx: &ScopeState) -> Self {
+ Self::new_with_initial_path(
+ cx,
+ "/".parse().unwrap_or_else(|err| {
+ panic!("index route does not exist:\n{}\n use LiveviewHistory::new_with_initial_path to set a custom path", err)
+ }),
+ )
+ }
+
+ /// Create a [`LiveviewHistory`] in the given scope, starting at `initial_path`.
+ /// When using a [`LiveviewHistory`] in combination with use_eval, history must be untampered with.
+ ///
+ /// # Panics
+ ///
+ /// Panics if not in a Liveview context.
+ pub fn new_with_initial_path(cx: &ScopeState, initial_path: R) -> Self {
+ let (action_tx, action_rx) = tokio::sync::mpsc::unbounded_channel::>();
+ let action_rx = Arc::new(Mutex::new(action_rx));
+ let timeline = Arc::new(Mutex::new(Timeline::new(initial_path)));
+ let updater_callback: Arc>> =
+ Arc::new(RwLock::new(Arc::new(|| {})));
+
+ let eval_provider = cx
+ .consume_context::>()
+ .expect("evaluator not provided");
+
+ let create_eval = Rc::new(move |script: &str| {
+ eval_provider
+ .new_evaluator(script.to_string())
+ .map(UseEval::new)
+ }) as Rc Result>;
+
+ // Listen to server actions
+ cx.push_future({
+ let timeline = timeline.clone();
+ let action_rx = action_rx.clone();
+ let create_eval = create_eval.clone();
+ async move {
+ let mut action_rx = action_rx.lock().expect("unpoisoned mutex");
+ loop {
+ let eval = action_rx.recv().await.expect("sender to exist");
+ let _ = match eval {
+ Action::GoBack => create_eval(
+ r#"
+ // this triggers a PopState event
+ history.back();
+ "#,
+ ),
+ Action::GoForward => create_eval(
+ r#"
+ // this triggers a PopState event
+ history.forward();
+ "#,
+ ),
+ Action::Push(route) => {
+ let mut timeline = timeline.lock().expect("unpoisoned mutex");
+ let state = timeline.push(route.clone());
+ let state = serde_json::to_string(&state).expect("serializable state");
+ let session = serde_json::to_string(&timeline.session())
+ .expect("serializable session");
+ create_eval(&format!(
+ r#"
+ // this does not trigger a PopState event
+ history.pushState({state}, "", "{route}");
+ sessionStorage.setItem("liveview", '{session}');
+ "#
+ ))
+ }
+ Action::Replace(route) => {
+ let mut timeline = timeline.lock().expect("unpoisoned mutex");
+ let state = timeline.replace(route.clone());
+ let state = serde_json::to_string(&state).expect("serializable state");
+ let session = serde_json::to_string(&timeline.session())
+ .expect("serializable session");
+ create_eval(&format!(
+ r#"
+ // this does not trigger a PopState event
+ history.replaceState({state}, "", "{route}");
+ sessionStorage.setItem("liveview", '{session}');
+ "#
+ ))
+ }
+ Action::External(url) => create_eval(&format!(
+ r#"
+ location.href = "{url}";
+ "#
+ )),
+ };
+ }
+ }
+ });
+
+ // Listen to browser actions
+ cx.push_future({
+ let updater = updater_callback.clone();
+ let timeline = timeline.clone();
+ let create_eval = create_eval.clone();
+ async move {
+ let popstate_eval = {
+ let init_eval = create_eval(
+ r#"
+ return [
+ document.location.pathname + "?" + document.location.search + "\#" + document.location.hash,
+ history.state,
+ JSON.parse(sessionStorage.getItem("liveview")),
+ history.length,
+ ];
+ "#,
+ ).expect("failed to load state").await.expect("serializable state");
+ let (route, state, session, depth) = serde_json::from_value::<(
+ String,
+ Option,
+ Option>,
+ usize,
+ )>(init_eval).expect("serializable state");
+ let Ok(route) = R::from_str(&route.to_string()) else {
+ return;
+ };
+ let mut timeline = timeline.lock().expect("unpoisoned mutex");
+ let state = timeline.init(route.clone(), state, session, depth);
+ let state = serde_json::to_string(&state).expect("serializable state");
+ let session = serde_json::to_string(&timeline.session())
+ .expect("serializable session");
+
+ // Call the updater callback
+ (updater.read().unwrap())();
+
+ create_eval(&format!(r#"
+ // this does not trigger a PopState event
+ history.replaceState({state}, "", "{route}");
+ sessionStorage.setItem("liveview", '{session}');
+
+ window.addEventListener("popstate", (event) => {{
+ dioxus.send([
+ document.location.pathname + "?" + document.location.search + "\#" + document.location.hash,
+ event.state,
+ ]);
+ }});
+ "#)).expect("failed to initialize popstate")
+ };
+
+ loop {
+ let event = match popstate_eval.recv().await {
+ Ok(event) => event,
+ Err(_) => continue,
+ };
+ let (route, state) = serde_json::from_value::<(String, Option)>(event).expect("serializable state");
+ let Ok(route) = R::from_str(&route.to_string()) else {
+ return;
+ };
+ let mut timeline = timeline.lock().expect("unpoisoned mutex");
+ let state = timeline.update(route.clone(), state);
+ let state = serde_json::to_string(&state).expect("serializable state");
+ let session = serde_json::to_string(&timeline.session())
+ .expect("serializable session");
+
+ let _ = create_eval(&format!(
+ r#"
+ // this does not trigger a PopState event
+ history.replaceState({state}, "", "{route}");
+ sessionStorage.setItem("liveview", '{session}');
+ "#));
+
+ // Call the updater callback
+ (updater.read().unwrap())();
+ }
+ }
+ });
+
+ Self {
+ action_tx,
+ timeline,
+ updater_callback,
+ }
+ }
+}
+
+impl HistoryProvider for LiveviewHistory
+where
+ ::Err: std::fmt::Display,
+{
+ fn go_back(&mut self) {
+ let _ = self.action_tx.send(Action::GoBack);
+ }
+
+ fn go_forward(&mut self) {
+ let _ = self.action_tx.send(Action::GoForward);
+ }
+
+ fn push(&mut self, route: R) {
+ let _ = self.action_tx.send(Action::Push(route));
+ }
+
+ fn replace(&mut self, route: R) {
+ let _ = self.action_tx.send(Action::Replace(route));
+ }
+
+ fn external(&mut self, url: String) -> bool {
+ let _ = self.action_tx.send(Action::External(url));
+ true
+ }
+
+ fn current_route(&self) -> R {
+ let timeline = self.timeline.lock().expect("unpoisoned mutex");
+ timeline.current_route().clone()
+ }
+
+ fn can_go_back(&self) -> bool {
+ let timeline = self.timeline.lock().expect("unpoisoned mutex");
+ // Check if the one before is contiguous (i.e., not an external page)
+ let visited_indices: Vec = timeline.routes.keys().cloned().collect();
+ visited_indices
+ .iter()
+ .position(|&rhs| timeline.current_index == rhs)
+ .map_or(false, |index| {
+ index > 0 && visited_indices[index - 1] == timeline.current_index - 1
+ })
+ }
+
+ fn can_go_forward(&self) -> bool {
+ let timeline = self.timeline.lock().expect("unpoisoned mutex");
+ // Check if the one after is contiguous (i.e., not an external page)
+ let visited_indices: Vec = timeline.routes.keys().cloned().collect();
+ visited_indices
+ .iter()
+ .rposition(|&rhs| timeline.current_index == rhs)
+ .map_or(false, |index| {
+ index < visited_indices.len() - 1
+ && visited_indices[index + 1] == timeline.current_index + 1
+ })
+ }
+
+ fn updater(&mut self, callback: Arc) {
+ let mut updater_callback = self.updater_callback.write().unwrap();
+ *updater_callback = callback;
+ }
+}
+
+mod routes {
+ use crate::prelude::Routable;
+ use core::str::FromStr;
+ use serde::de::{MapAccess, Visitor};
+ use serde::{ser::SerializeMap, Deserializer, Serializer};
+ use std::collections::BTreeMap;
+
+ pub fn serialize(routes: &BTreeMap, serializer: S) -> Result
+ where
+ S: Serializer,
+ R: Routable,
+ {
+ let mut map = serializer.serialize_map(Some(routes.len()))?;
+ for (index, route) in routes.iter() {
+ map.serialize_entry(&index.to_string(), &route.to_string())?;
+ }
+ map.end()
+ }
+
+ pub fn deserialize<'de, D, R>(deserializer: D) -> Result, D::Error>
+ where
+ D: Deserializer<'de>,
+ R: Routable,
+ ::Err: std::fmt::Display,
+ {
+ struct BTreeMapVisitor {
+ marker: std::marker::PhantomData,
+ }
+
+ impl<'de, R> Visitor<'de> for BTreeMapVisitor
+ where
+ R: Routable,
+ ::Err: std::fmt::Display,
+ {
+ type Value = BTreeMap;
+
+ fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
+ formatter.write_str("a map with indices and routable values")
+ }
+
+ fn visit_map(self, mut map: M) -> Result
+ where
+ M: MapAccess<'de>,
+ {
+ let mut routes = BTreeMap::new();
+ while let Some((index, route)) = map.next_entry::()? {
+ let index = index.parse::().map_err(serde::de::Error::custom)?;
+ let route = R::from_str(&route).map_err(serde::de::Error::custom)?;
+ routes.insert(index, route);
+ }
+ Ok(routes)
+ }
+ }
+
+ deserializer.deserialize_map(BTreeMapVisitor {
+ marker: std::marker::PhantomData,
+ })
+ }
+}
diff --git a/packages/router/src/history/mod.rs b/packages/router/src/history/mod.rs
index e9668b979..cf7183366 100644
--- a/packages/router/src/history/mod.rs
+++ b/packages/router/src/history/mod.rs
@@ -22,6 +22,11 @@ pub use web::*;
#[cfg(feature = "web")]
pub(crate) mod web_history;
+#[cfg(feature = "liveview")]
+mod liveview;
+#[cfg(feature = "liveview")]
+pub use liveview::*;
+
// #[cfg(feature = "web")]
// mod web_hash;
// #[cfg(feature = "web")]
From d4e6876ff648159c78ad008ade4e3b1b469bb996 Mon Sep 17 00:00:00 2001
From: Falco Hirschenberger
Date: Fri, 27 Oct 2023 15:29:43 +0200
Subject: [PATCH 14/17] Silence desktop build outputs from cargo (#1584)
Silence additional disturbing output from cargo in desktop builds
---
packages/cli/src/builder.rs | 1 +
1 file changed, 1 insertion(+)
diff --git a/packages/cli/src/builder.rs b/packages/cli/src/builder.rs
index 68c494c42..c85cdc484 100644
--- a/packages/cli/src/builder.rs
+++ b/packages/cli/src/builder.rs
@@ -254,6 +254,7 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result
Date: Sun, 29 Oct 2023 21:39:07 +0330
Subject: [PATCH 15/17] Make window invisible until the first render (#1588)
---
packages/desktop/src/lib.rs | 7 +++++++
packages/desktop/src/webview.rs | 2 +-
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs
index badbe3276..bfefaf547 100644
--- a/packages/desktop/src/lib.rs
+++ b/packages/desktop/src/lib.rs
@@ -161,6 +161,7 @@ pub fn launch_with_props(root: Component, props: P, cfg: Config)
// iOS panics if we create a window before the event loop is started
let props = Rc::new(Cell::new(Some(props)));
let cfg = Rc::new(Cell::new(Some(cfg)));
+ let mut is_visible_before_start = true;
event_loop.run(move |window_event, event_loop, control_flow| {
*control_flow = ControlFlow::Wait;
@@ -210,6 +211,8 @@ pub fn launch_with_props(root: Component, props: P, cfg: Config)
// Create a dom
let dom = VirtualDom::new_with_props(root, props);
+ is_visible_before_start = cfg.window.window.visible;
+
let handler = create_new_window(
cfg,
event_loop,
@@ -323,6 +326,10 @@ pub fn launch_with_props(root: Component, props: P, cfg: Config)
EventData::Ipc(msg) if msg.method() == "initialize" => {
let view = webviews.get_mut(&event.1).unwrap();
send_edits(view.dom.rebuild(), &view.desktop_context.webview);
+ view.desktop_context
+ .webview
+ .window()
+ .set_visible(is_visible_before_start);
}
EventData::Ipc(msg) if msg.method() == "browser_open" => {
diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs
index 469aa8068..6e6d26b17 100644
--- a/packages/desktop/src/webview.rs
+++ b/packages/desktop/src/webview.rs
@@ -13,7 +13,7 @@ pub fn build(
proxy: EventLoopProxy,
) -> (WebView, WebContext) {
let builder = cfg.window.clone();
- let window = builder.build(event_loop).unwrap();
+ let window = builder.with_visible(false).build(event_loop).unwrap();
let file_handler = cfg.file_drop_handler.take();
let custom_head = cfg.custom_head.clone();
let index_file = cfg.custom_index.clone();
From 78120ca0e260c9986612b254da7f4afc95edbe97 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 30 Oct 2023 08:44:28 -0500
Subject: [PATCH 16/17] Bump actions/setup-node from 3 to 4 (#1592)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 3 to 4.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v3...v4)
---
updated-dependencies:
- dependency-name: actions/setup-node
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
.github/workflows/playwright.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml
index 500d41564..cb5a1f1bb 100644
--- a/.github/workflows/playwright.yml
+++ b/.github/workflows/playwright.yml
@@ -20,7 +20,7 @@ jobs:
steps:
# Do our best to cache the toolchain and node install steps
- uses: actions/checkout@v4
- - uses: actions/setup-node@v3
+ - uses: actions/setup-node@v4
with:
node-version: 16
- name: Install Rust
From 51f342417a856dc0dfea3701d1488953879f2d73 Mon Sep 17 00:00:00 2001
From: Evan Almloff
Date: Mon, 30 Oct 2023 14:41:41 -0500
Subject: [PATCH 17/17] add examples to the default watch path
---
packages/cli/src/assets/dioxus.toml | 2 +-
packages/cli/src/config.rs | 2 +-
packages/cli/src/server/mod.rs | 14 +++++++-------
3 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/packages/cli/src/assets/dioxus.toml b/packages/cli/src/assets/dioxus.toml
index 6386fb76e..892a6cdf8 100644
--- a/packages/cli/src/assets/dioxus.toml
+++ b/packages/cli/src/assets/dioxus.toml
@@ -23,7 +23,7 @@ title = "Dioxus | An elegant GUI library for Rust"
index_on_404 = true
-watch_path = ["src"]
+watch_path = ["src", "examples"]
# include `assets` in web platform
[web.resource]
diff --git a/packages/cli/src/config.rs b/packages/cli/src/config.rs
index 0654dc375..3abf47f0c 100644
--- a/packages/cli/src/config.rs
+++ b/packages/cli/src/config.rs
@@ -105,7 +105,7 @@ impl Default for DioxusConfig {
},
proxy: Some(vec![]),
watcher: WebWatcherConfig {
- watch_path: Some(vec![PathBuf::from("src")]),
+ watch_path: Some(vec![PathBuf::from("src"), PathBuf::from("examples")]),
reload_html: Some(false),
index_on_404: Some(true),
},
diff --git a/packages/cli/src/server/mod.rs b/packages/cli/src/server/mod.rs
index c6073d787..79a5531b6 100644
--- a/packages/cli/src/server/mod.rs
+++ b/packages/cli/src/server/mod.rs
@@ -32,7 +32,7 @@ async fn setup_file_watcher Result + Send + 'static>(
.watcher
.watch_path
.clone()
- .unwrap_or_else(|| vec![PathBuf::from("src")]);
+ .unwrap_or_else(|| vec![PathBuf::from("src"), PathBuf::from("examples")]);
let watcher_config = config.clone();
let mut watcher = notify::recommended_watcher(move |info: notify::Result| {
@@ -121,12 +121,12 @@ async fn setup_file_watcher Result + Send + 'static>(
.unwrap();
for sub_path in allow_watch_path {
- watcher
- .watch(
- &config.crate_dir.join(sub_path),
- notify::RecursiveMode::Recursive,
- )
- .unwrap();
+ if let Err(err) = watcher.watch(
+ &config.crate_dir.join(sub_path),
+ notify::RecursiveMode::Recursive,
+ ) {
+ log::error!("Failed to watch path: {}", err);
+ }
}
Ok(watcher)
}