mirror of
https://github.com/DioxusLabs/dioxus
synced 2025-02-17 06:08:26 +00:00
Merge branch 'master' into binary-protocal
This commit is contained in:
commit
856fb1874e
25 changed files with 1027 additions and 184 deletions
2
.github/workflows/playwright.yml
vendored
2
.github/workflows/playwright.yml
vendored
|
@ -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
|
||||
|
|
|
@ -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!
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -254,6 +254,7 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResul
|
|||
let mut cmd = subprocess::Exec::cmd("cargo")
|
||||
.cwd(&config.crate_dir)
|
||||
.arg("build")
|
||||
.arg("--quiet")
|
||||
.arg("--message-format=json");
|
||||
|
||||
if config.release {
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
|
|
|
@ -32,7 +32,7 @@ async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + 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<notify::Event>| {
|
||||
|
@ -121,12 +121,12 @@ async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + 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)
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -30,166 +30,312 @@ impl ToTokens for InlinePropsDeserializerOutput {
|
|||
impl DeserializerArgs<InlinePropsDeserializerOutput> for InlinePropsDeserializerArgs {
|
||||
fn to_output(&self, component_body: &ComponentBody) -> Result<InlinePropsDeserializerOutput> {
|
||||
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<GenericParam, Comma> = 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<GenericParam, Comma> = 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<Attribute> {
|
||||
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,
|
||||
&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,
|
||||
))
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
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, 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);
|
||||
|
||||
let input_arg_doc = keep_up_to_n_consecutive_chars(input_arg_doc.trim(), 2, '\n')
|
||||
.replace("\n\n", "</p><p>");
|
||||
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("<p>👎 Deprecated");
|
||||
|
||||
if let Some(since) = deprecation.since {
|
||||
arg_doc.push_str(&format!(" since {since}"));
|
||||
}
|
||||
|
||||
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}"));
|
||||
}
|
||||
|
||||
arg_doc.push_str("</p>");
|
||||
|
||||
if !input_arg_doc.is_empty() {
|
||||
arg_doc.push_str("<hr/>");
|
||||
}
|
||||
}
|
||||
|
||||
if !input_arg_doc.is_empty() {
|
||||
arg_doc.push_str(&format!("<p>{input_arg_doc}</p>"));
|
||||
}
|
||||
|
||||
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_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<char> = 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 < n_of_consecutive_chars_allowed {
|
||||
output.push(c);
|
||||
consecutive_count += 1;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
output.push(c);
|
||||
prev_char = Some(c);
|
||||
consecutive_count = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -701,6 +701,14 @@ Finally, call `.build()` to create the instance of `{name}`.
|
|||
}
|
||||
|
||||
pub fn field_impl(&self, field: &FieldInfo) -> Result<TokenStream, Error> {
|
||||
let FieldInfo {
|
||||
name: field_name,
|
||||
ty: field_type,
|
||||
..
|
||||
} = field;
|
||||
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 {
|
||||
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<syn::GenericArgument> = self
|
||||
.generics
|
||||
.params
|
||||
|
|
129
packages/core-macro/src/utils.rs
Normal file
129
packages/core-macro/src/utils.rs
Normal file
|
@ -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<String> {
|
||||
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<String>,
|
||||
pub note: Option<String>,
|
||||
}
|
||||
|
||||
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<Self> {
|
||||
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::<DeprecatedAttributeArgsParser>()?;
|
||||
|
||||
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<String>,
|
||||
note: Option<String>,
|
||||
}
|
||||
|
||||
impl Parse for DeprecatedAttributeArgsParser {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let mut since: Option<String> = None;
|
||||
let mut note: Option<String> = None;
|
||||
|
||||
if input.peek(kw::since) {
|
||||
input.parse::<kw::since>()?;
|
||||
input.parse::<Token![=]>()?;
|
||||
|
||||
since = lit_to_string(input.parse()?);
|
||||
}
|
||||
|
||||
if input.peek(Token![,]) && input.peek2(kw::note) {
|
||||
input.parse::<Token![,]>()?;
|
||||
input.parse::<kw::note>()?;
|
||||
input.parse::<Token![=]>()?;
|
||||
|
||||
note = lit_to_string(input.parse()?);
|
||||
}
|
||||
|
||||
Ok(Self { since, note })
|
||||
}
|
||||
}
|
|
@ -166,6 +166,7 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, 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::Poll;
|
||||
|
@ -219,6 +220,8 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, 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,
|
||||
|
@ -335,6 +338,10 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, 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);
|
||||
view.desktop_context
|
||||
.webview
|
||||
.window()
|
||||
.set_visible(is_visible_before_start);
|
||||
}
|
||||
|
||||
EventData::Ipc(msg) if msg.method() == "browser_open" => {
|
||||
|
|
|
@ -13,7 +13,7 @@ pub(crate) fn build(
|
|||
proxy: EventLoopProxy<UserWindowEvent>,
|
||||
) -> (WebView, WebContext, EditQueue) {
|
||||
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();
|
||||
|
|
|
@ -121,8 +121,15 @@ impl<Props: Clone + serde::Serialize + serde::de::DeserializeOwned + Send + Sync
|
|||
#[cfg(feature = "web")]
|
||||
/// Launch the web application
|
||||
pub fn launch_web(self) {
|
||||
let cfg = self.web_cfg.hydrate(true);
|
||||
dioxus_web::launch_with_props(self.component, get_root_props_from_document().unwrap(), cfg);
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
{
|
||||
let cfg = self.web_cfg.hydrate(true);
|
||||
dioxus_web::launch_with_props(
|
||||
self.component,
|
||||
get_root_props_from_document().unwrap(),
|
||||
cfg,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "desktop")]
|
||||
|
|
|
@ -64,3 +64,10 @@ pub mod prelude {
|
|||
|
||||
pub use hooks::{server_cached::server_cached, server_future::use_server_future};
|
||||
}
|
||||
|
||||
// Warn users about overlapping features
|
||||
#[cfg(all(feature = "ssr", feature = "web"))]
|
||||
compile_error!("The `ssr` feature (enabled by `warp`, `axum`, or `salvo`) and `web` feature are overlapping. Please choose one or the other.");
|
||||
|
||||
#[cfg(all(feature = "ssr", feature = "desktop"))]
|
||||
compile_error!("The `ssr` feature (enabled by `warp`, `axum`, or `salvo`) and `desktop` feature are overlapping. Please choose one or the other.");
|
||||
|
|
|
@ -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"] }
|
||||
|
||||
|
|
|
@ -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#"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head></head>
|
||||
<body><div id="main"></div></body>
|
||||
{glue}
|
||||
</html>
|
||||
"#,
|
||||
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::<Route> {}
|
||||
}
|
||||
let history = LiveviewHistory::new(cx);
|
||||
render! { Router::<Route> {
|
||||
config: || RouterConfig::default().history(history),
|
||||
} }
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "liveview"))]
|
||||
#[component]
|
||||
fn Root(cx: Scope) -> Element {
|
||||
render! { Router::<Route> {} }
|
||||
}
|
||||
|
||||
#[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::<Route> {}
|
||||
}
|
||||
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<Url> 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> {
|
||||
|
|
|
@ -232,7 +232,7 @@ impl RouterContext {
|
|||
IntoRoutable::FromStr(url) => {
|
||||
let parsed_route: NavigationTarget<Rc<dyn Any>> = match self.route_from_str(&url) {
|
||||
Ok(route) => NavigationTarget::Internal(route),
|
||||
Err(err) => NavigationTarget::External(err),
|
||||
Err(_) => NavigationTarget::External(url),
|
||||
};
|
||||
parsed_route
|
||||
}
|
||||
|
|
441
packages/router/src/history/liveview.rs
Normal file
441
packages/router/src/history/liveview.rs
Normal file
|
@ -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<R: Routable>
|
||||
where
|
||||
<R as FromStr>::Err: std::fmt::Display,
|
||||
{
|
||||
action_tx: tokio::sync::mpsc::UnboundedSender<Action<R>>,
|
||||
timeline: Arc<Mutex<Timeline<R>>>,
|
||||
updater_callback: Arc<RwLock<Arc<dyn Fn() + Send + Sync>>>,
|
||||
}
|
||||
|
||||
struct Timeline<R: Routable>
|
||||
where
|
||||
<R as FromStr>::Err: std::fmt::Display,
|
||||
{
|
||||
current_index: usize,
|
||||
routes: BTreeMap<usize, R>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
struct State {
|
||||
index: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
struct Session<R: Routable>
|
||||
where
|
||||
<R as FromStr>::Err: std::fmt::Display,
|
||||
{
|
||||
#[serde(with = "routes")]
|
||||
routes: BTreeMap<usize, R>,
|
||||
last_visited: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SessionStorage {
|
||||
liveview: Option<String>,
|
||||
}
|
||||
|
||||
enum Action<R: Routable> {
|
||||
GoBack,
|
||||
GoForward,
|
||||
Push(R),
|
||||
Replace(R),
|
||||
External(String),
|
||||
}
|
||||
|
||||
impl<R: Routable> Timeline<R>
|
||||
where
|
||||
<R as FromStr>::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<State>,
|
||||
session: Option<Session<R>>,
|
||||
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>) -> 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<R> {
|
||||
Session {
|
||||
routes: self.routes.clone(),
|
||||
last_visited: self.current_index,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Routable> LiveviewHistory<R>
|
||||
where
|
||||
<R as FromStr>::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::<Action<R>>();
|
||||
let action_rx = Arc::new(Mutex::new(action_rx));
|
||||
let timeline = Arc::new(Mutex::new(Timeline::new(initial_path)));
|
||||
let updater_callback: Arc<RwLock<Arc<dyn Fn() + Send + Sync>>> =
|
||||
Arc::new(RwLock::new(Arc::new(|| {})));
|
||||
|
||||
let eval_provider = cx
|
||||
.consume_context::<Rc<dyn EvalProvider>>()
|
||||
.expect("evaluator not provided");
|
||||
|
||||
let create_eval = Rc::new(move |script: &str| {
|
||||
eval_provider
|
||||
.new_evaluator(script.to_string())
|
||||
.map(UseEval::new)
|
||||
}) as Rc<dyn Fn(&str) -> Result<UseEval, EvalError>>;
|
||||
|
||||
// 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<State>,
|
||||
Option<Session<R>>,
|
||||
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<State>)>(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<R: Routable> HistoryProvider<R> for LiveviewHistory<R>
|
||||
where
|
||||
<R as FromStr>::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<usize> = 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<usize> = 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<dyn Fn() + Send + Sync>) {
|
||||
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<S, R>(routes: &BTreeMap<usize, R>, serializer: S) -> Result<S::Ok, S::Error>
|
||||
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<BTreeMap<usize, R>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
R: Routable,
|
||||
<R as FromStr>::Err: std::fmt::Display,
|
||||
{
|
||||
struct BTreeMapVisitor<R> {
|
||||
marker: std::marker::PhantomData<R>,
|
||||
}
|
||||
|
||||
impl<'de, R> Visitor<'de> for BTreeMapVisitor<R>
|
||||
where
|
||||
R: Routable,
|
||||
<R as FromStr>::Err: std::fmt::Display,
|
||||
{
|
||||
type Value = BTreeMap<usize, R>;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("a map with indices and routable values")
|
||||
}
|
||||
|
||||
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
|
||||
where
|
||||
M: MapAccess<'de>,
|
||||
{
|
||||
let mut routes = BTreeMap::new();
|
||||
while let Some((index, route)) = map.next_entry::<String, String>()? {
|
||||
let index = index.parse::<usize>().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,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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")]
|
||||
|
|
|
@ -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<RefCell<Vec<Effect>>>,
|
||||
pub(crate) effects: CopyValue<Vec<Effect>>,
|
||||
}
|
||||
|
||||
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<Effect> {
|
||||
self.effects.read().last().copied()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_effect_stack() -> EffectStack {
|
||||
|
@ -18,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
|
||||
}
|
||||
}
|
||||
|
@ -57,6 +69,7 @@ pub fn use_effect_with_dependencies<D: Dependency>(
|
|||
pub struct Effect {
|
||||
pub(crate) source: ScopeId,
|
||||
pub(crate) callback: CopyValue<Box<dyn FnMut()>>,
|
||||
pub(crate) effect_stack: EffectStack,
|
||||
}
|
||||
|
||||
impl Debug for Effect {
|
||||
|
@ -67,7 +80,7 @@ impl Debug for Effect {
|
|||
|
||||
impl Effect {
|
||||
pub(crate) fn current() -> Option<Self> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -78,19 +78,21 @@ pub fn selector<R: PartialEq>(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 || {
|
||||
|
|
|
@ -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<T> {
|
|||
pub(crate) subscribers: Rc<RefCell<Vec<ScopeId>>>,
|
||||
pub(crate) effect_subscribers: Rc<RefCell<Vec<Effect>>>,
|
||||
pub(crate) update_any: Arc<dyn Fn(ScopeId)>,
|
||||
pub(crate) effect_stack: EffectStack,
|
||||
pub(crate) value: T,
|
||||
}
|
||||
|
||||
|
@ -144,6 +145,7 @@ impl<T: 'static> Signal<T> {
|
|||
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<T: 'static> Signal<T> {
|
|||
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<T: 'static> Signal<T> {
|
|||
/// If the signal has been dropped, this will panic.
|
||||
pub fn read(&self) -> Ref<T> {
|
||||
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);
|
||||
|
|
Loading…
Add table
Reference in a new issue