mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 12:31:55 -05:00
Compare commits
23 Commits
once-resou
...
3086
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb05e080e5 | ||
|
|
9f7bacfcf9 | ||
|
|
b0150ceeec | ||
|
|
af8df34360 | ||
|
|
b2e6185b22 | ||
|
|
d2bfb3080b | ||
|
|
72ebd17042 | ||
|
|
e2f0b4deeb | ||
|
|
57c07e9aec | ||
|
|
0835066bc0 | ||
|
|
656e83fe24 | ||
|
|
ad0252ecfd | ||
|
|
77f05c6f4e | ||
|
|
a4ea491dc0 | ||
|
|
3c89b9c930 | ||
|
|
93d7ba0d5f | ||
|
|
e188993800 | ||
|
|
c1dc8c7629 | ||
|
|
ab9de1b8c0 | ||
|
|
b39985d9b8 | ||
|
|
5e8e93001d | ||
|
|
a4ed0cbe5b | ||
|
|
422fe9f43b |
2
.github/workflows/run-cargo-make-task.yml
vendored
2
.github/workflows/run-cargo-make-task.yml
vendored
@@ -94,7 +94,7 @@ jobs:
|
||||
fi
|
||||
done
|
||||
- name: Install Deno
|
||||
uses: denoland/setup-deno@v1
|
||||
uses: denoland/setup-deno@v2
|
||||
with:
|
||||
deno-version: v1.x
|
||||
- name: Maybe install gtk-rs dependencies
|
||||
|
||||
@@ -64,7 +64,7 @@ pub fn RouterExample() -> impl IntoView {
|
||||
|
||||
// You can define other routes in their own component.
|
||||
// Routes implement the MatchNestedRoutes
|
||||
#[component]
|
||||
#[component(transparent)]
|
||||
pub fn ContactRoutes() -> impl MatchNestedRoutes + Clone {
|
||||
view! {
|
||||
<ParentRoute path=path!("") view=ContactList>
|
||||
|
||||
@@ -1381,39 +1381,41 @@ where
|
||||
),
|
||||
)
|
||||
} else {
|
||||
router.route(
|
||||
path,
|
||||
match mode {
|
||||
SsrMode::OutOfOrder => {
|
||||
render_app_to_stream_with_context(
|
||||
additional_context_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
method,
|
||||
)
|
||||
}
|
||||
SsrMode::PartiallyBlocked => {
|
||||
render_app_to_stream_with_context_and_replace_blocks(
|
||||
additional_context_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
method,
|
||||
true,
|
||||
)
|
||||
}
|
||||
SsrMode::InOrder => {
|
||||
render_app_to_stream_in_order_with_context(
|
||||
additional_context_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
method,
|
||||
)
|
||||
}
|
||||
SsrMode::Async => render_app_async_with_context(
|
||||
additional_context_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
method,
|
||||
),
|
||||
_ => unreachable!()
|
||||
},
|
||||
)
|
||||
router
|
||||
.route(path, web::head().to(HttpResponse::Ok))
|
||||
.route(
|
||||
path,
|
||||
match mode {
|
||||
SsrMode::OutOfOrder => {
|
||||
render_app_to_stream_with_context(
|
||||
additional_context_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
method,
|
||||
)
|
||||
}
|
||||
SsrMode::PartiallyBlocked => {
|
||||
render_app_to_stream_with_context_and_replace_blocks(
|
||||
additional_context_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
method,
|
||||
true,
|
||||
)
|
||||
}
|
||||
SsrMode::InOrder => {
|
||||
render_app_to_stream_in_order_with_context(
|
||||
additional_context_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
method,
|
||||
)
|
||||
}
|
||||
SsrMode::Async => render_app_async_with_context(
|
||||
additional_context_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
method,
|
||||
),
|
||||
_ => unreachable!()
|
||||
},
|
||||
)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ where
|
||||
T: Send + Sync + Serialize + DeserializeOwned + 'static,
|
||||
Fut: std::future::Future<Output = T> + Send + 'static,
|
||||
Chil: FnOnce(&T) -> V + Send + 'static,
|
||||
V: IntoView,
|
||||
V: IntoView + 'static,
|
||||
{
|
||||
let res = ArcOnceResource::<T>::new_with_options(future, blocking);
|
||||
let ready = res.ready();
|
||||
|
||||
@@ -41,7 +41,10 @@
|
||||
//!
|
||||
//! Use `SyncCallback` if the function is not `Sync` and `Send`.
|
||||
|
||||
use reactive_graph::owner::{LocalStorage, StoredValue};
|
||||
use reactive_graph::{
|
||||
owner::{LocalStorage, StoredValue},
|
||||
traits::WithValue,
|
||||
};
|
||||
use std::{fmt, rc::Rc, sync::Arc};
|
||||
|
||||
/// A wrapper trait for calling callbacks.
|
||||
|
||||
@@ -35,7 +35,7 @@ pub fn Provider<T, Chil>(
|
||||
) -> impl IntoView
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
Chil: IntoView,
|
||||
Chil: IntoView + 'static,
|
||||
{
|
||||
let owner = Owner::current()
|
||||
.expect("no current reactive Owner found")
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
use leptos::html::HtmlElement;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[test]
|
||||
fn simple_ssr_test() {
|
||||
use leptos::prelude::*;
|
||||
@@ -20,6 +22,7 @@ fn simple_ssr_test() {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[test]
|
||||
fn ssr_test_with_components() {
|
||||
use leptos::prelude::*;
|
||||
@@ -51,6 +54,7 @@ fn ssr_test_with_components() {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[test]
|
||||
fn ssr_test_with_snake_case_components() {
|
||||
use leptos::prelude::*;
|
||||
@@ -81,6 +85,7 @@ fn ssr_test_with_snake_case_components() {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[test]
|
||||
fn test_classes() {
|
||||
use leptos::prelude::*;
|
||||
@@ -98,6 +103,7 @@ fn test_classes() {
|
||||
assert_eq!(rendered.to_html(), "<div class=\"my big red car\"></div>");
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[test]
|
||||
fn ssr_with_styles() {
|
||||
use leptos::prelude::*;
|
||||
@@ -119,6 +125,7 @@ fn ssr_with_styles() {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[test]
|
||||
fn ssr_option() {
|
||||
use leptos::prelude::*;
|
||||
|
||||
@@ -396,8 +396,7 @@ impl IntervalHandle {
|
||||
}
|
||||
}
|
||||
|
||||
/// Repeatedly calls the given function, with a delay of the given duration between calls,
|
||||
/// returning a cancelable handle.
|
||||
/// Repeatedly calls the given function, with a delay of the given duration between calls.
|
||||
/// See [`setInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval).
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
|
||||
@@ -51,7 +51,15 @@ axum = ["server_fn_macro/axum"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["nightly", "tracing", "trace-component-props"]
|
||||
skip_feature_sets = [["csr", "hydrate"], ["hydrate", "csr"], ["hydrate", "ssr"]]
|
||||
skip_feature_sets = [
|
||||
["csr", "hydrate"],
|
||||
["hydrate", "csr"],
|
||||
["hydrate", "ssr"],
|
||||
["actix", "axum"]
|
||||
]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--generate-link-to-definition"]
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(erase_components)'] }
|
||||
|
||||
@@ -18,6 +18,7 @@ use syn::{
|
||||
};
|
||||
|
||||
pub struct Model {
|
||||
is_transparent: bool,
|
||||
island: Option<String>,
|
||||
docs: Docs,
|
||||
unknown_attrs: UnknownAttrs,
|
||||
@@ -62,6 +63,7 @@ impl Parse for Model {
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
is_transparent: false,
|
||||
island: None,
|
||||
docs,
|
||||
unknown_attrs,
|
||||
@@ -102,6 +104,7 @@ pub fn convert_from_snake_case(name: &Ident) -> Ident {
|
||||
impl ToTokens for Model {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let Self {
|
||||
is_transparent,
|
||||
island,
|
||||
docs,
|
||||
unknown_attrs,
|
||||
@@ -116,20 +119,22 @@ impl ToTokens for Model {
|
||||
let no_props = props.is_empty();
|
||||
|
||||
// check for components that end ;
|
||||
let ends_semi =
|
||||
body.block.stmts.iter().last().and_then(|stmt| match stmt {
|
||||
Stmt::Item(Item::Macro(mac)) => mac.semi_token.as_ref(),
|
||||
_ => None,
|
||||
});
|
||||
if let Some(semi) = ends_semi {
|
||||
proc_macro_error2::emit_error!(
|
||||
semi.span(),
|
||||
"A component that ends with a `view!` macro followed by a \
|
||||
semicolon will return (), an empty view. This is usually an \
|
||||
accident, not intentional, so we prevent it. If you’d like \
|
||||
to return (), you can do it it explicitly by returning () as \
|
||||
the last item from the component."
|
||||
);
|
||||
if !is_transparent {
|
||||
let ends_semi =
|
||||
body.block.stmts.iter().last().and_then(|stmt| match stmt {
|
||||
Stmt::Item(Item::Macro(mac)) => mac.semi_token.as_ref(),
|
||||
_ => None,
|
||||
});
|
||||
if let Some(semi) = ends_semi {
|
||||
proc_macro_error2::emit_error!(
|
||||
semi.span(),
|
||||
"A component that ends with a `view!` macro followed by a \
|
||||
semicolon will return (), an empty view. This is usually \
|
||||
an accident, not intentional, so we prevent it. If you’d \
|
||||
like to return (), you can do it it explicitly by \
|
||||
returning () as the last item from the component."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//body.sig.ident = format_ident!("__{}", body.sig.ident);
|
||||
@@ -265,14 +270,30 @@ impl ToTokens for Model {
|
||||
}
|
||||
};
|
||||
|
||||
let component = quote! {
|
||||
::leptos::prelude::untrack(
|
||||
move || {
|
||||
#tracing_guard_expr
|
||||
#tracing_props_expr
|
||||
#body_expr
|
||||
}
|
||||
)
|
||||
let component = if *is_transparent {
|
||||
body_expr
|
||||
} else if cfg!(erase_components) {
|
||||
quote! {
|
||||
::leptos::prelude::IntoAny::into_any(
|
||||
::leptos::prelude::untrack(
|
||||
move || {
|
||||
#tracing_guard_expr
|
||||
#tracing_props_expr
|
||||
#body_expr
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
::leptos::prelude::untrack(
|
||||
move || {
|
||||
#tracing_guard_expr
|
||||
#tracing_props_expr
|
||||
#body_expr
|
||||
}
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
// add island wrapper if island
|
||||
@@ -520,6 +541,13 @@ impl ToTokens for Model {
|
||||
}
|
||||
|
||||
impl Model {
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
pub fn is_transparent(mut self, is_transparent: bool) -> Self {
|
||||
self.is_transparent = is_transparent;
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
pub fn with_island(mut self, island: Option<String>) -> Self {
|
||||
self.island = island;
|
||||
|
||||
@@ -535,11 +535,24 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
|
||||
/// ```
|
||||
#[proc_macro_error2::proc_macro_error]
|
||||
#[proc_macro_attribute]
|
||||
pub fn component(
|
||||
_args: proc_macro::TokenStream,
|
||||
s: TokenStream,
|
||||
) -> TokenStream {
|
||||
component_macro(s, None)
|
||||
pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
let is_transparent = if !args.is_empty() {
|
||||
let transparent = parse_macro_input!(args as syn::Ident);
|
||||
|
||||
if transparent != "transparent" {
|
||||
abort!(
|
||||
transparent,
|
||||
"only `transparent` is supported";
|
||||
help = "try `#[component(transparent)]` or `#[component]`"
|
||||
);
|
||||
}
|
||||
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
component_macro(s, is_transparent, None)
|
||||
}
|
||||
|
||||
/// Defines a component as an interactive island when you are using the
|
||||
@@ -615,17 +628,37 @@ pub fn component(
|
||||
/// ```
|
||||
#[proc_macro_error2::proc_macro_error]
|
||||
#[proc_macro_attribute]
|
||||
pub fn island(_args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
pub fn island(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
let is_transparent = if !args.is_empty() {
|
||||
let transparent = parse_macro_input!(args as syn::Ident);
|
||||
|
||||
if transparent != "transparent" {
|
||||
abort!(
|
||||
transparent,
|
||||
"only `transparent` is supported";
|
||||
help = "try `#[component(transparent)]` or `#[component]`"
|
||||
);
|
||||
}
|
||||
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let island_src = s.to_string();
|
||||
component_macro(s, Some(island_src))
|
||||
component_macro(s, is_transparent, Some(island_src))
|
||||
}
|
||||
|
||||
fn component_macro(s: TokenStream, island: Option<String>) -> TokenStream {
|
||||
fn component_macro(
|
||||
s: TokenStream,
|
||||
is_transparent: bool,
|
||||
island: Option<String>,
|
||||
) -> TokenStream {
|
||||
let mut dummy = syn::parse::<DummyModel>(s.clone());
|
||||
let parse_result = syn::parse::<component::Model>(s);
|
||||
|
||||
if let (Ok(ref mut unexpanded), Ok(model)) = (&mut dummy, parse_result) {
|
||||
let expanded = model.with_island(island).into_token_stream();
|
||||
let expanded = model.is_transparent(is_transparent).with_island(island).into_token_stream();
|
||||
if !matches!(unexpanded.vis, Visibility::Public(_)) {
|
||||
unexpanded.vis = Visibility::Public(Pub {
|
||||
span: unexpanded.vis.span(),
|
||||
|
||||
@@ -179,7 +179,7 @@ fn is_inert_element(orig_node: &Node<impl CustomNode>) -> bool {
|
||||
}
|
||||
|
||||
enum Item<'a, T> {
|
||||
Node(&'a Node<T>),
|
||||
Node(&'a Node<T>, bool),
|
||||
ClosingTag(String),
|
||||
}
|
||||
|
||||
@@ -290,10 +290,11 @@ impl<'a> InertElementBuilder<'a> {
|
||||
|
||||
fn inert_element_to_tokens(
|
||||
node: &Node<impl CustomNode>,
|
||||
escape_text: bool,
|
||||
global_class: Option<&TokenTree>,
|
||||
) -> Option<TokenStream> {
|
||||
let mut html = InertElementBuilder::new(global_class);
|
||||
let mut nodes = VecDeque::from([Item::Node(node)]);
|
||||
let mut nodes = VecDeque::from([Item::Node(node, escape_text)]);
|
||||
|
||||
while let Some(current) = nodes.pop_front() {
|
||||
match current {
|
||||
@@ -303,21 +304,32 @@ fn inert_element_to_tokens(
|
||||
html.push_str(&tag);
|
||||
html.push('>');
|
||||
}
|
||||
Item::Node(current) => {
|
||||
Item::Node(current, escape) => {
|
||||
match current {
|
||||
Node::RawText(raw) => {
|
||||
let text = raw.to_string_best();
|
||||
let text = html_escape::encode_text(&text);
|
||||
let text = if escape {
|
||||
html_escape::encode_text(&text)
|
||||
} else {
|
||||
text.into()
|
||||
};
|
||||
html.push_str(&text);
|
||||
}
|
||||
Node::Text(text) => {
|
||||
let text = text.value_string();
|
||||
let text = html_escape::encode_text(&text);
|
||||
let text = if escape {
|
||||
html_escape::encode_text(&text)
|
||||
} else {
|
||||
text.into()
|
||||
};
|
||||
html.push_str(&text);
|
||||
}
|
||||
Node::Element(node) => {
|
||||
let self_closing = is_self_closing(node);
|
||||
let el_name = node.name().to_string();
|
||||
let escape = el_name != "script"
|
||||
&& el_name != "style"
|
||||
&& el_name != "textarea";
|
||||
|
||||
// opening tag
|
||||
html.push('<');
|
||||
@@ -364,7 +376,7 @@ fn inert_element_to_tokens(
|
||||
nodes.push_front(Item::ClosingTag(el_name));
|
||||
let children = node.children.iter().rev();
|
||||
for child in children {
|
||||
nodes.push_front(Item::Node(child));
|
||||
nodes.push_front(Item::Node(child, escape));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -548,7 +560,9 @@ fn node_to_tokens(
|
||||
view_marker,
|
||||
disable_inert_html,
|
||||
),
|
||||
Node::Block(block) => Some(quote! { #block }),
|
||||
Node::Block(block) => {
|
||||
Some(quote! { ::leptos::prelude::IntoRender::into_render(#block) })
|
||||
}
|
||||
Node::Text(text) => Some(text_to_tokens(&text.value)),
|
||||
Node::RawText(raw) => {
|
||||
let text = raw.to_string_best();
|
||||
@@ -557,7 +571,11 @@ fn node_to_tokens(
|
||||
}
|
||||
Node::Element(el_node) => {
|
||||
if !top_level && is_inert {
|
||||
inert_element_to_tokens(node, global_class)
|
||||
let el_name = el_node.name().to_string();
|
||||
let escape = el_name != "script"
|
||||
&& el_name != "style"
|
||||
&& el_name != "textarea";
|
||||
inert_element_to_tokens(node, escape, global_class)
|
||||
} else {
|
||||
element_to_tokens(
|
||||
el_node,
|
||||
@@ -725,6 +743,11 @@ pub(crate) fn element_to_tokens(
|
||||
quote! { ::leptos::tachys::html::element::#custom(#name) }
|
||||
} else if is_svg_element(&tag) {
|
||||
parent_type = TagType::Svg;
|
||||
let name = if tag == "use" || tag == "use_" {
|
||||
Ident::new_raw("use", name.span()).to_token_stream()
|
||||
} else {
|
||||
name.to_token_stream()
|
||||
};
|
||||
quote! { ::leptos::tachys::svg::#name() }
|
||||
} else if is_math_ml_element(&tag) {
|
||||
parent_type = TagType::Math;
|
||||
@@ -878,7 +901,7 @@ fn attribute_to_tokens(
|
||||
NodeName::Path(path) => path.path.get_ident(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let value = attribute_value(node);
|
||||
let value = attribute_value(node, false);
|
||||
quote! {
|
||||
.#node_ref(#value)
|
||||
}
|
||||
@@ -928,13 +951,13 @@ fn attribute_to_tokens(
|
||||
// we don't provide statically-checked methods for SVG attributes
|
||||
|| (tag_type == TagType::Svg && name != "inner_html")
|
||||
{
|
||||
let value = attribute_value(node);
|
||||
let value = attribute_value(node, true);
|
||||
quote! {
|
||||
.attr(#name, #value)
|
||||
}
|
||||
} else {
|
||||
let key = attribute_name(&node.key);
|
||||
let value = attribute_value(node);
|
||||
let value = attribute_value(node, true);
|
||||
|
||||
// special case of global_class and class attribute
|
||||
if &node.key.to_string() == "class"
|
||||
@@ -971,11 +994,11 @@ pub(crate) fn attribute_absolute(
|
||||
let id = &parts[0];
|
||||
match id {
|
||||
NodeNameFragment::Ident(id) => {
|
||||
let value = attribute_value(node);
|
||||
// ignore `let:` and `clone:`
|
||||
if id == "let" || id == "clone" {
|
||||
None
|
||||
} else if id == "attr" {
|
||||
let value = attribute_value(node, true);
|
||||
let key = &parts[1];
|
||||
let key_name = key.to_string();
|
||||
if key_name == "class" || key_name == "style" {
|
||||
@@ -983,6 +1006,7 @@ pub(crate) fn attribute_absolute(
|
||||
quote! { ::leptos::tachys::html::#key::#key(#value) },
|
||||
)
|
||||
} else if key_name == "aria" {
|
||||
let value = attribute_value(node, true);
|
||||
let mut parts_iter = parts.iter();
|
||||
parts_iter.next();
|
||||
let fn_name = parts_iter.map(|p| p.to_string()).collect::<Vec<String>>().join("_");
|
||||
@@ -1011,6 +1035,7 @@ pub(crate) fn attribute_absolute(
|
||||
},
|
||||
)
|
||||
} else if id == "style" || id == "class" {
|
||||
let value = attribute_value(node, false);
|
||||
let key = &node.key.to_string();
|
||||
let key = key
|
||||
.replacen("style:", "", 1)
|
||||
@@ -1019,6 +1044,7 @@ pub(crate) fn attribute_absolute(
|
||||
quote! { ::leptos::tachys::html::#id::#id((#key, #value)) },
|
||||
)
|
||||
} else if id == "prop" {
|
||||
let value = attribute_value(node, false);
|
||||
let key = &node.key.to_string();
|
||||
let key = key.replacen("prop:", "", 1);
|
||||
Some(
|
||||
@@ -1075,7 +1101,7 @@ pub(crate) fn two_way_binding_to_tokens(
|
||||
name: &str,
|
||||
node: &KeyedAttribute,
|
||||
) -> TokenStream {
|
||||
let value = attribute_value(node);
|
||||
let value = attribute_value(node, false);
|
||||
|
||||
let ident =
|
||||
format_ident!("{}", name.to_case(UpperCamel), span = node.key.span());
|
||||
@@ -1100,7 +1126,7 @@ pub(crate) fn event_type_and_handler(
|
||||
name: &str,
|
||||
node: &KeyedAttribute,
|
||||
) -> (TokenStream, TokenStream, TokenStream) {
|
||||
let handler = attribute_value(node);
|
||||
let handler = attribute_value(node, false);
|
||||
|
||||
let (event_type, is_custom, is_force_undelegated, is_targeted) =
|
||||
parse_event_name(name);
|
||||
@@ -1157,7 +1183,7 @@ fn class_to_tokens(
|
||||
class: TokenStream,
|
||||
class_name: Option<&str>,
|
||||
) -> TokenStream {
|
||||
let value = attribute_value(node);
|
||||
let value = attribute_value(node, false);
|
||||
if let Some(class_name) = class_name {
|
||||
quote! {
|
||||
.#class((#class_name, #value))
|
||||
@@ -1174,7 +1200,7 @@ fn style_to_tokens(
|
||||
style: TokenStream,
|
||||
style_name: Option<&str>,
|
||||
) -> TokenStream {
|
||||
let value = attribute_value(node);
|
||||
let value = attribute_value(node, false);
|
||||
if let Some(style_name) = style_name {
|
||||
quote! {
|
||||
.#style((#style_name, #value))
|
||||
@@ -1191,7 +1217,7 @@ fn prop_to_tokens(
|
||||
prop: TokenStream,
|
||||
key: &str,
|
||||
) -> TokenStream {
|
||||
let value = attribute_value(node);
|
||||
let value = attribute_value(node, false);
|
||||
quote! {
|
||||
.#prop(#key, #value)
|
||||
}
|
||||
@@ -1348,7 +1374,10 @@ fn attribute_name(name: &NodeName) -> TokenStream {
|
||||
}
|
||||
}
|
||||
|
||||
fn attribute_value(attr: &KeyedAttribute) -> TokenStream {
|
||||
fn attribute_value(
|
||||
attr: &KeyedAttribute,
|
||||
is_attribute_proper: bool,
|
||||
) -> TokenStream {
|
||||
match attr.possible_value.to_value() {
|
||||
None => quote! { true },
|
||||
Some(value) => match &value.value {
|
||||
@@ -1363,14 +1392,26 @@ fn attribute_value(attr: &KeyedAttribute) -> TokenStream {
|
||||
}
|
||||
}
|
||||
|
||||
quote! {
|
||||
{#expr}
|
||||
if matches!(expr, Expr::Lit(_)) || !is_attribute_proper {
|
||||
quote! {
|
||||
#expr
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
::leptos::prelude::IntoAttributeValue::into_attribute_value(#expr)
|
||||
}
|
||||
}
|
||||
}
|
||||
// any value in braces: expand as-is to give proper r-a support
|
||||
KVAttributeValue::InvalidBraced(block) => {
|
||||
quote! {
|
||||
#block
|
||||
if is_attribute_proper {
|
||||
quote! {
|
||||
::leptos::prelude::IntoAttributeValue::into_attribute_value(#block)
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
#block
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#[cfg(not(erase_components))]
|
||||
#[test]
|
||||
fn ui() {
|
||||
let t = trybuild::TestCases::new();
|
||||
|
||||
@@ -35,10 +35,10 @@ impl<T> Clone for ArcLocalResource<T> {
|
||||
|
||||
impl<T> ArcLocalResource<T> {
|
||||
#[track_caller]
|
||||
pub fn new<Fut>(fetcher: impl Fn() -> Fut + Send + Sync + 'static) -> Self
|
||||
pub fn new<Fut>(fetcher: impl Fn() -> Fut + 'static) -> Self
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
Fut: Future<Output = T> + Send + 'static,
|
||||
T: 'static,
|
||||
Fut: Future<Output = T> + 'static,
|
||||
{
|
||||
let fetcher = move || {
|
||||
let fut = fetcher();
|
||||
@@ -60,7 +60,7 @@ impl<T> ArcLocalResource<T> {
|
||||
}
|
||||
};
|
||||
Self {
|
||||
data: ArcAsyncDerived::new(fetcher),
|
||||
data: ArcAsyncDerived::new_unsync(fetcher),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
}
|
||||
@@ -103,7 +103,7 @@ impl<T> DefinedAt for ArcLocalResource<T> {
|
||||
|
||||
impl<T> ReadUntracked for ArcLocalResource<T>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
T: 'static,
|
||||
{
|
||||
type Value = ReadGuard<Option<T>, AsyncPlain<Option<T>>>;
|
||||
|
||||
@@ -201,7 +201,7 @@ impl<T> LocalResource<T> {
|
||||
pub fn new<Fut>(fetcher: impl Fn() -> Fut + 'static) -> Self
|
||||
where
|
||||
T: 'static,
|
||||
Fut: Future<Output = T> + Send + 'static,
|
||||
Fut: Future<Output = T> + 'static,
|
||||
{
|
||||
let fetcher = move || {
|
||||
let fut = fetcher();
|
||||
@@ -230,7 +230,7 @@ impl<T> LocalResource<T> {
|
||||
let fetcher = SendWrapper::new(fetcher);
|
||||
AsyncDerived::new(move || {
|
||||
let fut = fetcher();
|
||||
async move { SendWrapper::new(fut.await) }
|
||||
SendWrapper::new(async move { SendWrapper::new(fut.await) })
|
||||
})
|
||||
},
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -280,7 +280,7 @@ impl<T> DefinedAt for LocalResource<T> {
|
||||
|
||||
impl<T> ReadUntracked for LocalResource<T>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
T: 'static,
|
||||
{
|
||||
type Value =
|
||||
ReadGuard<Option<SendWrapper<T>>, AsyncPlain<Option<SendWrapper<T>>>>;
|
||||
@@ -307,7 +307,7 @@ impl<T: 'static> IsDisposed for LocalResource<T> {
|
||||
|
||||
impl<T: 'static> ToAnySource for LocalResource<T>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
T: 'static,
|
||||
{
|
||||
fn to_any_source(&self) -> AnySource {
|
||||
self.data.to_any_source()
|
||||
@@ -316,7 +316,7 @@ where
|
||||
|
||||
impl<T: 'static> ToAnySubscriber for LocalResource<T>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
T: 'static,
|
||||
{
|
||||
fn to_any_subscriber(&self) -> AnySubscriber {
|
||||
self.data.to_any_subscriber()
|
||||
@@ -325,7 +325,7 @@ where
|
||||
|
||||
impl<T> Source for LocalResource<T>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
T: 'static,
|
||||
{
|
||||
fn add_subscriber(&self, subscriber: AnySubscriber) {
|
||||
self.data.add_subscriber(subscriber)
|
||||
@@ -342,7 +342,7 @@ where
|
||||
|
||||
impl<T> ReactiveNode for LocalResource<T>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
T: 'static,
|
||||
{
|
||||
fn mark_dirty(&self) {
|
||||
self.data.mark_dirty();
|
||||
@@ -363,7 +363,7 @@ where
|
||||
|
||||
impl<T> Subscriber for LocalResource<T>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
T: 'static,
|
||||
{
|
||||
fn add_source(&self, source: AnySource) {
|
||||
self.data.add_source(source);
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
use crate::{
|
||||
computed::{ArcMemo, Memo},
|
||||
diagnostics::is_suppressing_resource_load,
|
||||
owner::{ArenaItem, FromLocal, LocalStorage, Storage, SyncStorage},
|
||||
owner::{
|
||||
ArcStoredValue, ArenaItem, FromLocal, LocalStorage, Storage,
|
||||
SyncStorage,
|
||||
},
|
||||
signal::{ArcRwSignal, RwSignal},
|
||||
traits::{DefinedAt, Dispose, Get, GetUntracked, Update},
|
||||
traits::{DefinedAt, Dispose, Get, GetUntracked, GetValue, Update},
|
||||
unwrap_signal,
|
||||
};
|
||||
use any_spawner::Executor;
|
||||
@@ -93,6 +96,7 @@ pub struct ArcAction<I, O> {
|
||||
input: ArcRwSignal<Option<I>>,
|
||||
value: ArcRwSignal<Option<O>>,
|
||||
version: ArcRwSignal<usize>,
|
||||
dispatched: ArcStoredValue<usize>,
|
||||
#[allow(clippy::complexity)]
|
||||
action_fn: Arc<
|
||||
dyn Fn(&I) -> Pin<Box<dyn Future<Output = O> + Send>> + Send + Sync,
|
||||
@@ -108,6 +112,7 @@ impl<I, O> Clone for ArcAction<I, O> {
|
||||
input: self.input.clone(),
|
||||
value: self.value.clone(),
|
||||
version: self.version.clone(),
|
||||
dispatched: self.dispatched.clone(),
|
||||
action_fn: self.action_fn.clone(),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: self.defined_at,
|
||||
@@ -191,6 +196,7 @@ where
|
||||
input: Default::default(),
|
||||
value: ArcRwSignal::new(value),
|
||||
version: Default::default(),
|
||||
dispatched: Default::default(),
|
||||
action_fn: Arc::new(move |input| Box::pin(action_fn(input))),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
@@ -230,14 +236,14 @@ where
|
||||
|
||||
// Update the state before loading
|
||||
self.in_flight.update(|n| *n += 1);
|
||||
let current_version =
|
||||
self.version.try_get_untracked().unwrap_or_default();
|
||||
let current_version = self.dispatched.get_value();
|
||||
self.input.try_update(|inp| *inp = Some(input));
|
||||
|
||||
// Spawn the task
|
||||
crate::spawn({
|
||||
let input = self.input.clone();
|
||||
let version = self.version.clone();
|
||||
let dispatched = self.dispatched.clone();
|
||||
let value = self.value.clone();
|
||||
let in_flight = self.in_flight.clone();
|
||||
async move {
|
||||
@@ -249,7 +255,7 @@ where
|
||||
// otherwise, update the value
|
||||
result = fut => {
|
||||
in_flight.update(|n| *n = n.saturating_sub(1));
|
||||
let is_latest = version.get_untracked() <= current_version;
|
||||
let is_latest = dispatched.get_value() <= current_version;
|
||||
if is_latest {
|
||||
version.update(|n| *n += 1);
|
||||
value.update(|n| *n = Some(result));
|
||||
@@ -282,8 +288,7 @@ where
|
||||
|
||||
// Update the state before loading
|
||||
self.in_flight.update(|n| *n += 1);
|
||||
let current_version =
|
||||
self.version.try_get_untracked().unwrap_or_default();
|
||||
let current_version = self.dispatched.get_value();
|
||||
self.input.try_update(|inp| *inp = Some(input));
|
||||
|
||||
// Spawn the task
|
||||
@@ -291,6 +296,7 @@ where
|
||||
let input = self.input.clone();
|
||||
let version = self.version.clone();
|
||||
let value = self.value.clone();
|
||||
let dispatched = self.dispatched.clone();
|
||||
let in_flight = self.in_flight.clone();
|
||||
async move {
|
||||
select! {
|
||||
@@ -301,7 +307,7 @@ where
|
||||
// otherwise, update the value
|
||||
result = fut => {
|
||||
in_flight.update(|n| *n = n.saturating_sub(1));
|
||||
let is_latest = version.get_untracked() <= current_version;
|
||||
let is_latest = dispatched.get_value() <= current_version;
|
||||
if is_latest {
|
||||
version.update(|n| *n += 1);
|
||||
value.update(|n| *n = Some(result));
|
||||
@@ -351,6 +357,7 @@ where
|
||||
input: Default::default(),
|
||||
value: ArcRwSignal::new(value),
|
||||
version: Default::default(),
|
||||
dispatched: Default::default(),
|
||||
action_fn: Arc::new(move |input| {
|
||||
Box::pin(SendWrapper::new(action_fn(input)))
|
||||
}),
|
||||
|
||||
@@ -15,6 +15,7 @@ use crate::{
|
||||
};
|
||||
pub use arc_memo::*;
|
||||
pub use async_derived::*;
|
||||
pub(crate) use inner::MemoInner;
|
||||
pub use memo::*;
|
||||
pub use selector::*;
|
||||
|
||||
|
||||
@@ -12,12 +12,14 @@ use std::{
|
||||
sync::{Arc, RwLock, Weak},
|
||||
};
|
||||
|
||||
mod arc_stored_value;
|
||||
mod arena;
|
||||
mod arena_item;
|
||||
mod context;
|
||||
mod storage;
|
||||
mod stored_value;
|
||||
use self::arena::Arena;
|
||||
pub use arc_stored_value::ArcStoredValue;
|
||||
#[cfg(feature = "sandboxed-arenas")]
|
||||
pub use arena::sandboxed::Sandboxed;
|
||||
#[cfg(feature = "sandboxed-arenas")]
|
||||
|
||||
126
reactive_graph/src/owner/arc_stored_value.rs
Normal file
126
reactive_graph/src/owner/arc_stored_value.rs
Normal file
@@ -0,0 +1,126 @@
|
||||
use crate::{
|
||||
signal::guards::{Plain, ReadGuard, UntrackedWriteGuard},
|
||||
traits::{DefinedAt, IsDisposed, ReadValue, WriteValue},
|
||||
};
|
||||
use std::{
|
||||
fmt::{Debug, Formatter},
|
||||
hash::Hash,
|
||||
panic::Location,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
/// A reference-counted getter for any value non-reactively.
|
||||
///
|
||||
/// This is a reference-counted value, which is `Clone` but not `Copy`.
|
||||
/// For arena-allocated `Copy` values, use [`StoredValue`](super::StoredValue).
|
||||
///
|
||||
/// This allows you to create a stable reference for any value by storing it within
|
||||
/// the reactive system. Unlike e.g. [`ArcRwSignal`](crate::signal::ArcRwSignal), it is not reactive;
|
||||
/// accessing it does not cause effects to subscribe, and
|
||||
/// updating it does not notify anything else.
|
||||
pub struct ArcStoredValue<T> {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: &'static Location<'static>,
|
||||
value: Arc<RwLock<T>>,
|
||||
}
|
||||
|
||||
impl<T> Clone for ArcStoredValue<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: self.defined_at,
|
||||
value: Arc::clone(&self.value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Debug for ArcStoredValue<T> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
|
||||
f.debug_struct("ArcStoredValue")
|
||||
.field("type", &std::any::type_name::<T>())
|
||||
.field("value", &Arc::as_ptr(&self.value))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Default> Default for ArcStoredValue<T> {
|
||||
#[track_caller]
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
value: Arc::new(RwLock::new(T::default())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> PartialEq for ArcStoredValue<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
Arc::ptr_eq(&self.value, &other.value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Eq for ArcStoredValue<T> {}
|
||||
|
||||
impl<T> Hash for ArcStoredValue<T> {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
std::ptr::hash(&Arc::as_ptr(&self.value), state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DefinedAt for ArcStoredValue<T> {
|
||||
fn defined_at(&self) -> Option<&'static Location<'static>> {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
Some(self.defined_at)
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ArcStoredValue<T> {
|
||||
/// Creates a new stored value, taking the initial value as its argument.
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", skip_all)
|
||||
)]
|
||||
#[track_caller]
|
||||
pub fn new(value: T) -> Self {
|
||||
Self {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
value: Arc::new(RwLock::new(value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ReadValue for ArcStoredValue<T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
type Value = ReadGuard<T, Plain<T>>;
|
||||
|
||||
fn try_read_value(&self) -> Option<ReadGuard<T, Plain<T>>> {
|
||||
Plain::try_new(Arc::clone(&self.value)).map(ReadGuard::new)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> WriteValue for ArcStoredValue<T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
type Value = T;
|
||||
|
||||
fn try_write_value(&self) -> Option<UntrackedWriteGuard<T>> {
|
||||
UntrackedWriteGuard::try_new(self.value.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IsDisposed for ArcStoredValue<T> {
|
||||
fn is_disposed(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,16 @@
|
||||
use super::{ArenaItem, LocalStorage, Storage, SyncStorage};
|
||||
use super::{
|
||||
arc_stored_value::ArcStoredValue, ArenaItem, LocalStorage, Storage,
|
||||
SyncStorage,
|
||||
};
|
||||
use crate::{
|
||||
signal::guards::{Plain, ReadGuard, UntrackedWriteGuard},
|
||||
traits::{DefinedAt, Dispose, IsDisposed},
|
||||
traits::{DefinedAt, Dispose, IsDisposed, ReadValue, WriteValue},
|
||||
unwrap_signal,
|
||||
};
|
||||
use or_poisoned::OrPoisoned;
|
||||
use std::{
|
||||
fmt::{Debug, Formatter},
|
||||
hash::Hash,
|
||||
panic::Location,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
/// A **non-reactive**, `Copy` handle for any value.
|
||||
@@ -18,9 +20,8 @@ use std::{
|
||||
/// and [`RwSignal`](crate::signal::RwSignal)), it is `Copy` and `'static`. Unlike the signal
|
||||
/// types, it is not reactive; accessing it does not cause effects to subscribe, and
|
||||
/// updating it does not notify anything else.
|
||||
#[derive(Debug)]
|
||||
pub struct StoredValue<T, S = SyncStorage> {
|
||||
value: ArenaItem<Arc<RwLock<T>>, S>,
|
||||
value: ArenaItem<ArcStoredValue<T>, S>,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: &'static Location<'static>,
|
||||
}
|
||||
@@ -33,6 +34,18 @@ impl<T, S> Clone for StoredValue<T, S> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> Debug for StoredValue<T, S>
|
||||
where
|
||||
S: Debug,
|
||||
{
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
|
||||
f.debug_struct("StoredValue")
|
||||
.field("type", &std::any::type_name::<T>())
|
||||
.field("value", &self.value)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> PartialEq for StoredValue<T, S> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.value == other.value
|
||||
@@ -63,13 +76,13 @@ impl<T, S> DefinedAt for StoredValue<T, S> {
|
||||
impl<T, S> StoredValue<T, S>
|
||||
where
|
||||
T: 'static,
|
||||
S: Storage<Arc<RwLock<T>>>,
|
||||
S: Storage<ArcStoredValue<T>>,
|
||||
{
|
||||
/// Stores the given value in the arena allocator.
|
||||
#[track_caller]
|
||||
pub fn new_with_storage(value: T) -> Self {
|
||||
Self {
|
||||
value: ArenaItem::new_with_storage(Arc::new(RwLock::new(value))),
|
||||
value: ArenaItem::new_with_storage(ArcStoredValue::new(value)),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
}
|
||||
@@ -79,7 +92,7 @@ where
|
||||
impl<T, S> Default for StoredValue<T, S>
|
||||
where
|
||||
T: Default + 'static,
|
||||
S: Storage<Arc<RwLock<T>>>,
|
||||
S: Storage<ArcStoredValue<T>>,
|
||||
{
|
||||
#[track_caller] // Default trait is not annotated with #[track_caller]
|
||||
fn default() -> Self {
|
||||
@@ -109,268 +122,31 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S: Storage<Arc<RwLock<T>>>> StoredValue<T, S> {
|
||||
/// Returns an [`Option`] of applying a function to the value within the [`StoredValue`].
|
||||
///
|
||||
/// If the owner of the reactive node has not been disposed [`Some`] is returned. Calling this
|
||||
/// function after the owner has been disposed will always return [`None`].
|
||||
///
|
||||
/// See [`StoredValue::with_value`] for a version that panics in the case of the owner being
|
||||
/// disposed.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use reactive_graph::owner::StoredValue; let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// # use reactive_graph::traits::Dispose;
|
||||
///
|
||||
/// // Does not implement Clone
|
||||
/// struct Data {
|
||||
/// rows: Vec<u8>,
|
||||
/// }
|
||||
///
|
||||
/// let data = StoredValue::new(Data {
|
||||
/// rows: vec![0, 1, 2, 3, 4],
|
||||
/// });
|
||||
///
|
||||
/// // Easy to move into closures because StoredValue is Copy + 'static.
|
||||
/// // *NOTE* this is not the same thing as a derived signal!
|
||||
/// // *NOTE* this will not be automatically rerun as StoredValue is NOT reactive!
|
||||
/// let length_fn = move || data.try_with_value(|inner| inner.rows.len());
|
||||
///
|
||||
/// let sum = data.try_with_value(|inner| inner.rows.iter().sum::<u8>());
|
||||
///
|
||||
/// assert_eq!(sum, Some(10));
|
||||
/// assert_eq!(length_fn(), Some(5));
|
||||
///
|
||||
/// // You should not call dispose yourself in normal user code.
|
||||
/// // This is shown here for the sake of the example.
|
||||
/// data.dispose();
|
||||
///
|
||||
/// let last = data.try_with_value(|inner| inner.rows.last().cloned());
|
||||
///
|
||||
/// assert_eq!(last, None);
|
||||
/// assert_eq!(length_fn(), None);
|
||||
/// ```
|
||||
#[track_caller]
|
||||
pub fn try_with_value<U>(&self, fun: impl FnOnce(&T) -> U) -> Option<U> {
|
||||
impl<T, S> ReadValue for StoredValue<T, S>
|
||||
where
|
||||
T: 'static,
|
||||
S: Storage<ArcStoredValue<T>>,
|
||||
{
|
||||
type Value = ReadGuard<T, Plain<T>>;
|
||||
|
||||
fn try_read_value(&self) -> Option<ReadGuard<T, Plain<T>>> {
|
||||
self.value
|
||||
.try_get_value()
|
||||
.map(|inner| fun(&*inner.read().or_poisoned()))
|
||||
.and_then(|inner| inner.try_read_value())
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the output of applying a function to the value within the [`StoredValue`].
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This function panics when called after the owner of the reactive node has been disposed.
|
||||
/// See [`StoredValue::try_with_value`] for a version without panic.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use reactive_graph::owner::StoredValue; let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
///
|
||||
/// // Does not implement Clone
|
||||
/// struct Data {
|
||||
/// rows: Vec<u8>,
|
||||
/// }
|
||||
///
|
||||
/// let data = StoredValue::new(Data {
|
||||
/// rows: vec![1, 2, 3],
|
||||
/// });
|
||||
///
|
||||
/// // Easy to move into closures because StoredValue is Copy + 'static.
|
||||
/// // *NOTE* this is not the same thing as a derived signal!
|
||||
/// // *NOTE* this will not be automatically rerun as StoredValue is NOT reactive!
|
||||
/// let length_fn = move || data.with_value(|inner| inner.rows.len());
|
||||
///
|
||||
/// let sum = data.with_value(|inner| inner.rows.iter().sum::<u8>());
|
||||
///
|
||||
/// assert_eq!(sum, 6);
|
||||
/// assert_eq!(length_fn(), 3);
|
||||
/// ```
|
||||
#[track_caller]
|
||||
pub fn with_value<U>(&self, fun: impl FnOnce(&T) -> U) -> U {
|
||||
self.try_with_value(fun)
|
||||
.unwrap_or_else(unwrap_signal!(self))
|
||||
}
|
||||
impl<T, S> WriteValue for StoredValue<T, S>
|
||||
where
|
||||
T: 'static,
|
||||
S: Storage<ArcStoredValue<T>>,
|
||||
{
|
||||
type Value = T;
|
||||
|
||||
/// Returns a read guard to the stored data, or `None` if the owner of the reactive node has been disposed.
|
||||
#[track_caller]
|
||||
pub fn try_read_value(&self) -> Option<ReadGuard<T, Plain<T>>> {
|
||||
fn try_write_value(&self) -> Option<UntrackedWriteGuard<T>> {
|
||||
self.value
|
||||
.try_get_value()
|
||||
.and_then(|inner| Plain::try_new(inner).map(ReadGuard::new))
|
||||
}
|
||||
|
||||
/// Returns a read guard to the stored data.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This function panics when called after the owner of the reactive node has been disposed.
|
||||
/// See [`StoredValue::try_read_value`] for a version without panic.
|
||||
#[track_caller]
|
||||
pub fn read_value(&self) -> ReadGuard<T, Plain<T>> {
|
||||
self.try_read_value().unwrap_or_else(unwrap_signal!(self))
|
||||
}
|
||||
|
||||
/// Returns a write guard to the stored data, or `None` if the owner of the reactive node has been disposed.
|
||||
#[track_caller]
|
||||
pub fn try_write_value(&self) -> Option<UntrackedWriteGuard<T>> {
|
||||
self.value
|
||||
.try_get_value()
|
||||
.and_then(|inner| UntrackedWriteGuard::try_new(inner))
|
||||
}
|
||||
|
||||
/// Returns a write guard to the stored data.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This function panics when called after the owner of the reactive node has been disposed.
|
||||
/// See [`StoredValue::try_write_value`] for a version without panic.
|
||||
#[track_caller]
|
||||
pub fn write_value(&self) -> UntrackedWriteGuard<T> {
|
||||
self.try_write_value().unwrap_or_else(unwrap_signal!(self))
|
||||
}
|
||||
|
||||
/// Updates the current value by applying the given closure, returning the return value of the
|
||||
/// closure, or `None` if the value has already been disposed.
|
||||
pub fn try_update_value<U>(
|
||||
&self,
|
||||
fun: impl FnOnce(&mut T) -> U,
|
||||
) -> Option<U> {
|
||||
self.value
|
||||
.try_get_value()
|
||||
.map(|inner| fun(&mut *inner.write().or_poisoned()))
|
||||
}
|
||||
|
||||
/// Updates the value within [`StoredValue`] by applying a function to it.
|
||||
///
|
||||
/// # Panics
|
||||
/// This function panics when called after the owner of the reactive node has been disposed.
|
||||
/// See [`StoredValue::try_update_value`] for a version without panic.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use reactive_graph::owner::StoredValue; let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
///
|
||||
/// #[derive(Default)] // Does not implement Clone
|
||||
/// struct Data {
|
||||
/// rows: Vec<u8>,
|
||||
/// }
|
||||
///
|
||||
/// let data = StoredValue::new(Data::default());
|
||||
///
|
||||
/// // Easy to move into closures because StoredValue is Copy + 'static.
|
||||
/// // *NOTE* this is not the same thing as a derived signal!
|
||||
/// // *NOTE* this will not be automatically rerun as StoredValue is NOT reactive!
|
||||
/// let push_next = move || {
|
||||
/// data.update_value(|inner| match inner.rows.last().as_deref() {
|
||||
/// Some(n) => inner.rows.push(n + 1),
|
||||
/// None => inner.rows.push(0),
|
||||
/// })
|
||||
/// };
|
||||
///
|
||||
/// data.update_value(|inner| inner.rows = vec![5, 6, 7]);
|
||||
/// data.with_value(|inner| assert_eq!(inner.rows.last(), Some(&7)));
|
||||
///
|
||||
/// push_next();
|
||||
/// data.with_value(|inner| assert_eq!(inner.rows.last(), Some(&8)));
|
||||
///
|
||||
/// data.update_value(|inner| {
|
||||
/// std::mem::take(inner) // sets Data back to default
|
||||
/// });
|
||||
/// data.with_value(|inner| assert!(inner.rows.is_empty()));
|
||||
/// ```
|
||||
pub fn update_value<U>(&self, fun: impl FnOnce(&mut T) -> U) {
|
||||
self.try_update_value(fun);
|
||||
}
|
||||
|
||||
/// Sets the value within [`StoredValue`].
|
||||
///
|
||||
/// Returns [`Some`] containing the passed value if the owner of the reactive node has been
|
||||
/// disposed.
|
||||
///
|
||||
/// For types that do not implement [`Clone`], or in cases where allocating the entire object
|
||||
/// would be too expensive, prefer [`StoredValue::try_update_value`].
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use reactive_graph::owner::StoredValue; let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// # use reactive_graph::traits::Dispose;
|
||||
///
|
||||
/// let data = StoredValue::new(String::default());
|
||||
///
|
||||
/// // Easy to move into closures because StoredValue is Copy + 'static.
|
||||
/// // *NOTE* this is not the same thing as a derived signal!
|
||||
/// // *NOTE* this will not be automatically rerun as StoredValue is NOT reactive!
|
||||
/// let say_hello = move || {
|
||||
/// // Note that using the `update` methods would be more efficient here.
|
||||
/// data.try_set_value("Hello, World!".into())
|
||||
/// };
|
||||
/// // *NOTE* this is not the same thing as a derived signal!
|
||||
/// // *NOTE* this will not be automatically rerun as StoredValue is NOT reactive!
|
||||
/// let reset = move || {
|
||||
/// // Note that using the `update` methods would be more efficient here.
|
||||
/// data.try_set_value(Default::default())
|
||||
/// };
|
||||
/// assert_eq!(data.get_value(), "");
|
||||
///
|
||||
/// // None is returned because the value was able to be updated
|
||||
/// assert_eq!(say_hello(), None);
|
||||
///
|
||||
/// assert_eq!(data.get_value(), "Hello, World!");
|
||||
///
|
||||
/// reset();
|
||||
/// assert_eq!(data.get_value(), "");
|
||||
///
|
||||
/// // You should not call dispose yourself in normal user code.
|
||||
/// // This is shown here for the sake of the example.
|
||||
/// data.dispose();
|
||||
///
|
||||
/// // The reactive owner is disposed, so the value we intended to set is now
|
||||
/// // returned as some.
|
||||
/// assert_eq!(say_hello().as_deref(), Some("Hello, World!"));
|
||||
/// assert_eq!(reset().as_deref(), Some(""));
|
||||
/// ```
|
||||
pub fn try_set_value(&self, value: T) -> Option<T> {
|
||||
match self.value.try_get_value() {
|
||||
Some(inner) => {
|
||||
*inner.write().or_poisoned() = value;
|
||||
None
|
||||
}
|
||||
None => Some(value),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the value within [`StoredValue`].
|
||||
///
|
||||
/// For types that do not implement [`Clone`], or in cases where allocating the entire object
|
||||
/// would be too expensive, prefer [`StoredValue::update_value`].
|
||||
///
|
||||
/// # Panics
|
||||
/// This function panics when called after the owner of the reactive node has been disposed.
|
||||
/// See [`StoredValue::try_set_value`] for a version without panic.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use reactive_graph::owner::StoredValue; let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
///
|
||||
/// let data = StoredValue::new(10);
|
||||
///
|
||||
/// // Easy to move into closures because StoredValue is Copy + 'static.
|
||||
/// // *NOTE* this is not the same thing as a derived signal!
|
||||
/// // *NOTE* this will not be automatically rerun as StoredValue is NOT reactive!
|
||||
/// let maxout = move || data.set_value(u8::MAX);
|
||||
/// let zero = move || data.set_value(u8::MIN);
|
||||
///
|
||||
/// maxout();
|
||||
/// assert_eq!(data.get_value(), u8::MAX);
|
||||
///
|
||||
/// zero();
|
||||
/// assert_eq!(data.get_value(), u8::MIN);
|
||||
/// ```
|
||||
pub fn set_value(&self, value: T) {
|
||||
self.update_value(|n| *n = value);
|
||||
.and_then(|inner| inner.try_write_value())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -380,90 +156,39 @@ impl<T, S> IsDisposed for StoredValue<T, S> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S: Storage<Arc<RwLock<T>>>> StoredValue<T, S>
|
||||
where
|
||||
T: Clone + 'static,
|
||||
{
|
||||
/// Returns the value within [`StoredValue`] by cloning.
|
||||
///
|
||||
/// Returns [`Some`] containing the value if the owner of the reactive node has not been
|
||||
/// disposed. When disposed, returns [`None`].
|
||||
///
|
||||
/// See [`StoredValue::try_with_value`] for a version that avoids cloning. See
|
||||
/// [`StoredValue::get_value`] for a version that clones, but panics if the node is disposed.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use reactive_graph::owner::StoredValue; let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// # use reactive_graph::traits::Dispose;
|
||||
///
|
||||
/// // u8 is practically free to clone.
|
||||
/// let data: StoredValue<u8> = StoredValue::new(10);
|
||||
///
|
||||
/// // Larger data structures can become very expensive to clone.
|
||||
/// // You may prefer to use StoredValue::try_with_value.
|
||||
/// let _expensive: StoredValue<Vec<String>> = StoredValue::new(vec![]);
|
||||
///
|
||||
/// // Easy to move into closures because StoredValue is Copy + 'static
|
||||
/// let maxout = move || data.set_value(u8::MAX);
|
||||
/// let zero = move || data.set_value(u8::MIN);
|
||||
///
|
||||
/// maxout();
|
||||
/// assert_eq!(data.try_get_value(), Some(u8::MAX));
|
||||
///
|
||||
/// zero();
|
||||
/// assert_eq!(data.try_get_value(), Some(u8::MIN));
|
||||
///
|
||||
/// // You should not call dispose yourself in normal user code.
|
||||
/// // This is shown here for the sake of the example.
|
||||
/// data.dispose();
|
||||
///
|
||||
/// assert_eq!(data.try_get_value(), None);
|
||||
/// ```
|
||||
pub fn try_get_value(&self) -> Option<T> {
|
||||
self.try_with_value(T::clone)
|
||||
}
|
||||
|
||||
/// Returns the value within [`StoredValue`] by cloning.
|
||||
///
|
||||
/// See [`StoredValue::with_value`] for a version that avoids cloning.
|
||||
///
|
||||
/// # Panics
|
||||
/// This function panics when called after the owner of the reactive node has been disposed.
|
||||
/// See [`StoredValue::try_get_value`] for a version without panic.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use reactive_graph::owner::StoredValue; let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
///
|
||||
/// // u8 is practically free to clone.
|
||||
/// let data: StoredValue<u8> = StoredValue::new(10);
|
||||
///
|
||||
/// // Larger data structures can become very expensive to clone.
|
||||
/// // You may prefer to use StoredValue::try_with_value.
|
||||
/// let _expensive: StoredValue<Vec<String>> = StoredValue::new(vec![]);
|
||||
///
|
||||
/// // Easy to move into closures because StoredValue is Copy + 'static
|
||||
/// let maxout = move || data.set_value(u8::MAX);
|
||||
/// let zero = move || data.set_value(u8::MIN);
|
||||
///
|
||||
/// maxout();
|
||||
/// assert_eq!(data.get_value(), u8::MAX);
|
||||
///
|
||||
/// zero();
|
||||
/// assert_eq!(data.get_value(), u8::MIN);
|
||||
/// ```
|
||||
pub fn get_value(&self) -> T {
|
||||
self.with_value(T::clone)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> Dispose for StoredValue<T, S> {
|
||||
fn dispose(self) {
|
||||
self.value.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<ArcStoredValue<T>> for StoredValue<T>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(value: ArcStoredValue<T>) -> Self {
|
||||
StoredValue {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
value: ArenaItem::new(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> From<StoredValue<T, S>> for ArcStoredValue<T>
|
||||
where
|
||||
S: Storage<ArcStoredValue<T>>,
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(value: StoredValue<T, S>) -> Self {
|
||||
value
|
||||
.value
|
||||
.try_get_value()
|
||||
.unwrap_or_else(unwrap_signal!(value))
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new [`StoredValue`].
|
||||
#[inline(always)]
|
||||
#[track_caller]
|
||||
|
||||
@@ -52,7 +52,7 @@ use crate::{
|
||||
effect::Effect,
|
||||
graph::{Observer, Source, Subscriber, ToAnySource},
|
||||
owner::Owner,
|
||||
signal::{arc_signal, ArcReadSignal},
|
||||
signal::{arc_signal, guards::UntrackedWriteGuard, ArcReadSignal},
|
||||
};
|
||||
use any_spawner::Executor;
|
||||
use futures::{Stream, StreamExt};
|
||||
@@ -639,3 +639,189 @@ pub fn panic_getting_disposed_signal(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// A variation of the [`Read`] trait that provides a signposted "always-non-reactive" API.
|
||||
/// E.g. for [`StoredValue`](`crate::owner::StoredValue`).
|
||||
pub trait ReadValue: Sized + DefinedAt {
|
||||
/// The guard type that will be returned, which can be dereferenced to the value.
|
||||
type Value: Deref;
|
||||
|
||||
/// Returns the non-reactive guard, or `None` if the value has already been disposed.
|
||||
#[track_caller]
|
||||
fn try_read_value(&self) -> Option<Self::Value>;
|
||||
|
||||
/// Returns the non-reactive guard.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if you try to access a value that has been disposed.
|
||||
#[track_caller]
|
||||
fn read_value(&self) -> Self::Value {
|
||||
self.try_read_value().unwrap_or_else(unwrap_signal!(self))
|
||||
}
|
||||
}
|
||||
|
||||
/// A variation of the [`With`] trait that provides a signposted "always-non-reactive" API.
|
||||
/// E.g. for [`StoredValue`](`crate::owner::StoredValue`).
|
||||
pub trait WithValue: DefinedAt {
|
||||
/// The type of the value contained in the value.
|
||||
type Value: ?Sized;
|
||||
|
||||
/// Applies the closure to the value, non-reactively, and returns the result,
|
||||
/// or `None` if the value has already been disposed.
|
||||
#[track_caller]
|
||||
fn try_with_value<U>(
|
||||
&self,
|
||||
fun: impl FnOnce(&Self::Value) -> U,
|
||||
) -> Option<U>;
|
||||
|
||||
/// Applies the closure to the value, non-reactively, and returns the result.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if you try to access a value that has been disposed.
|
||||
#[track_caller]
|
||||
fn with_value<U>(&self, fun: impl FnOnce(&Self::Value) -> U) -> U {
|
||||
self.try_with_value(fun)
|
||||
.unwrap_or_else(unwrap_signal!(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> WithValue for T
|
||||
where
|
||||
T: DefinedAt + ReadValue,
|
||||
{
|
||||
type Value = <<Self as ReadValue>::Value as Deref>::Target;
|
||||
|
||||
fn try_with_value<U>(
|
||||
&self,
|
||||
fun: impl FnOnce(&Self::Value) -> U,
|
||||
) -> Option<U> {
|
||||
self.try_read_value().map(|value| fun(&value))
|
||||
}
|
||||
}
|
||||
|
||||
/// A variation of the [`Get`] trait that provides a signposted "always-non-reactive" API.
|
||||
/// E.g. for [`StoredValue`](`crate::owner::StoredValue`).
|
||||
pub trait GetValue: DefinedAt {
|
||||
/// The type of the value contained in the value.
|
||||
type Value: Clone;
|
||||
|
||||
/// Clones and returns the value of the value, non-reactively,
|
||||
/// or `None` if the value has already been disposed.
|
||||
#[track_caller]
|
||||
fn try_get_value(&self) -> Option<Self::Value>;
|
||||
|
||||
/// Clones and returns the value of the value, non-reactively.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if you try to access a value that has been disposed.
|
||||
#[track_caller]
|
||||
fn get_value(&self) -> Self::Value {
|
||||
self.try_get_value().unwrap_or_else(unwrap_signal!(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> GetValue for T
|
||||
where
|
||||
T: WithValue,
|
||||
T::Value: Clone,
|
||||
{
|
||||
type Value = <Self as WithValue>::Value;
|
||||
|
||||
fn try_get_value(&self) -> Option<Self::Value> {
|
||||
self.try_with_value(Self::Value::clone)
|
||||
}
|
||||
}
|
||||
|
||||
/// A variation of the [`Write`] trait that provides a signposted "always-non-reactive" API.
|
||||
/// E.g. for [`StoredValue`](`crate::owner::StoredValue`).
|
||||
pub trait WriteValue: Sized + DefinedAt {
|
||||
/// The type of the value's value.
|
||||
type Value: Sized + 'static;
|
||||
|
||||
/// Returns a non-reactive write guard, or `None` if the value has already been disposed.
|
||||
#[track_caller]
|
||||
fn try_write_value(&self) -> Option<UntrackedWriteGuard<Self::Value>>;
|
||||
|
||||
/// Returns a non-reactive write guard.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if you try to access a value that has been disposed.
|
||||
#[track_caller]
|
||||
fn write_value(&self) -> UntrackedWriteGuard<Self::Value> {
|
||||
self.try_write_value().unwrap_or_else(unwrap_signal!(self))
|
||||
}
|
||||
}
|
||||
|
||||
/// A variation of the [`Update`] trait that provides a signposted "always-non-reactive" API.
|
||||
/// E.g. for [`StoredValue`](`crate::owner::StoredValue`).
|
||||
pub trait UpdateValue: DefinedAt {
|
||||
/// The type of the value contained in the value.
|
||||
type Value;
|
||||
|
||||
/// Updates the value, returning the value that is
|
||||
/// returned by the update function, or `None` if the value has already been disposed.
|
||||
#[track_caller]
|
||||
fn try_update_value<U>(
|
||||
&self,
|
||||
fun: impl FnOnce(&mut Self::Value) -> U,
|
||||
) -> Option<U>;
|
||||
|
||||
/// Updates the value.
|
||||
#[track_caller]
|
||||
fn update_value(&self, fun: impl FnOnce(&mut Self::Value)) {
|
||||
self.try_update_value(fun);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> UpdateValue for T
|
||||
where
|
||||
T: WriteValue,
|
||||
{
|
||||
type Value = <Self as WriteValue>::Value;
|
||||
|
||||
#[track_caller]
|
||||
fn try_update_value<U>(
|
||||
&self,
|
||||
fun: impl FnOnce(&mut Self::Value) -> U,
|
||||
) -> Option<U> {
|
||||
let mut guard = self.try_write_value()?;
|
||||
Some(fun(&mut *guard))
|
||||
}
|
||||
}
|
||||
|
||||
/// A variation of the [`Set`] trait that provides a signposted "always-non-reactive" API.
|
||||
/// E.g. for [`StoredValue`](`crate::owner::StoredValue`).
|
||||
pub trait SetValue: DefinedAt {
|
||||
/// The type of the value contained in the value.
|
||||
type Value;
|
||||
|
||||
/// Updates the value by replacing it, non-reactively.
|
||||
///
|
||||
/// If the value has already been disposed, returns `Some(value)` with the value that was
|
||||
/// passed in. Otherwise, returns `None`.
|
||||
#[track_caller]
|
||||
fn try_set_value(&self, value: Self::Value) -> Option<Self::Value>;
|
||||
|
||||
/// Updates the value by replacing it, non-reactively.
|
||||
#[track_caller]
|
||||
fn set_value(&self, value: Self::Value) {
|
||||
self.try_set_value(value);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> SetValue for T
|
||||
where
|
||||
T: WriteValue,
|
||||
{
|
||||
type Value = <Self as WriteValue>::Value;
|
||||
|
||||
fn try_set_value(&self, value: Self::Value) -> Option<Self::Value> {
|
||||
// Unlike most other traits, for these None actually means success:
|
||||
if let Some(mut guard) = self.try_write_value() {
|
||||
*guard = value;
|
||||
None
|
||||
} else {
|
||||
Some(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,15 +3,30 @@
|
||||
/// Types that abstract over signals with values that can be read.
|
||||
pub mod read {
|
||||
use crate::{
|
||||
computed::{ArcMemo, Memo},
|
||||
computed::{ArcMemo, Memo, MemoInner},
|
||||
graph::untrack,
|
||||
owner::{ArenaItem, FromLocal, LocalStorage, Storage, SyncStorage},
|
||||
signal::{ArcReadSignal, ArcRwSignal, ReadSignal, RwSignal},
|
||||
traits::{DefinedAt, Dispose, Get, With, WithUntracked},
|
||||
owner::{
|
||||
ArcStoredValue, ArenaItem, FromLocal, LocalStorage, Storage,
|
||||
SyncStorage,
|
||||
},
|
||||
signal::{
|
||||
guards::{Mapped, Plain, ReadGuard},
|
||||
ArcReadSignal, ArcRwSignal, ReadSignal, RwSignal,
|
||||
},
|
||||
traits::{
|
||||
DefinedAt, Dispose, Get, Read, ReadUntracked, ReadValue, Track,
|
||||
With, WithValue,
|
||||
},
|
||||
unwrap_signal,
|
||||
};
|
||||
use send_wrapper::SendWrapper;
|
||||
use std::{panic::Location, sync::Arc};
|
||||
use std::{
|
||||
borrow::Borrow,
|
||||
fmt::Display,
|
||||
ops::Deref,
|
||||
panic::Location,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
/// Possibilities for the inner type of a [`Signal`].
|
||||
pub enum SignalTypes<T, S = SyncStorage>
|
||||
@@ -24,6 +39,8 @@ pub mod read {
|
||||
Memo(ArcMemo<T, S>),
|
||||
/// A derived signal.
|
||||
DerivedSignal(Arc<dyn Fn() -> T + Send + Sync>),
|
||||
/// A static, stored value.
|
||||
Stored(ArcStoredValue<T>),
|
||||
}
|
||||
|
||||
impl<T, S> Clone for SignalTypes<T, S>
|
||||
@@ -35,6 +52,7 @@ pub mod read {
|
||||
Self::ReadSignal(arg0) => Self::ReadSignal(arg0.clone()),
|
||||
Self::Memo(arg0) => Self::Memo(arg0.clone()),
|
||||
Self::DerivedSignal(arg0) => Self::DerivedSignal(arg0.clone()),
|
||||
Self::Stored(arg0) => Self::Stored(arg0.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,6 +70,9 @@ pub mod read {
|
||||
Self::DerivedSignal(_) => {
|
||||
f.debug_tuple("DerivedSignal").finish()
|
||||
}
|
||||
Self::Stored(arg0) => {
|
||||
f.debug_tuple("Static").field(arg0).finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,6 +189,39 @@ pub mod read {
|
||||
defined_at: std::panic::Location::caller(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves a static, nonreactive value into a signal, backed by [`ArcStoredValue`].
|
||||
#[track_caller]
|
||||
pub fn stored(value: T) -> Self {
|
||||
Self {
|
||||
inner: SignalTypes::Stored(ArcStoredValue::new(value)),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: std::panic::Location::caller(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> ArcSignal<T, S>
|
||||
where
|
||||
S: Storage<T>,
|
||||
{
|
||||
/// Subscribes to this signal in the current reactive scope without doing anything with its value.
|
||||
#[track_caller]
|
||||
pub fn track(&self) {
|
||||
match &self.inner {
|
||||
SignalTypes::ReadSignal(i) => {
|
||||
i.track();
|
||||
}
|
||||
SignalTypes::Memo(i) => {
|
||||
i.track();
|
||||
}
|
||||
SignalTypes::DerivedSignal(i) => {
|
||||
i();
|
||||
}
|
||||
// Doesn't change.
|
||||
SignalTypes::Stored(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for ArcSignal<T, SyncStorage>
|
||||
@@ -175,7 +229,7 @@ pub mod read {
|
||||
T: Default + Send + Sync + 'static,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::derive(|| Default::default())
|
||||
Self::stored(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,24 +285,6 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> WithUntracked for ArcSignal<T, S>
|
||||
where
|
||||
S: Storage<T>,
|
||||
{
|
||||
type Value = T;
|
||||
|
||||
fn try_with_untracked<U>(
|
||||
&self,
|
||||
fun: impl FnOnce(&Self::Value) -> U,
|
||||
) -> Option<U> {
|
||||
match &self.inner {
|
||||
SignalTypes::ReadSignal(i) => i.try_with_untracked(fun),
|
||||
SignalTypes::Memo(i) => i.try_with_untracked(fun),
|
||||
SignalTypes::DerivedSignal(i) => Some(untrack(|| fun(&i()))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> With for ArcSignal<T, S>
|
||||
where
|
||||
S: Storage<T>,
|
||||
@@ -264,10 +300,63 @@ pub mod read {
|
||||
SignalTypes::ReadSignal(i) => i.try_with(fun),
|
||||
SignalTypes::Memo(i) => i.try_with(fun),
|
||||
SignalTypes::DerivedSignal(i) => Some(fun(&i())),
|
||||
SignalTypes::Stored(i) => i.try_with_value(fun),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> ReadUntracked for ArcSignal<T, S>
|
||||
where
|
||||
S: Storage<T>,
|
||||
{
|
||||
type Value = ReadGuard<T, SignalReadGuard<T, S>>;
|
||||
|
||||
fn try_read_untracked(&self) -> Option<Self::Value> {
|
||||
match &self.inner {
|
||||
SignalTypes::ReadSignal(i) => {
|
||||
i.try_read_untracked().map(SignalReadGuard::Read)
|
||||
}
|
||||
SignalTypes::Memo(i) => {
|
||||
i.try_read_untracked().map(SignalReadGuard::Memo)
|
||||
}
|
||||
SignalTypes::DerivedSignal(i) => {
|
||||
Some(SignalReadGuard::Owned(untrack(|| i())))
|
||||
}
|
||||
SignalTypes::Stored(i) => {
|
||||
i.try_read_value().map(SignalReadGuard::Read)
|
||||
}
|
||||
}
|
||||
.map(ReadGuard::new)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> Read for ArcSignal<T, S>
|
||||
where
|
||||
S: Storage<T>,
|
||||
{
|
||||
type Value = ReadGuard<T, SignalReadGuard<T, S>>;
|
||||
|
||||
fn try_read(&self) -> Option<Self::Value> {
|
||||
match &self.inner {
|
||||
SignalTypes::ReadSignal(i) => {
|
||||
i.try_read().map(SignalReadGuard::Read)
|
||||
}
|
||||
SignalTypes::Memo(i) => i.try_read().map(SignalReadGuard::Memo),
|
||||
SignalTypes::DerivedSignal(i) => {
|
||||
Some(SignalReadGuard::Owned(i()))
|
||||
}
|
||||
SignalTypes::Stored(i) => {
|
||||
i.try_read_value().map(SignalReadGuard::Read)
|
||||
}
|
||||
}
|
||||
.map(ReadGuard::new)
|
||||
}
|
||||
|
||||
fn read(&self) -> Self::Value {
|
||||
self.try_read().unwrap_or_else(unwrap_signal!(self))
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper for any kind of arena-allocated reactive signal:
|
||||
/// an [`ReadSignal`], [`Memo`], [`RwSignal`], or derived signal closure.
|
||||
///
|
||||
@@ -343,31 +432,6 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> WithUntracked for Signal<T, S>
|
||||
where
|
||||
T: 'static,
|
||||
S: Storage<SignalTypes<T, S>> + Storage<T>,
|
||||
{
|
||||
type Value = T;
|
||||
|
||||
fn try_with_untracked<U>(
|
||||
&self,
|
||||
fun: impl FnOnce(&Self::Value) -> U,
|
||||
) -> Option<U> {
|
||||
self.inner
|
||||
// clone the inner Arc type and release the lock
|
||||
// prevents deadlocking if the derived value includes taking a lock on the arena
|
||||
.try_with_value(Clone::clone)
|
||||
.and_then(|inner| match &inner {
|
||||
SignalTypes::ReadSignal(i) => i.try_with_untracked(fun),
|
||||
SignalTypes::Memo(i) => i.try_with_untracked(fun),
|
||||
SignalTypes::DerivedSignal(i) => {
|
||||
Some(untrack(|| fun(&i())))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> With for Signal<T, S>
|
||||
where
|
||||
T: 'static,
|
||||
@@ -387,10 +451,79 @@ pub mod read {
|
||||
SignalTypes::ReadSignal(i) => i.try_with(fun),
|
||||
SignalTypes::Memo(i) => i.try_with(fun),
|
||||
SignalTypes::DerivedSignal(i) => Some(fun(&i())),
|
||||
SignalTypes::Stored(i) => i.try_with_value(fun),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> ReadUntracked for Signal<T, S>
|
||||
where
|
||||
T: 'static,
|
||||
S: Storage<SignalTypes<T, S>> + Storage<T>,
|
||||
{
|
||||
type Value = ReadGuard<T, SignalReadGuard<T, S>>;
|
||||
|
||||
fn try_read_untracked(&self) -> Option<Self::Value> {
|
||||
self.inner
|
||||
// clone the inner Arc type and release the lock
|
||||
// prevents deadlocking if the derived value includes taking a lock on the arena
|
||||
.try_with_value(Clone::clone)
|
||||
.and_then(|inner| {
|
||||
match &inner {
|
||||
SignalTypes::ReadSignal(i) => {
|
||||
i.try_read_untracked().map(SignalReadGuard::Read)
|
||||
}
|
||||
SignalTypes::Memo(i) => {
|
||||
i.try_read_untracked().map(SignalReadGuard::Memo)
|
||||
}
|
||||
SignalTypes::DerivedSignal(i) => {
|
||||
Some(SignalReadGuard::Owned(untrack(|| i())))
|
||||
}
|
||||
SignalTypes::Stored(i) => {
|
||||
i.try_read_value().map(SignalReadGuard::Read)
|
||||
}
|
||||
}
|
||||
.map(ReadGuard::new)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> Read for Signal<T, S>
|
||||
where
|
||||
T: 'static,
|
||||
S: Storage<SignalTypes<T, S>> + Storage<T>,
|
||||
{
|
||||
type Value = ReadGuard<T, SignalReadGuard<T, S>>;
|
||||
|
||||
fn try_read(&self) -> Option<Self::Value> {
|
||||
self.inner
|
||||
// clone the inner Arc type and release the lock
|
||||
// prevents deadlocking if the derived value includes taking a lock on the arena
|
||||
.try_with_value(Clone::clone)
|
||||
.and_then(|inner| {
|
||||
match &inner {
|
||||
SignalTypes::ReadSignal(i) => {
|
||||
i.try_read().map(SignalReadGuard::Read)
|
||||
}
|
||||
SignalTypes::Memo(i) => {
|
||||
i.try_read().map(SignalReadGuard::Memo)
|
||||
}
|
||||
SignalTypes::DerivedSignal(i) => {
|
||||
Some(SignalReadGuard::Owned(i()))
|
||||
}
|
||||
SignalTypes::Stored(i) => {
|
||||
i.try_read_value().map(SignalReadGuard::Read)
|
||||
}
|
||||
}
|
||||
.map(ReadGuard::new)
|
||||
})
|
||||
}
|
||||
|
||||
fn read(&self) -> Self::Value {
|
||||
self.try_read().unwrap_or_else(unwrap_signal!(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Signal<T>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
@@ -433,6 +566,18 @@ pub mod read {
|
||||
defined_at: std::panic::Location::caller(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves a static, nonreactive value into a signal, backed by [`ArcStoredValue`].
|
||||
#[track_caller]
|
||||
pub fn stored(value: T) -> Self {
|
||||
Self {
|
||||
inner: ArenaItem::new_with_storage(SignalTypes::Stored(
|
||||
ArcStoredValue::new(value),
|
||||
)),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: std::panic::Location::caller(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Signal<T, LocalStorage>
|
||||
@@ -460,6 +605,49 @@ pub mod read {
|
||||
defined_at: std::panic::Location::caller(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves a static, nonreactive value into a signal, backed by [`ArcStoredValue`].
|
||||
/// Works like [`Signal::stored`] but uses [`LocalStorage`].
|
||||
#[track_caller]
|
||||
pub fn stored_local(value: T) -> Self {
|
||||
Self {
|
||||
inner: ArenaItem::new_local(SignalTypes::Stored(
|
||||
ArcStoredValue::new(value),
|
||||
)),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: std::panic::Location::caller(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> Signal<T, S>
|
||||
where
|
||||
T: 'static,
|
||||
S: Storage<SignalTypes<T, S>> + Storage<T>,
|
||||
{
|
||||
/// Subscribes to this signal in the current reactive scope without doing anything with its value.
|
||||
#[track_caller]
|
||||
pub fn track(&self) {
|
||||
let inner = self
|
||||
.inner
|
||||
// clone the inner Arc type and release the lock
|
||||
// prevents deadlocking if the derived value includes taking a lock on the arena
|
||||
.try_with_value(Clone::clone)
|
||||
.unwrap_or_else(unwrap_signal!(self));
|
||||
match inner {
|
||||
SignalTypes::ReadSignal(i) => {
|
||||
i.track();
|
||||
}
|
||||
SignalTypes::Memo(i) => {
|
||||
i.track();
|
||||
}
|
||||
SignalTypes::DerivedSignal(i) => {
|
||||
i();
|
||||
}
|
||||
// Doesn't change.
|
||||
SignalTypes::Stored(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for Signal<T>
|
||||
@@ -467,7 +655,7 @@ pub mod read {
|
||||
T: Send + Sync + Default + 'static,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::derive(|| Default::default())
|
||||
Self::stored(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -476,34 +664,34 @@ pub mod read {
|
||||
T: Default + 'static,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::derive_local(|| Default::default())
|
||||
Self::stored_local(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone + Send + Sync + 'static> From<T> for ArcSignal<T, SyncStorage> {
|
||||
impl<T: Send + Sync + 'static> From<T> for ArcSignal<T, SyncStorage> {
|
||||
#[track_caller]
|
||||
fn from(value: T) -> Self {
|
||||
Self::derive(move || value.clone())
|
||||
ArcSignal::stored(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for Signal<T>
|
||||
where
|
||||
T: Clone + Send + Sync + 'static,
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(value: T) -> Self {
|
||||
Self::derive(move || value.clone())
|
||||
Self::stored(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for Signal<T, LocalStorage>
|
||||
where
|
||||
T: Clone + 'static,
|
||||
T: 'static,
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(value: T) -> Self {
|
||||
Self::derive_local(move || value.clone())
|
||||
Self::stored_local(value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -715,23 +903,6 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> WithUntracked for MaybeSignal<T, S>
|
||||
where
|
||||
S: Storage<SignalTypes<T, S>> + Storage<T>,
|
||||
{
|
||||
type Value = T;
|
||||
|
||||
fn try_with_untracked<U>(
|
||||
&self,
|
||||
fun: impl FnOnce(&Self::Value) -> U,
|
||||
) -> Option<U> {
|
||||
match self {
|
||||
Self::Static(t) => Some(fun(t)),
|
||||
Self::Dynamic(s) => s.try_with_untracked(fun),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> With for MaybeSignal<T, S>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
@@ -750,6 +921,44 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> ReadUntracked for MaybeSignal<T, S>
|
||||
where
|
||||
T: Clone,
|
||||
S: Storage<SignalTypes<T, S>> + Storage<T>,
|
||||
{
|
||||
type Value = ReadGuard<T, SignalReadGuard<T, S>>;
|
||||
|
||||
fn try_read_untracked(&self) -> Option<Self::Value> {
|
||||
match self {
|
||||
Self::Static(t) => {
|
||||
Some(ReadGuard::new(SignalReadGuard::Owned(t.clone())))
|
||||
}
|
||||
Self::Dynamic(s) => s.try_read_untracked(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> Read for MaybeSignal<T, S>
|
||||
where
|
||||
T: Clone,
|
||||
S: Storage<SignalTypes<T, S>> + Storage<T>,
|
||||
{
|
||||
type Value = ReadGuard<T, SignalReadGuard<T, S>>;
|
||||
|
||||
fn try_read(&self) -> Option<Self::Value> {
|
||||
match self {
|
||||
Self::Static(t) => {
|
||||
Some(ReadGuard::new(SignalReadGuard::Owned(t.clone())))
|
||||
}
|
||||
Self::Dynamic(s) => s.try_read(),
|
||||
}
|
||||
}
|
||||
|
||||
fn read(&self) -> Self::Value {
|
||||
self.try_read().unwrap_or_else(unwrap_signal!(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> MaybeSignal<T>
|
||||
where
|
||||
T: Send + Sync,
|
||||
@@ -771,6 +980,21 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> MaybeSignal<T, S>
|
||||
where
|
||||
T: 'static,
|
||||
S: Storage<SignalTypes<T, S>> + Storage<T>,
|
||||
{
|
||||
/// Subscribes to this signal in the current reactive scope without doing anything with its value.
|
||||
#[track_caller]
|
||||
pub fn track(&self) {
|
||||
match self {
|
||||
Self::Static(_) => {}
|
||||
Self::Dynamic(signal) => signal.track(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for MaybeSignal<T, SyncStorage>
|
||||
where
|
||||
SyncStorage: Storage<T>,
|
||||
@@ -893,7 +1117,7 @@ pub mod read {
|
||||
|
||||
impl<S> From<&str> for MaybeSignal<String, S>
|
||||
where
|
||||
S: Storage<String>,
|
||||
S: Storage<String> + Storage<Arc<RwLock<String>>>,
|
||||
{
|
||||
fn from(value: &str) -> Self {
|
||||
Self::Static(value.to_string())
|
||||
@@ -968,23 +1192,6 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> WithUntracked for MaybeProp<T, S>
|
||||
where
|
||||
S: Storage<SignalTypes<Option<T>, S>> + Storage<Option<T>>,
|
||||
{
|
||||
type Value = Option<T>;
|
||||
|
||||
fn try_with_untracked<U>(
|
||||
&self,
|
||||
fun: impl FnOnce(&Self::Value) -> U,
|
||||
) -> Option<U> {
|
||||
match &self.0 {
|
||||
None => Some(fun(&None)),
|
||||
Some(inner) => inner.try_with_untracked(fun),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> With for MaybeProp<T, S>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
@@ -1003,6 +1210,40 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> ReadUntracked for MaybeProp<T, S>
|
||||
where
|
||||
T: Clone,
|
||||
S: Storage<SignalTypes<Option<T>, S>> + Storage<Option<T>>,
|
||||
{
|
||||
type Value = ReadGuard<Option<T>, SignalReadGuard<Option<T>, S>>;
|
||||
|
||||
fn try_read_untracked(&self) -> Option<Self::Value> {
|
||||
match &self.0 {
|
||||
None => Some(ReadGuard::new(SignalReadGuard::Owned(None))),
|
||||
Some(inner) => inner.try_read_untracked(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> Read for MaybeProp<T, S>
|
||||
where
|
||||
T: Clone,
|
||||
S: Storage<SignalTypes<Option<T>, S>> + Storage<Option<T>>,
|
||||
{
|
||||
type Value = ReadGuard<Option<T>, SignalReadGuard<Option<T>, S>>;
|
||||
|
||||
fn try_read(&self) -> Option<Self::Value> {
|
||||
match &self.0 {
|
||||
None => Some(ReadGuard::new(SignalReadGuard::Owned(None))),
|
||||
Some(inner) => inner.try_read(),
|
||||
}
|
||||
}
|
||||
|
||||
fn read(&self) -> Self::Value {
|
||||
self.try_read().unwrap_or_else(unwrap_signal!(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> MaybeProp<T>
|
||||
where
|
||||
T: Send + Sync,
|
||||
@@ -1016,6 +1257,21 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> MaybeProp<T, S>
|
||||
where
|
||||
T: 'static,
|
||||
S: Storage<SignalTypes<Option<T>, S>> + Storage<Option<T>>,
|
||||
{
|
||||
/// Subscribes to this signal in the current reactive scope without doing anything with its value.
|
||||
#[track_caller]
|
||||
pub fn track(&self) {
|
||||
match &self.0 {
|
||||
None => {}
|
||||
Some(signal) => signal.track(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for MaybeProp<T>
|
||||
where
|
||||
SyncStorage: Storage<Option<T>>,
|
||||
@@ -1242,6 +1498,76 @@ pub mod read {
|
||||
Self(Some(MaybeSignal::from_local(Some(value.to_string()))))
|
||||
}
|
||||
}
|
||||
|
||||
/// The content of a [`Signal`] wrapper read guard, variable depending on the signal type.
|
||||
#[derive(Debug)]
|
||||
pub enum SignalReadGuard<T: 'static, S: Storage<T>> {
|
||||
/// A read signal guard.
|
||||
Read(ReadGuard<T, Plain<T>>),
|
||||
/// A memo guard.
|
||||
Memo(ReadGuard<T, Mapped<Plain<MemoInner<T, S>>, T>>),
|
||||
/// A fake guard for derived signals, the content had to actually be cloned, so it's not a guard but we pretend it is.
|
||||
Owned(T),
|
||||
}
|
||||
|
||||
impl<T, S> Clone for SignalReadGuard<T, S>
|
||||
where
|
||||
S: Storage<T>,
|
||||
T: Clone,
|
||||
Plain<T>: Clone,
|
||||
Mapped<Plain<MemoInner<T, S>>, T>: Clone,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
SignalReadGuard::Read(i) => SignalReadGuard::Read(i.clone()),
|
||||
SignalReadGuard::Memo(i) => SignalReadGuard::Memo(i.clone()),
|
||||
SignalReadGuard::Owned(i) => SignalReadGuard::Owned(i.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> Deref for SignalReadGuard<T, S>
|
||||
where
|
||||
S: Storage<T>,
|
||||
{
|
||||
type Target = T;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
match self {
|
||||
SignalReadGuard::Read(i) => i,
|
||||
SignalReadGuard::Memo(i) => i,
|
||||
SignalReadGuard::Owned(i) => i,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> Borrow<T> for SignalReadGuard<T, S>
|
||||
where
|
||||
S: Storage<T>,
|
||||
{
|
||||
fn borrow(&self) -> &T {
|
||||
self.deref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> PartialEq<T> for SignalReadGuard<T, S>
|
||||
where
|
||||
S: Storage<T>,
|
||||
T: PartialEq,
|
||||
{
|
||||
fn eq(&self, other: &T) -> bool {
|
||||
self.deref() == other
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> Display for SignalReadGuard<T, S>
|
||||
where
|
||||
S: Storage<T>,
|
||||
T: Display,
|
||||
{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
Display::fmt(&**self, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Types that abstract over the ability to update a signal.
|
||||
|
||||
88
reactive_graph/tests/cleanup.rs
Normal file
88
reactive_graph/tests/cleanup.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
use reactive_graph::{
|
||||
computed::Memo,
|
||||
owner::{on_cleanup, Owner},
|
||||
signal::{RwSignal, Trigger},
|
||||
traits::{Dispose, GetUntracked, Track},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[test]
|
||||
fn cleanup_on_dispose() {
|
||||
let owner = Owner::new();
|
||||
owner.set();
|
||||
|
||||
struct ExecuteOnDrop(Option<Box<dyn FnOnce() + Send + Sync>>);
|
||||
|
||||
impl ExecuteOnDrop {
|
||||
fn new(f: impl FnOnce() + Send + Sync + 'static) -> Self {
|
||||
Self(Some(Box::new(f)))
|
||||
}
|
||||
}
|
||||
impl Drop for ExecuteOnDrop {
|
||||
fn drop(&mut self) {
|
||||
self.0.take().unwrap()();
|
||||
}
|
||||
}
|
||||
|
||||
let trigger = Trigger::new();
|
||||
|
||||
println!("STARTING");
|
||||
|
||||
let memo = Memo::new(move |_| {
|
||||
trigger.track();
|
||||
|
||||
// An example of why you might want to do this is that
|
||||
// when something goes out of reactive scope you want it to be cleaned up.
|
||||
// The cleaning up might have side effects, and those side effects might cause
|
||||
// re-renders where new `on_cleanup` are registered.
|
||||
let on_drop = ExecuteOnDrop::new(|| {
|
||||
on_cleanup(|| println!("Nested cleanup in progress."))
|
||||
});
|
||||
|
||||
on_cleanup(move || {
|
||||
println!("Cleanup in progress.");
|
||||
drop(on_drop)
|
||||
});
|
||||
});
|
||||
println!("Memo 1: {:?}", memo);
|
||||
memo.get_untracked(); // First cleanup registered.
|
||||
|
||||
memo.dispose(); // Cleanup not run here.
|
||||
|
||||
println!("Cleanup should have been executed.");
|
||||
|
||||
let memo = Memo::new(move |_| {
|
||||
// New cleanup registered. It'll panic here.
|
||||
on_cleanup(move || println!("Test passed."));
|
||||
});
|
||||
println!("Memo 2: {:?}", memo);
|
||||
println!("^ Note how the memos have the same key (different versions).");
|
||||
memo.get_untracked(); // First cleanup registered.
|
||||
|
||||
println!("Test passed.");
|
||||
|
||||
memo.dispose();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leak_on_dispose() {
|
||||
let owner = Owner::new();
|
||||
owner.set();
|
||||
|
||||
let trigger = Trigger::new();
|
||||
|
||||
let value = Arc::new(());
|
||||
let weak = Arc::downgrade(&value);
|
||||
|
||||
let memo = Memo::new(move |_| {
|
||||
trigger.track();
|
||||
|
||||
RwSignal::new(value.clone());
|
||||
});
|
||||
|
||||
memo.get_untracked();
|
||||
|
||||
memo.dispose();
|
||||
|
||||
assert!(weak.upgrade().is_none()); // Should have been dropped.
|
||||
}
|
||||
@@ -171,12 +171,26 @@ impl KeyMap {
|
||||
where
|
||||
K: Debug + Hash + PartialEq + Eq + Send + Sync + 'static,
|
||||
{
|
||||
// this incredibly defensive mechanism takes the guard twice
|
||||
// on initialization. unfortunately, this is because `initialize`, on
|
||||
// a nested keyed field can, when being initialized), can in fact try
|
||||
// to take the lock again, as we try to insert the keys of the parent
|
||||
// while inserting the keys on this child.
|
||||
//
|
||||
// see here https://github.com/leptos-rs/leptos/issues/3086
|
||||
let mut guard = self.0.write().or_poisoned();
|
||||
let entry = guard
|
||||
.entry(path)
|
||||
.or_insert_with(|| Box::new(FieldKeys::new(initialize())));
|
||||
let entry = entry.downcast_mut::<FieldKeys<K>>()?;
|
||||
Some(fun(entry))
|
||||
if guard.contains_key(&path) {
|
||||
let entry = guard.get_mut(&path)?;
|
||||
let entry = entry.downcast_mut::<FieldKeys<K>>()?;
|
||||
Some(fun(entry))
|
||||
} else {
|
||||
drop(guard);
|
||||
let keys = Box::new(FieldKeys::new(initialize()));
|
||||
let mut guard = self.0.write().or_poisoned();
|
||||
let entry = guard.entry(path).or_insert(keys);
|
||||
let entry = entry.downcast_mut::<FieldKeys<K>>()?;
|
||||
Some(fun(entry))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ use reactive_graph::{
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fmt::{Debug, Display},
|
||||
mem,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
@@ -47,7 +48,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
#[component(transparent)]
|
||||
pub fn Router<Chil>(
|
||||
/// The base URL for the router. Defaults to `""`.
|
||||
#[prop(optional, into)]
|
||||
@@ -101,6 +102,7 @@ where
|
||||
location,
|
||||
state,
|
||||
set_is_routing,
|
||||
query_mutations: Default::default(),
|
||||
});
|
||||
|
||||
let children = children.into_inner();
|
||||
@@ -114,6 +116,8 @@ pub(crate) struct RouterContext {
|
||||
pub location: Location,
|
||||
pub state: ArcRwSignal<State>,
|
||||
pub set_is_routing: Option<SignalSetter<bool>>,
|
||||
pub query_mutations:
|
||||
ArcStoredValue<Vec<(Oco<'static, str>, Option<String>)>>,
|
||||
}
|
||||
|
||||
impl RouterContext {
|
||||
@@ -130,7 +134,7 @@ impl RouterContext {
|
||||
resolve_path("", path, None)
|
||||
};
|
||||
|
||||
let url = match resolved_to.map(|to| BrowserUrl::parse(&to)) {
|
||||
let mut url = match resolved_to.map(|to| BrowserUrl::parse(&to)) {
|
||||
Some(Ok(url)) => url,
|
||||
Some(Err(e)) => {
|
||||
leptos::logging::error!("Error parsing URL: {e:?}");
|
||||
@@ -141,6 +145,22 @@ impl RouterContext {
|
||||
return;
|
||||
}
|
||||
};
|
||||
let query_mutations =
|
||||
mem::take(&mut *self.query_mutations.write_value());
|
||||
if !query_mutations.is_empty() {
|
||||
for (key, value) in query_mutations {
|
||||
if let Some(value) = value {
|
||||
url.search_params_mut().replace(key, value);
|
||||
} else {
|
||||
url.search_params_mut().remove(&key);
|
||||
}
|
||||
}
|
||||
*url.search_mut() = url
|
||||
.search_params()
|
||||
.to_query_string()
|
||||
.trim_start_matches('?')
|
||||
.into()
|
||||
}
|
||||
|
||||
if url.origin() != current.origin() {
|
||||
window().location().set_href(path).unwrap();
|
||||
@@ -153,13 +173,14 @@ impl RouterContext {
|
||||
}
|
||||
|
||||
// update URL signal, if necessary
|
||||
let value = url.to_full_path();
|
||||
if current != url {
|
||||
drop(current);
|
||||
self.current_url.set(url);
|
||||
}
|
||||
|
||||
BrowserUrl::complete_navigation(&LocationChange {
|
||||
value: path.to_string(),
|
||||
value,
|
||||
replace: options.replace,
|
||||
scroll: options.scroll,
|
||||
state: options.state,
|
||||
@@ -204,7 +225,7 @@ where
|
||||
}
|
||||
}*/
|
||||
|
||||
#[component]
|
||||
#[component(transparent)]
|
||||
pub fn Routes<Defs, FallbackFn, Fallback>(
|
||||
fallback: FallbackFn,
|
||||
children: RouteChildren<Defs>,
|
||||
@@ -247,7 +268,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
#[component(transparent)]
|
||||
pub fn FlatRoutes<Defs, FallbackFn, Fallback>(
|
||||
fallback: FallbackFn,
|
||||
children: RouteChildren<Defs>,
|
||||
@@ -294,7 +315,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
#[component(transparent)]
|
||||
pub fn Route<Segments, View>(
|
||||
path: Segments,
|
||||
view: View,
|
||||
@@ -306,7 +327,7 @@ where
|
||||
NestedRoute::new(path, view).ssr_mode(ssr)
|
||||
}
|
||||
|
||||
#[component]
|
||||
#[component(transparent)]
|
||||
pub fn ParentRoute<Segments, View, Children>(
|
||||
path: Segments,
|
||||
view: View,
|
||||
@@ -320,7 +341,7 @@ where
|
||||
NestedRoute::new(path, view).ssr_mode(ssr).child(children)
|
||||
}
|
||||
|
||||
#[component]
|
||||
#[component(transparent)]
|
||||
pub fn ProtectedRoute<Segments, ViewFn, View, C, PathFn, P>(
|
||||
path: Segments,
|
||||
view: ViewFn,
|
||||
@@ -362,7 +383,7 @@ where
|
||||
NestedRoute::new(path, view).ssr_mode(ssr)
|
||||
}
|
||||
|
||||
#[component]
|
||||
#[component(transparent)]
|
||||
pub fn ProtectedParentRoute<Segments, ViewFn, View, C, PathFn, P, Children>(
|
||||
path: Segments,
|
||||
view: ViewFn,
|
||||
@@ -424,7 +445,7 @@ where
|
||||
///
|
||||
/// [`leptos_actix`]: <https://docs.rs/leptos_actix/>
|
||||
/// [`leptos_axum`]: <https://docs.rs/leptos_axum/>
|
||||
#[component]
|
||||
#[component(transparent)]
|
||||
pub fn Redirect<P>(
|
||||
/// The relative path to which the user should be redirected.
|
||||
path: P,
|
||||
|
||||
@@ -4,15 +4,18 @@ use crate::{
|
||||
navigate::NavigateOptions,
|
||||
params::{Params, ParamsError, ParamsMap},
|
||||
};
|
||||
use leptos::oco::Oco;
|
||||
use leptos::{leptos_dom::helpers::request_animation_frame, oco::Oco};
|
||||
use reactive_graph::{
|
||||
computed::{ArcMemo, Memo},
|
||||
owner::use_context,
|
||||
owner::{expect_context, use_context},
|
||||
signal::{ArcRwSignal, ReadSignal},
|
||||
traits::{Get, GetUntracked, With},
|
||||
traits::{Get, GetUntracked, ReadUntracked, With, WriteValue},
|
||||
wrappers::write::SignalSetter,
|
||||
};
|
||||
use std::str::FromStr;
|
||||
use std::{
|
||||
str::FromStr,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
|
||||
#[track_caller]
|
||||
#[deprecated = "This has been renamed to `query_signal` to match Rust naming \
|
||||
@@ -93,10 +96,15 @@ pub fn query_signal_with_options<T>(
|
||||
where
|
||||
T: FromStr + ToString + PartialEq + Send + Sync,
|
||||
{
|
||||
static IS_NAVIGATING: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
let mut key: Oco<'static, str> = key.into();
|
||||
let query_map = use_query_map();
|
||||
let navigate = use_navigate();
|
||||
let location = use_location();
|
||||
let RouterContext {
|
||||
query_mutations, ..
|
||||
} = expect_context();
|
||||
|
||||
let get = Memo::new({
|
||||
let key = key.clone_inplace();
|
||||
@@ -108,20 +116,25 @@ where
|
||||
});
|
||||
|
||||
let set = SignalSetter::map(move |value: Option<T>| {
|
||||
let mut new_query_map = query_map.get();
|
||||
match value {
|
||||
Some(value) => {
|
||||
new_query_map.insert(key.to_string(), value.to_string());
|
||||
}
|
||||
None => {
|
||||
new_query_map.remove(&key);
|
||||
}
|
||||
}
|
||||
let qs = new_query_map.to_query_string();
|
||||
let path = location.pathname.get_untracked();
|
||||
let hash = location.hash.get_untracked();
|
||||
let qs = location.query.read_untracked().to_query_string();
|
||||
let new_url = format!("{path}{qs}{hash}");
|
||||
navigate(&new_url, nav_options.clone());
|
||||
query_mutations
|
||||
.write_value()
|
||||
.push((key.clone(), value.as_ref().map(ToString::to_string)));
|
||||
|
||||
if !IS_NAVIGATING.load(Ordering::Relaxed) {
|
||||
IS_NAVIGATING.store(true, Ordering::Relaxed);
|
||||
request_animation_frame({
|
||||
let navigate = navigate.clone();
|
||||
let nav_options = nav_options.clone();
|
||||
move || {
|
||||
navigate(&new_url, nav_options.clone());
|
||||
IS_NAVIGATING.store(false, Ordering::Relaxed)
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
(get, set)
|
||||
|
||||
@@ -121,7 +121,7 @@ impl LocationProvider for BrowserUrl {
|
||||
&& curr.path() == new_url.path()
|
||||
};
|
||||
|
||||
url.set(new_url);
|
||||
url.set(new_url.clone());
|
||||
if same_path {
|
||||
Self::complete_navigation(&loc);
|
||||
}
|
||||
@@ -130,12 +130,19 @@ impl LocationProvider for BrowserUrl {
|
||||
if !same_path {
|
||||
*pending.lock().or_poisoned() = Some(tx);
|
||||
}
|
||||
let url = url.clone();
|
||||
async move {
|
||||
if !same_path {
|
||||
// if it has been canceled, ignore
|
||||
// otherwise, complete navigation -- i.e., set URL in address bar
|
||||
if rx.await.is_ok() {
|
||||
Self::complete_navigation(&loc);
|
||||
// only update the URL in the browser if this is still the current URL
|
||||
// if we've navigated to another page in the meantime, don't update the
|
||||
// browser URL
|
||||
let curr = url.read_untracked();
|
||||
if curr == new_url {
|
||||
Self::complete_navigation(&loc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,22 +36,42 @@ impl Url {
|
||||
&self.origin
|
||||
}
|
||||
|
||||
pub fn origin_mut(&mut self) -> &mut String {
|
||||
&mut self.origin
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &str {
|
||||
&self.path
|
||||
}
|
||||
|
||||
pub fn path_mut(&mut self) -> &mut str {
|
||||
&mut self.path
|
||||
}
|
||||
|
||||
pub fn search(&self) -> &str {
|
||||
&self.search
|
||||
}
|
||||
|
||||
pub fn search_mut(&mut self) -> &mut String {
|
||||
&mut self.search
|
||||
}
|
||||
|
||||
pub fn search_params(&self) -> &ParamsMap {
|
||||
&self.search_params
|
||||
}
|
||||
|
||||
pub fn search_params_mut(&mut self) -> &mut ParamsMap {
|
||||
&mut self.search_params
|
||||
}
|
||||
|
||||
pub fn hash(&self) -> &str {
|
||||
&self.hash
|
||||
}
|
||||
|
||||
pub fn hash_mut(&mut self) -> &mut String {
|
||||
&mut self.hash
|
||||
}
|
||||
|
||||
pub fn provide_server_action_error(&self) {
|
||||
let search_params = self.search_params();
|
||||
if let (Some(err), Some(path)) = (
|
||||
@@ -62,6 +82,19 @@ impl Url {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn to_full_path(&self) -> String {
|
||||
let mut path = self.path.to_string();
|
||||
if !self.search.is_empty() {
|
||||
path.push('?');
|
||||
path.push_str(&self.search);
|
||||
}
|
||||
if !self.hash.is_empty() {
|
||||
path.push('#');
|
||||
path.push_str(&self.hash);
|
||||
}
|
||||
path
|
||||
}
|
||||
|
||||
pub fn escape(s: &str) -> String {
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
{
|
||||
|
||||
@@ -21,6 +21,9 @@ impl ParamsMap {
|
||||
}
|
||||
|
||||
/// Inserts a value into the map.
|
||||
///
|
||||
/// If a value with that key already exists, the new value will be added to it.
|
||||
/// To replace the value instead, see [`replace`].
|
||||
pub fn insert(&mut self, key: impl Into<Cow<'static, str>>, value: String) {
|
||||
let value = unescape(&value);
|
||||
|
||||
@@ -32,6 +35,23 @@ impl ParamsMap {
|
||||
}
|
||||
}
|
||||
|
||||
/// Inserts a value into the map, replacing any existing value for that key.
|
||||
pub fn replace(
|
||||
&mut self,
|
||||
key: impl Into<Cow<'static, str>>,
|
||||
value: String,
|
||||
) {
|
||||
let value = unescape(&value);
|
||||
|
||||
let key = key.into();
|
||||
if let Some(prev) = self.0.iter_mut().find(|(k, _)| k == &key) {
|
||||
prev.1.clear();
|
||||
prev.1.push(value);
|
||||
} else {
|
||||
self.0.push((key, vec![value]));
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the most-recently-added value of this param from the map.
|
||||
pub fn get(&self, key: &str) -> Option<String> {
|
||||
self.get_str(key).map(ToOwned::to_owned)
|
||||
|
||||
@@ -67,107 +67,121 @@ where
|
||||
|
||||
let value = Box::new(self) as Box<dyn Any + Send>;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
let to_html = |value: Box<dyn Any>,
|
||||
buf: &mut String,
|
||||
class: &mut String,
|
||||
style: &mut String,
|
||||
inner_html: &mut String| {
|
||||
let value = value
|
||||
.downcast::<T>()
|
||||
.expect("AnyAttribute::to_html could not be downcast");
|
||||
value.to_html(buf, class, style, inner_html);
|
||||
};
|
||||
let build = |value: Box<dyn Any>,
|
||||
match value.downcast::<AnyAttribute>() {
|
||||
// if it's already an AnyAttribute, we don't need to double-wrap it
|
||||
Ok(any_attribute) => *any_attribute,
|
||||
Err(value) => {
|
||||
#[cfg(feature = "ssr")]
|
||||
let to_html =
|
||||
|value: Box<dyn Any>,
|
||||
buf: &mut String,
|
||||
class: &mut String,
|
||||
style: &mut String,
|
||||
inner_html: &mut String| {
|
||||
let value = value.downcast::<T>().expect(
|
||||
"AnyAttribute::to_html could not be downcast",
|
||||
);
|
||||
value.to_html(buf, class, style, inner_html);
|
||||
};
|
||||
let build =
|
||||
|value: Box<dyn Any>,
|
||||
el: &crate::renderer::types::Element| {
|
||||
let value = value
|
||||
.downcast::<T>()
|
||||
.expect("AnyAttribute::build couldn't downcast");
|
||||
let state = Box::new(value.build(el));
|
||||
let value = value
|
||||
.downcast::<T>()
|
||||
.expect("AnyAttribute::build couldn't downcast");
|
||||
let state = Box::new(value.build(el));
|
||||
|
||||
AnyAttributeState {
|
||||
type_id: TypeId::of::<T>(),
|
||||
state,
|
||||
el: el.clone(),
|
||||
}
|
||||
};
|
||||
#[cfg(feature = "hydrate")]
|
||||
let hydrate_from_server =
|
||||
|value: Box<dyn Any>, el: &crate::renderer::types::Element| {
|
||||
let value = value.downcast::<T>().expect(
|
||||
"AnyAttribute::hydrate_from_server couldn't downcast",
|
||||
);
|
||||
let state = Box::new(value.hydrate::<true>(el));
|
||||
AnyAttributeState {
|
||||
type_id: TypeId::of::<T>(),
|
||||
state,
|
||||
el: el.clone(),
|
||||
}
|
||||
};
|
||||
#[cfg(feature = "hydrate")]
|
||||
let hydrate_from_server =
|
||||
|value: Box<dyn Any>,
|
||||
el: &crate::renderer::types::Element| {
|
||||
let value = value.downcast::<T>().expect(
|
||||
"AnyAttribute::hydrate_from_server couldn't \
|
||||
downcast",
|
||||
);
|
||||
let state = Box::new(value.hydrate::<true>(el));
|
||||
|
||||
AnyAttributeState {
|
||||
AnyAttributeState {
|
||||
type_id: TypeId::of::<T>(),
|
||||
state,
|
||||
el: el.clone(),
|
||||
}
|
||||
};
|
||||
#[cfg(feature = "hydrate")]
|
||||
let hydrate_from_template =
|
||||
|value: Box<dyn Any>,
|
||||
el: &crate::renderer::types::Element| {
|
||||
let value = value.downcast::<T>().expect(
|
||||
"AnyAttribute::hydrate_from_server couldn't \
|
||||
downcast",
|
||||
);
|
||||
let state = Box::new(value.hydrate::<true>(el));
|
||||
|
||||
AnyAttributeState {
|
||||
type_id: TypeId::of::<T>(),
|
||||
state,
|
||||
el: el.clone(),
|
||||
}
|
||||
};
|
||||
let rebuild =
|
||||
|new_type_id: TypeId,
|
||||
value: Box<dyn Any>,
|
||||
state: &mut AnyAttributeState| {
|
||||
let value = value.downcast::<T>().expect(
|
||||
"AnyAttribute::rebuild couldn't downcast value",
|
||||
);
|
||||
if new_type_id == state.type_id {
|
||||
let state = state.state.downcast_mut().expect(
|
||||
"AnyAttribute::rebuild couldn't downcast state",
|
||||
);
|
||||
value.rebuild(state);
|
||||
} else {
|
||||
let new = value.into_any_attr().build(&state.el);
|
||||
*state = new;
|
||||
}
|
||||
};
|
||||
#[cfg(feature = "ssr")]
|
||||
let dry_resolve = |value: &mut Box<dyn Any + Send>| {
|
||||
let value = value
|
||||
.downcast_mut::<T>()
|
||||
.expect("AnyView::resolve could not be downcast");
|
||||
value.dry_resolve();
|
||||
};
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
let resolve = |value: Box<dyn Any>| {
|
||||
let value = value
|
||||
.downcast::<T>()
|
||||
.expect("AnyView::resolve could not be downcast");
|
||||
Box::pin(
|
||||
async move { value.resolve().await.into_any_attr() },
|
||||
)
|
||||
as Pin<Box<dyn Future<Output = AnyAttribute> + Send>>
|
||||
};
|
||||
AnyAttribute {
|
||||
type_id: TypeId::of::<T>(),
|
||||
state,
|
||||
el: el.clone(),
|
||||
html_len,
|
||||
value,
|
||||
#[cfg(feature = "ssr")]
|
||||
to_html,
|
||||
build,
|
||||
rebuild,
|
||||
#[cfg(feature = "hydrate")]
|
||||
hydrate_from_server,
|
||||
#[cfg(feature = "hydrate")]
|
||||
hydrate_from_template,
|
||||
#[cfg(feature = "ssr")]
|
||||
resolve,
|
||||
#[cfg(feature = "ssr")]
|
||||
dry_resolve,
|
||||
}
|
||||
};
|
||||
#[cfg(feature = "hydrate")]
|
||||
let hydrate_from_template =
|
||||
|value: Box<dyn Any>, el: &crate::renderer::types::Element| {
|
||||
let value = value.downcast::<T>().expect(
|
||||
"AnyAttribute::hydrate_from_server couldn't downcast",
|
||||
);
|
||||
let state = Box::new(value.hydrate::<true>(el));
|
||||
|
||||
AnyAttributeState {
|
||||
type_id: TypeId::of::<T>(),
|
||||
state,
|
||||
el: el.clone(),
|
||||
}
|
||||
};
|
||||
let rebuild = |new_type_id: TypeId,
|
||||
value: Box<dyn Any>,
|
||||
state: &mut AnyAttributeState| {
|
||||
let value = value
|
||||
.downcast::<T>()
|
||||
.expect("AnyAttribute::rebuild couldn't downcast value");
|
||||
if new_type_id == state.type_id {
|
||||
let state = state
|
||||
.state
|
||||
.downcast_mut()
|
||||
.expect("AnyAttribute::rebuild couldn't downcast state");
|
||||
value.rebuild(state);
|
||||
} else {
|
||||
let new = value.into_any_attr().build(&state.el);
|
||||
*state = new;
|
||||
}
|
||||
};
|
||||
#[cfg(feature = "ssr")]
|
||||
let dry_resolve = |value: &mut Box<dyn Any + Send>| {
|
||||
let value = value
|
||||
.downcast_mut::<T>()
|
||||
.expect("AnyView::resolve could not be downcast");
|
||||
value.dry_resolve();
|
||||
};
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
let resolve = |value: Box<dyn Any>| {
|
||||
let value = value
|
||||
.downcast::<T>()
|
||||
.expect("AnyView::resolve could not be downcast");
|
||||
Box::pin(async move { value.resolve().await.into_any_attr() })
|
||||
as Pin<Box<dyn Future<Output = AnyAttribute> + Send>>
|
||||
};
|
||||
AnyAttribute {
|
||||
type_id: TypeId::of::<T>(),
|
||||
html_len,
|
||||
value,
|
||||
#[cfg(feature = "ssr")]
|
||||
to_html,
|
||||
build,
|
||||
rebuild,
|
||||
#[cfg(feature = "hydrate")]
|
||||
hydrate_from_server,
|
||||
#[cfg(feature = "hydrate")]
|
||||
hydrate_from_template,
|
||||
#[cfg(feature = "ssr")]
|
||||
resolve,
|
||||
#[cfg(feature = "ssr")]
|
||||
dry_resolve,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,26 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
/// Declares that this type can be converted into some other type, which is a valid attribute value.
|
||||
pub trait IntoAttributeValue {
|
||||
/// The attribute value into which this type can be converted.
|
||||
type Output;
|
||||
|
||||
/// Consumes this value, transforming it into an attribute value.
|
||||
fn into_attribute_value(self) -> Self::Output;
|
||||
}
|
||||
|
||||
impl<T> IntoAttributeValue for T
|
||||
where
|
||||
T: AttributeValue,
|
||||
{
|
||||
type Output = Self;
|
||||
|
||||
fn into_attribute_value(self) -> Self::Output {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A possible value for an HTML attribute.
|
||||
pub trait AttributeValue: Send {
|
||||
/// The state that should be retained between building and rebuilding.
|
||||
|
||||
@@ -4,8 +4,8 @@ use crate::{
|
||||
renderer::{CastFrom, Rndr},
|
||||
ssr::StreamBuilder,
|
||||
view::{
|
||||
add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
|
||||
RenderHtml, ToTemplate,
|
||||
add_attr::AddAnyAttr, IntoRender, Mountable, Position, PositionState,
|
||||
Render, RenderHtml, ToTemplate,
|
||||
},
|
||||
};
|
||||
use const_str_slice_concat::{
|
||||
@@ -65,11 +65,13 @@ impl<E, At, Ch, NewChild> ElementChild<NewChild> for HtmlElement<E, At, Ch>
|
||||
where
|
||||
E: ElementWithChildren,
|
||||
Ch: Render + NextTuple,
|
||||
<Ch as NextTuple>::Output<NewChild>: Render,
|
||||
<Ch as NextTuple>::Output<NewChild::Output>: Render,
|
||||
|
||||
NewChild: Render,
|
||||
NewChild: IntoRender,
|
||||
NewChild::Output: Render,
|
||||
{
|
||||
type Output = HtmlElement<E, At, <Ch as NextTuple>::Output<NewChild>>;
|
||||
type Output =
|
||||
HtmlElement<E, At, <Ch as NextTuple>::Output<NewChild::Output>>;
|
||||
|
||||
fn child(self, child: NewChild) -> Self::Output {
|
||||
let HtmlElement {
|
||||
@@ -82,7 +84,7 @@ where
|
||||
tag,
|
||||
|
||||
attributes,
|
||||
children: children.next_tuple(child),
|
||||
children: children.next_tuple(child.into_render()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,7 +118,7 @@ where
|
||||
/// Adds a child to the element.
|
||||
pub trait ElementChild<NewChild>
|
||||
where
|
||||
NewChild: Render,
|
||||
NewChild: IntoRender,
|
||||
{
|
||||
/// The type of the element, with the child added.
|
||||
type Output;
|
||||
|
||||
@@ -19,6 +19,7 @@ pub mod prelude {
|
||||
OnAttribute, OnTargetAttribute, PropAttribute,
|
||||
StyleAttribute,
|
||||
},
|
||||
IntoAttributeValue,
|
||||
},
|
||||
directive::DirectiveAttribute,
|
||||
element::{ElementChild, ElementExt, InnerHtmlAttribute},
|
||||
@@ -26,8 +27,8 @@ pub mod prelude {
|
||||
},
|
||||
renderer::{dom::Dom, Renderer},
|
||||
view::{
|
||||
add_attr::AddAnyAttr, any_view::IntoAny, Mountable, Render,
|
||||
RenderHtml,
|
||||
add_attr::AddAnyAttr, any_view::IntoAny, IntoRender, Mountable,
|
||||
Render, RenderHtml,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ macro_rules! svg_elements {
|
||||
($($tag:ident [$($attr:ty),*]),* $(,)?) => {
|
||||
paste::paste! {
|
||||
$(
|
||||
/// An SVG attribute.
|
||||
/// An SVG element.
|
||||
// `tag()` function
|
||||
#[allow(non_snake_case)]
|
||||
pub fn $tag() -> HtmlElement<[<$tag:camel>], (), ()>
|
||||
@@ -151,4 +151,33 @@ svg_elements![
|
||||
view [],
|
||||
];
|
||||
|
||||
// TODO <use>
|
||||
/// An SVG element.
|
||||
#[allow(non_snake_case)]
|
||||
pub fn r#use() -> HtmlElement<Use, (), ()>
|
||||
where {
|
||||
HtmlElement {
|
||||
tag: Use,
|
||||
attributes: (),
|
||||
children: (),
|
||||
}
|
||||
}
|
||||
|
||||
/// An SVG element.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub struct Use;
|
||||
|
||||
impl ElementType for Use {
|
||||
type Output = web_sys::SvgElement;
|
||||
|
||||
const TAG: &'static str = "use";
|
||||
const SELF_CLOSING: bool = false;
|
||||
const ESCAPE_CHILDREN: bool = true;
|
||||
const NAMESPACE: Option<&'static str> = Some("http://www.w3.org/2000/svg");
|
||||
|
||||
#[inline(always)]
|
||||
fn tag(&self) -> &str {
|
||||
Self::TAG
|
||||
}
|
||||
}
|
||||
|
||||
impl ElementWithChildren for Use {}
|
||||
|
||||
@@ -27,7 +27,8 @@ use std::{future::Future, pin::Pin};
|
||||
pub struct AnyView {
|
||||
type_id: TypeId,
|
||||
value: Box<dyn Any + Send>,
|
||||
|
||||
build: fn(Box<dyn Any>) -> AnyViewState,
|
||||
rebuild: fn(TypeId, Box<dyn Any>, &mut AnyViewState),
|
||||
// The fields below are cfg-gated so they will not be included in WASM bundles if not needed.
|
||||
// Ordinarily, the compiler can simply omit this dead code because the methods are not called.
|
||||
// With this type-erased wrapper, however, the compiler is not *always* able to correctly
|
||||
@@ -42,8 +43,6 @@ pub struct AnyView {
|
||||
#[cfg(feature = "ssr")]
|
||||
to_html_async_ooo:
|
||||
fn(Box<dyn Any>, &mut StreamBuilder, &mut Position, bool, bool),
|
||||
build: fn(Box<dyn Any>) -> AnyViewState,
|
||||
rebuild: fn(TypeId, Box<dyn Any>, &mut AnyViewState),
|
||||
#[cfg(feature = "ssr")]
|
||||
#[allow(clippy::type_complexity)]
|
||||
resolve: fn(Box<dyn Any>) -> Pin<Box<dyn Future<Output = AnyView> + Send>>,
|
||||
@@ -138,156 +137,172 @@ where
|
||||
|
||||
let value = Box::new(self) as Box<dyn Any + Send>;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
let dry_resolve = |value: &mut Box<dyn Any + Send>| {
|
||||
let value = value
|
||||
.downcast_mut::<T>()
|
||||
.expect("AnyView::resolve could not be downcast");
|
||||
value.dry_resolve();
|
||||
};
|
||||
match value.downcast::<AnyView>() {
|
||||
// if it's already an AnyView, we don't need to double-wrap it
|
||||
Ok(any_view) => *any_view,
|
||||
Err(value) => {
|
||||
#[cfg(feature = "ssr")]
|
||||
let dry_resolve = |value: &mut Box<dyn Any + Send>| {
|
||||
let value = value
|
||||
.downcast_mut::<T>()
|
||||
.expect("AnyView::resolve could not be downcast");
|
||||
value.dry_resolve();
|
||||
};
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
let resolve = |value: Box<dyn Any>| {
|
||||
let value = value
|
||||
.downcast::<T>()
|
||||
.expect("AnyView::resolve could not be downcast");
|
||||
Box::pin(async move { value.resolve().await.into_any() })
|
||||
as Pin<Box<dyn Future<Output = AnyView> + Send>>
|
||||
};
|
||||
#[cfg(feature = "ssr")]
|
||||
let to_html = |value: Box<dyn Any>,
|
||||
buf: &mut String,
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool| {
|
||||
let type_id = mark_branches
|
||||
.then(|| format!("{:?}", TypeId::of::<T>()))
|
||||
.unwrap_or_default();
|
||||
let value = value
|
||||
.downcast::<T>()
|
||||
.expect("AnyView::to_html could not be downcast");
|
||||
if mark_branches {
|
||||
buf.open_branch(&type_id);
|
||||
}
|
||||
value.to_html_with_buf(buf, position, escape, mark_branches);
|
||||
if mark_branches {
|
||||
buf.close_branch(&type_id);
|
||||
}
|
||||
};
|
||||
#[cfg(feature = "ssr")]
|
||||
let to_html_async = |value: Box<dyn Any>,
|
||||
buf: &mut StreamBuilder,
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool| {
|
||||
let type_id = mark_branches
|
||||
.then(|| format!("{:?}", TypeId::of::<T>()))
|
||||
.unwrap_or_default();
|
||||
let value = value
|
||||
.downcast::<T>()
|
||||
.expect("AnyView::to_html could not be downcast");
|
||||
if mark_branches {
|
||||
buf.open_branch(&type_id);
|
||||
}
|
||||
value.to_html_async_with_buf::<false>(
|
||||
buf,
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
);
|
||||
if mark_branches {
|
||||
buf.close_branch(&type_id);
|
||||
}
|
||||
};
|
||||
#[cfg(feature = "ssr")]
|
||||
let to_html_async_ooo =
|
||||
|value: Box<dyn Any>,
|
||||
buf: &mut StreamBuilder,
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool| {
|
||||
let value = value
|
||||
.downcast::<T>()
|
||||
.expect("AnyView::to_html could not be downcast");
|
||||
value.to_html_async_with_buf::<true>(
|
||||
buf,
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
);
|
||||
};
|
||||
let build = |value: Box<dyn Any>| {
|
||||
let value = value
|
||||
.downcast::<T>()
|
||||
.expect("AnyView::build couldn't downcast");
|
||||
let state = Box::new(value.build());
|
||||
#[cfg(feature = "ssr")]
|
||||
let resolve = |value: Box<dyn Any>| {
|
||||
let value = value
|
||||
.downcast::<T>()
|
||||
.expect("AnyView::resolve could not be downcast");
|
||||
Box::pin(async move { value.resolve().await.into_any() })
|
||||
as Pin<Box<dyn Future<Output = AnyView> + Send>>
|
||||
};
|
||||
#[cfg(feature = "ssr")]
|
||||
let to_html =
|
||||
|value: Box<dyn Any>,
|
||||
buf: &mut String,
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool| {
|
||||
let type_id = mark_branches
|
||||
.then(|| format!("{:?}", TypeId::of::<T>()))
|
||||
.unwrap_or_default();
|
||||
let value = value
|
||||
.downcast::<T>()
|
||||
.expect("AnyView::to_html could not be downcast");
|
||||
if mark_branches {
|
||||
buf.open_branch(&type_id);
|
||||
}
|
||||
value.to_html_with_buf(
|
||||
buf,
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
);
|
||||
if mark_branches {
|
||||
buf.close_branch(&type_id);
|
||||
}
|
||||
};
|
||||
#[cfg(feature = "ssr")]
|
||||
let to_html_async =
|
||||
|value: Box<dyn Any>,
|
||||
buf: &mut StreamBuilder,
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool| {
|
||||
let type_id = mark_branches
|
||||
.then(|| format!("{:?}", TypeId::of::<T>()))
|
||||
.unwrap_or_default();
|
||||
let value = value
|
||||
.downcast::<T>()
|
||||
.expect("AnyView::to_html could not be downcast");
|
||||
if mark_branches {
|
||||
buf.open_branch(&type_id);
|
||||
}
|
||||
value.to_html_async_with_buf::<false>(
|
||||
buf,
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
);
|
||||
if mark_branches {
|
||||
buf.close_branch(&type_id);
|
||||
}
|
||||
};
|
||||
#[cfg(feature = "ssr")]
|
||||
let to_html_async_ooo =
|
||||
|value: Box<dyn Any>,
|
||||
buf: &mut StreamBuilder,
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool| {
|
||||
let value = value
|
||||
.downcast::<T>()
|
||||
.expect("AnyView::to_html could not be downcast");
|
||||
value.to_html_async_with_buf::<true>(
|
||||
buf,
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
);
|
||||
};
|
||||
let build = |value: Box<dyn Any>| {
|
||||
let value = value
|
||||
.downcast::<T>()
|
||||
.expect("AnyView::build couldn't downcast");
|
||||
let state = Box::new(value.build());
|
||||
|
||||
AnyViewState {
|
||||
type_id: TypeId::of::<T>(),
|
||||
state,
|
||||
AnyViewState {
|
||||
type_id: TypeId::of::<T>(),
|
||||
state,
|
||||
|
||||
mount: mount_any::<T>,
|
||||
unmount: unmount_any::<T>,
|
||||
insert_before_this: insert_before_this::<T>,
|
||||
}
|
||||
};
|
||||
#[cfg(feature = "hydrate")]
|
||||
let hydrate_from_server =
|
||||
|value: Box<dyn Any>, cursor: &Cursor, position: &PositionState| {
|
||||
let value = value
|
||||
.downcast::<T>()
|
||||
.expect("AnyView::hydrate_from_server couldn't downcast");
|
||||
let state = Box::new(value.hydrate::<true>(cursor, position));
|
||||
mount: mount_any::<T>,
|
||||
unmount: unmount_any::<T>,
|
||||
insert_before_this: insert_before_this::<T>,
|
||||
}
|
||||
};
|
||||
#[cfg(feature = "hydrate")]
|
||||
let hydrate_from_server =
|
||||
|value: Box<dyn Any>,
|
||||
cursor: &Cursor,
|
||||
position: &PositionState| {
|
||||
let value = value.downcast::<T>().expect(
|
||||
"AnyView::hydrate_from_server couldn't downcast",
|
||||
);
|
||||
let state =
|
||||
Box::new(value.hydrate::<true>(cursor, position));
|
||||
|
||||
AnyViewState {
|
||||
AnyViewState {
|
||||
type_id: TypeId::of::<T>(),
|
||||
state,
|
||||
|
||||
mount: mount_any::<T>,
|
||||
unmount: unmount_any::<T>,
|
||||
insert_before_this: insert_before_this::<T>,
|
||||
}
|
||||
};
|
||||
|
||||
let rebuild =
|
||||
|new_type_id: TypeId,
|
||||
value: Box<dyn Any>,
|
||||
state: &mut AnyViewState| {
|
||||
let value = value
|
||||
.downcast::<T>()
|
||||
.expect("AnyView::rebuild couldn't downcast value");
|
||||
if new_type_id == state.type_id {
|
||||
let state = state.state.downcast_mut().expect(
|
||||
"AnyView::rebuild couldn't downcast state",
|
||||
);
|
||||
value.rebuild(state);
|
||||
} else {
|
||||
let mut new = value.into_any().build();
|
||||
state.insert_before_this(&mut new);
|
||||
state.unmount();
|
||||
*state = new;
|
||||
}
|
||||
};
|
||||
|
||||
AnyView {
|
||||
type_id: TypeId::of::<T>(),
|
||||
state,
|
||||
|
||||
mount: mount_any::<T>,
|
||||
unmount: unmount_any::<T>,
|
||||
insert_before_this: insert_before_this::<T>,
|
||||
value,
|
||||
build,
|
||||
rebuild,
|
||||
#[cfg(feature = "ssr")]
|
||||
resolve,
|
||||
#[cfg(feature = "ssr")]
|
||||
dry_resolve,
|
||||
#[cfg(feature = "ssr")]
|
||||
html_len,
|
||||
#[cfg(feature = "ssr")]
|
||||
to_html,
|
||||
#[cfg(feature = "ssr")]
|
||||
to_html_async,
|
||||
#[cfg(feature = "ssr")]
|
||||
to_html_async_ooo,
|
||||
#[cfg(feature = "hydrate")]
|
||||
hydrate_from_server,
|
||||
}
|
||||
};
|
||||
|
||||
let rebuild = |new_type_id: TypeId,
|
||||
value: Box<dyn Any>,
|
||||
state: &mut AnyViewState| {
|
||||
let value = value
|
||||
.downcast::<T>()
|
||||
.expect("AnyView::rebuild couldn't downcast value");
|
||||
if new_type_id == state.type_id {
|
||||
let state = state
|
||||
.state
|
||||
.downcast_mut()
|
||||
.expect("AnyView::rebuild couldn't downcast state");
|
||||
value.rebuild(state);
|
||||
} else {
|
||||
let mut new = value.into_any().build();
|
||||
state.insert_before_this(&mut new);
|
||||
state.unmount();
|
||||
*state = new;
|
||||
}
|
||||
};
|
||||
|
||||
AnyView {
|
||||
type_id: TypeId::of::<T>(),
|
||||
value,
|
||||
build,
|
||||
rebuild,
|
||||
#[cfg(feature = "ssr")]
|
||||
resolve,
|
||||
#[cfg(feature = "ssr")]
|
||||
dry_resolve,
|
||||
#[cfg(feature = "ssr")]
|
||||
html_len,
|
||||
#[cfg(feature = "ssr")]
|
||||
to_html,
|
||||
#[cfg(feature = "ssr")]
|
||||
to_html_async,
|
||||
#[cfg(feature = "ssr")]
|
||||
to_html_async_ooo,
|
||||
#[cfg(feature = "hydrate")]
|
||||
hydrate_from_server,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -314,7 +329,7 @@ impl AddAnyAttr for AnyView {
|
||||
where
|
||||
Self::Output<NewAttr>: RenderHtml,
|
||||
{
|
||||
todo!()
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -432,3 +432,23 @@ pub enum Position {
|
||||
/// This is the last child of its parent.
|
||||
LastChild,
|
||||
}
|
||||
|
||||
/// Declares that this type can be converted into some other type, which can be renderered.
|
||||
pub trait IntoRender {
|
||||
/// The renderable type into which this type can be converted.
|
||||
type Output;
|
||||
|
||||
/// Consumes this value, transforming it into the renderable type.
|
||||
fn into_render(self) -> Self::Output;
|
||||
}
|
||||
|
||||
impl<T> IntoRender for T
|
||||
where
|
||||
T: Render,
|
||||
{
|
||||
type Output = Self;
|
||||
|
||||
fn into_render(self) -> Self::Output {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user