From a02c230299fcd58d0d45071614f6d2b560e74590 Mon Sep 17 00:00:00 2001
From: Pete Hunt <239742+petehunt@users.noreply.github.com>
Date: Sun, 2 Aug 2026 12:53:17 -0400
Subject: [PATCH 1/2] feat(view): render component trees concurrently
---
Cargo.lock | 1 +
crates/topcoat-view/Cargo.toml | 1 +
.../grammar/src/view/component.rs | 54 ++-
.../grammar/src/view/view_writer.rs | 336 ++++++++++++++----
crates/topcoat-view/macro/docs/view.md | 4 +
crates/topcoat-view/macro/tests/component.rs | 132 +++++++
crates/topcoat-view/src/lib.rs | 3 +
crates/topcoat-view/src/view.rs | 19 +-
crates/topcoat-view/src/view_tree.rs | 97 +++++
9 files changed, 560 insertions(+), 87 deletions(-)
create mode 100644 crates/topcoat-view/src/view_tree.rs
diff --git a/Cargo.lock b/Cargo.lock
index 2ae13e609..4af78b91e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3194,6 +3194,7 @@ name = "topcoat-view"
version = "0.5.0"
dependencies = [
"criterion",
+ "futures-util",
"http",
"itoa",
"memchr",
diff --git a/crates/topcoat-view/Cargo.toml b/crates/topcoat-view/Cargo.toml
index 9df451824..fbd5c4ac1 100644
--- a/crates/topcoat-view/Cargo.toml
+++ b/crates/topcoat-view/Cargo.toml
@@ -18,6 +18,7 @@ http = [
[dependencies]
topcoat-core.workspace = true
+futures-util.workspace = true
http = { workspace = true, optional = true }
itoa.workspace = true
memchr.workspace = true
diff --git a/crates/topcoat-view/grammar/src/view/component.rs b/crates/topcoat-view/grammar/src/view/component.rs
index ad06fa849..28a350b2b 100644
--- a/crates/topcoat-view/grammar/src/view/component.rs
+++ b/crates/topcoat-view/grammar/src/view/component.rs
@@ -8,11 +8,14 @@ use syn::{
spanned::Spanned,
token::Paren,
};
-use topcoat_core_grammar::{ParseOption, paths::topcoat_view};
+use topcoat_core_grammar::{
+ ParseOption,
+ paths::{topcoat_error, topcoat_view},
+};
use crate::{
template::RuntimeExpr,
- view::{ExprKind, Nodes, ViewWriter, WriteView},
+ view::{Nodes, ViewWriter, WriteView},
};
/// A component invocation, written as `path(name: value, ..., child_node child_node ...)`.
@@ -84,29 +87,46 @@ impl WriteView for Component {
for child in &self.children {
child.write(&mut child_writer);
}
- let child = child_writer.into_token_stream();
- quote_spanned! {self.paren_token.span.span()=>
- .child(#child)
- }
+ child_writer.into_token_stream()
});
- writer.write_expr(
- ExprKind::View,
+ let component = if let Some(child) = child {
quote_spanned! {self.paren_token.span.span()=>
- {
+ async {
use #topcoat_view::Component;
- let props = #name::props_builder()#(#setters)*#child.build();
+ let __child_slot = __ViewSlot::new();
+ let props = #name::props_builder()
+ #(#setters)*
+ .child(__child_slot.view())
+ .build();
+ let __child = async {
+ __child_slot.fill(#child);
+ ::core::result::Result::<(), #topcoat_error::Error>::Ok(())
+ };
// The marker is built via `Default` so the same construction
// works for both unit-struct and generic (`PhantomData`) markers.
#[allow(clippy::default_constructed_unit_structs)]
- Component::render(
- #name::default(),
- __cx,
- props,
- ).await?
+ let __component = Component::render(#name::default(), __cx, props);
+ let (__child_result, __component_result) =
+ __join(__child, __component).await;
+ __child_result?;
+ __component_result
}
- },
- );
+ }
+ } else {
+ quote_spanned! {self.paren_token.span.span()=>
+ async {
+ use #topcoat_view::Component;
+ let props = #name::props_builder()#(#setters)*.build();
+ // The marker is built via `Default` so the same construction
+ // works for both unit-struct and generic (`PhantomData`) markers.
+ #[allow(clippy::default_constructed_unit_structs)]
+ Component::render(#name::default(), __cx, props).await
+ }
+ }
+ };
+
+ writer.write_component(component);
}
}
diff --git a/crates/topcoat-view/grammar/src/view/view_writer.rs b/crates/topcoat-view/grammar/src/view/view_writer.rs
index f497d720d..281b3ba09 100644
--- a/crates/topcoat-view/grammar/src/view/view_writer.rs
+++ b/crates/topcoat-view/grammar/src/view/view_writer.rs
@@ -70,6 +70,11 @@ impl ViewWriter {
self.chunks.push(Chunk::Expr { kind, tokens });
}
+ pub fn write_component(&mut self, tokens: TokenStream) {
+ self.flush();
+ self.chunks.push(Chunk::Component { tokens });
+ }
+
pub fn local_binding(&mut self, pat: &Pat, expr: &Expr) {
self.flush();
self.chunks.push(Chunk::Local {
@@ -130,72 +135,8 @@ impl ViewWriter {
&& let Chunk::Static { string } = &self.chunks[0]
{
quote! { #topcoat_view::View::unescaped_unchecked(#string) }
- } else {
- fn build_parts(chunks: &[Chunk]) -> TokenStream {
- let mut output = TokenStream::new();
- for chunk in chunks {
- match chunk {
- Chunk::Static { string } => {
- let helper = ExprKind::Unescaped.helper();
- let tokens = quote! { #string };
- quote! { #helper(__cx, &mut __parts, #tokens); }
- }
- Chunk::Expr { kind, tokens } => {
- let helper = kind.helper();
- quote! { #helper(__cx, &mut __parts, #tokens); }
- }
- Chunk::Local { pat, expr } => {
- quote! { let #pat = #expr; }
- }
- Chunk::Statement { tokens } => {
- quote! { #tokens }
- }
- Chunk::If {
- expr,
- then_branch: then,
- else_branch: r#else,
- } => {
- let then_branch = build_parts(&then.chunks);
- let else_branch = build_parts(&r#else.chunks);
- let else_branch = (!r#else.chunks.is_empty())
- .then(|| quote! { else { #else_branch } });
- quote! {
- if #expr {
- #then_branch
- }
- #else_branch
- }
- }
- Chunk::For { pat, expr, body } => {
- let body = build_parts(&body.chunks);
- quote! {
- for #pat in #expr {
- #body
- }
- }
- }
- Chunk::Match { expr, arms } => {
- let arm_tokens = arms.iter().map(|arm| {
- let pat = &arm.pat;
- let guard = arm.guard.as_ref().map(|g| quote! { if #g });
- let body = build_parts(&arm.body.chunks);
- quote! {
- #pat #guard => { #body }
- }
- });
- quote! {
- match #expr {
- #(#arm_tokens,)*
- }
- }
- }
- }
- .to_tokens(&mut output);
- }
- output
- }
-
- let statements = build_parts(&self.chunks);
+ } else if !contains_component(&self.chunks) {
+ let statements = build_ready_parts(&self.chunks);
quote! {{
use #topcoat_view::internal::*;
@@ -203,6 +144,13 @@ impl ViewWriter {
#statements
#topcoat_view::View::new(__parts)
}}
+ } else {
+ let future = build_tree_future(&self.chunks);
+ return if self.nested {
+ quote! { (#future).await? }
+ } else {
+ quote! { (#future).await }
+ };
}
};
@@ -214,6 +162,234 @@ impl ViewWriter {
}
}
+fn contains_component(chunks: &[Chunk]) -> bool {
+ chunks.iter().any(|chunk| match chunk {
+ Chunk::Component { .. } => true,
+ Chunk::For { body, .. } => contains_component(&body.chunks),
+ Chunk::If {
+ then_branch,
+ else_branch,
+ ..
+ } => contains_component(&then_branch.chunks) || contains_component(&else_branch.chunks),
+ Chunk::Match { arms, .. } => arms.iter().any(|arm| contains_component(&arm.body.chunks)),
+ Chunk::Static { .. }
+ | Chunk::Expr { .. }
+ | Chunk::Local { .. }
+ | Chunk::Statement { .. } => false,
+ })
+}
+
+fn build_tree_future(chunks: &[Chunk]) -> TokenStream {
+ // Declare locals before the tree whose futures may borrow them.
+ let prologue_len = chunks
+ .iter()
+ .take_while(|chunk| matches!(chunk, Chunk::Local { .. } | Chunk::Statement { .. }))
+ .count();
+ let prologue_chunks = &chunks[..prologue_len];
+ let prologue = build_ready_parts(prologue_chunks);
+ let prologue_view = prologue_chunks
+ .iter()
+ .any(|chunk| matches!(chunk, Chunk::Statement { .. }))
+ .then(|| {
+ quote! {
+ __tree.push_view(#topcoat_view::View::new(::core::mem::take(&mut __parts)));
+ }
+ });
+ let (statements, parts_dirty) = build_tree_statements(&chunks[prologue_len..]);
+ let flush = parts_dirty.then(|| {
+ quote! {
+ __tree.push_view(#topcoat_view::View::new(__parts));
+ }
+ });
+
+ quote! {
+ async {
+ use #topcoat_view::internal::*;
+ let mut __parts = #topcoat_view::ViewParts::new();
+ #prologue
+ let mut __tree = __ViewTree::new();
+ #prologue_view
+ #statements
+ #flush
+ __tree.resolve().await
+ }
+ }
+}
+
+fn build_tree_statements(chunks: &[Chunk]) -> (TokenStream, bool) {
+ let mut output = TokenStream::new();
+ let mut parts_dirty = false;
+
+ for (index, chunk) in chunks.iter().enumerate() {
+ match chunk {
+ Chunk::Static { string } => {
+ parts_dirty = true;
+ let helper = ExprKind::Unescaped.helper();
+ quote! { #helper(__cx, &mut __parts, #string); }
+ }
+ Chunk::Expr { kind, tokens } => {
+ parts_dirty = true;
+ let helper = kind.helper();
+ quote! { #helper(__cx, &mut __parts, #tokens); }
+ }
+ Chunk::Component { tokens } => {
+ let flush = flush_tree_parts(&mut parts_dirty);
+ quote! {
+ #flush
+ __tree.push_future(#tokens);
+ }
+ }
+ Chunk::Local { .. } | Chunk::Statement { .. } => {
+ let flush = flush_tree_parts(&mut parts_dirty);
+ // Keep later locals inside a subtree so they outlive its pending nodes.
+ let remainder = build_tree_future(&chunks[index..]);
+ quote! {
+ #flush
+ __tree.push_future(#remainder);
+ }
+ .to_tokens(&mut output);
+ return (output, false);
+ }
+ Chunk::If {
+ expr,
+ then_branch,
+ else_branch,
+ } if contains_component(&then_branch.chunks)
+ || contains_component(&else_branch.chunks) =>
+ {
+ let then_branch = build_tree_future(&then_branch.chunks);
+ let else_branch = build_tree_future(&else_branch.chunks);
+ let flush = flush_tree_parts(&mut parts_dirty);
+ quote! {
+ #flush
+ __tree.push_future(async {
+ if #expr {
+ (#then_branch).await
+ } else {
+ (#else_branch).await
+ }
+ });
+ }
+ }
+ Chunk::For { pat, expr, body } if contains_component(&body.chunks) => {
+ let body = build_tree_future(&body.chunks);
+ let flush = flush_tree_parts(&mut parts_dirty);
+ quote! {
+ #flush
+ __tree.push_future(async {
+ let __iterations = (#expr)
+ .into_iter()
+ .map(async |#pat| (#body).await);
+ let mut __iteration_parts = #topcoat_view::ViewParts::new();
+ for __iteration in __join_all(__iterations).await {
+ __view(__cx, &mut __iteration_parts, __iteration?);
+ }
+ ::core::result::Result::<
+ #topcoat_view::View,
+ #topcoat_error::Error,
+ >::Ok(#topcoat_view::View::new(__iteration_parts))
+ });
+ }
+ }
+ Chunk::Match { expr, arms }
+ if arms.iter().any(|arm| contains_component(&arm.body.chunks)) =>
+ {
+ let arm_tokens = arms.iter().map(|arm| {
+ let pat = &arm.pat;
+ let guard = arm.guard.as_ref().map(|guard| quote! { if #guard });
+ let body = build_tree_future(&arm.body.chunks);
+ quote! { #pat #guard => (#body).await }
+ });
+ let flush = flush_tree_parts(&mut parts_dirty);
+ quote! {
+ #flush
+ __tree.push_future(async {
+ match #expr {
+ #(#arm_tokens,)*
+ }
+ });
+ }
+ }
+ Chunk::If { .. } | Chunk::For { .. } | Chunk::Match { .. } => {
+ parts_dirty = true;
+ build_ready_parts(core::slice::from_ref(chunk))
+ }
+ }
+ .to_tokens(&mut output);
+ }
+
+ (output, parts_dirty)
+}
+
+fn flush_tree_parts(parts_dirty: &mut bool) -> TokenStream {
+ if *parts_dirty {
+ *parts_dirty = false;
+ quote! {
+ __tree.push_view(#topcoat_view::View::new(::core::mem::take(&mut __parts)));
+ }
+ } else {
+ TokenStream::new()
+ }
+}
+
+fn build_ready_parts(chunks: &[Chunk]) -> TokenStream {
+ let mut output = TokenStream::new();
+ for chunk in chunks {
+ match chunk {
+ Chunk::Static { string } => {
+ let helper = ExprKind::Unescaped.helper();
+ quote! { #helper(__cx, &mut __parts, #string); }
+ }
+ Chunk::Expr { kind, tokens } => {
+ let helper = kind.helper();
+ quote! { #helper(__cx, &mut __parts, #tokens); }
+ }
+ Chunk::Component { .. } => unreachable!(),
+ Chunk::Local { pat, expr } => quote! { let #pat = #expr; },
+ Chunk::Statement { tokens } => quote! { #tokens },
+ Chunk::If {
+ expr,
+ then_branch,
+ else_branch,
+ } => {
+ let then_branch = build_ready_parts(&then_branch.chunks);
+ let else_branch = build_ready_parts(&else_branch.chunks);
+ let else_branch =
+ (!else_branch.is_empty()).then(|| quote! { else { #else_branch } });
+ quote! {
+ if #expr {
+ #then_branch
+ }
+ #else_branch
+ }
+ }
+ Chunk::For { pat, expr, body } => {
+ let body = build_ready_parts(&body.chunks);
+ quote! {
+ for #pat in #expr {
+ #body
+ }
+ }
+ }
+ Chunk::Match { expr, arms } => {
+ let arm_tokens = arms.iter().map(|arm| {
+ let pat = &arm.pat;
+ let guard = arm.guard.as_ref().map(|guard| quote! { if #guard });
+ let body = build_ready_parts(&arm.body.chunks);
+ quote! { #pat #guard => { #body } }
+ });
+ quote! {
+ match #expr {
+ #(#arm_tokens,)*
+ }
+ }
+ }
+ }
+ .to_tokens(&mut output);
+ }
+ output
+}
+
/// Identifies which `internal` helper a [`Chunk::Expr`] should be wrapped in
/// when emitted, so the generated code uses the matching `__*` function and
/// the corresponding `*ViewParts` trait.
@@ -221,7 +397,6 @@ impl ViewWriter {
pub(crate) enum ExprKind {
Unescaped,
Node,
- View,
ElementName,
Attribute,
AttributeUnescaped,
@@ -235,7 +410,6 @@ impl ExprKind {
let name = match self {
Self::Unescaped => "__unescaped",
Self::Node => "__node",
- Self::View => "__view",
Self::ElementName => "__element_name",
Self::Attribute => "__attribute",
Self::AttributeUnescaped => "__attribute_unescaped",
@@ -255,6 +429,9 @@ enum Chunk {
kind: ExprKind,
tokens: TokenStream,
},
+ Component {
+ tokens: TokenStream,
+ },
Local {
pat: Pat,
expr: Box,
@@ -364,6 +541,28 @@ mod tests {
assert!(out.contains("__unescaped (__cx , & mut __parts , \"
\")"));
}
+ #[test]
+ fn components_across_control_flow_build_a_future_tree() {
+ let mut writer = ViewWriter::new();
+ writer.write_component(quote! { first() });
+ writer.if_else(&syn::parse_quote!(cond), |then_branch, _| {
+ then_branch.write_component(quote! { second() });
+ });
+ writer.for_loop(
+ &syn::parse_quote!(item),
+ &syn::parse_quote!(items),
+ |body| {
+ body.write_component(quote! { row(item) });
+ },
+ );
+ let out = rendered(writer);
+
+ assert!(out.contains("__ViewTree :: new"));
+ assert!(out.contains("push_future (first ())"));
+ assert!(out.contains("if cond"));
+ assert!(out.contains("__join_all"));
+ }
+
#[test]
fn if_else_renders_both_branches() {
let mut writer = ViewWriter::new();
@@ -434,7 +633,6 @@ mod tests {
for (kind, expected) in [
(ExprKind::Unescaped, "__unescaped"),
(ExprKind::Node, "__node"),
- (ExprKind::View, "__view"),
(ExprKind::ElementName, "__element_name"),
(ExprKind::Attribute, "__attribute"),
(ExprKind::AttributeUnescaped, "__attribute_unescaped"),
diff --git a/crates/topcoat-view/macro/docs/view.md b/crates/topcoat-view/macro/docs/view.md
index 97b748f73..2f1746451 100644
--- a/crates/topcoat-view/macro/docs/view.md
+++ b/crates/topcoat-view/macro/docs/view.md
@@ -345,6 +345,10 @@ view! {
See how to define components in the [`component`] macro guide.
+The macro collects component calls into an unresolved view tree, then polls every independent component in that tree concurrently. This includes components selected by `if` and `match`, components separated by static HTML, and every iteration of a `for` loop. Completed views are inserted in source order, regardless of completion order. Components in branches that are not selected do not run.
+
+Child nodes use an internal placeholder while the component renders, allowing components inside the child and the parent to run concurrently. The placeholder is filled before the completed view can be rendered.
+
# Boolean And Conditional Attributes
[Boolean HTML attributes](https://developer.mozilla.org/en-US/docs/Glossary/Boolean/HTML) such as `disabled`, `required`, and `checked` are true when the attribute is present and false when it is absent. HTML expects a present boolean attribute to have an empty value.
diff --git a/crates/topcoat-view/macro/tests/component.rs b/crates/topcoat-view/macro/tests/component.rs
index a4d0bca38..215aeff53 100644
--- a/crates/topcoat-view/macro/tests/component.rs
+++ b/crates/topcoat-view/macro/tests/component.rs
@@ -1,3 +1,5 @@
+use std::sync::{Arc, Mutex};
+
use topcoat::{
Result,
context::Cx,
@@ -95,6 +97,136 @@ async fn component_can_call_other_components_and_forward_child_views() {
assert!(html.contains("inner"));
}
+#[component]
+async fn concurrent_slot(log: Arc>>, label: &'static str) -> Result {
+ log.lock().unwrap().push(format!("enter {label}"));
+ tokio::task::yield_now().await;
+ log.lock().unwrap().push(format!("exit {label}"));
+
+ view! { (label) }
+}
+
+#[component]
+async fn concurrent_group(
+ log: Arc>>,
+ first: &'static str,
+ second: &'static str,
+ child: View,
+) -> Result {
+ view! {
+
+ concurrent_slot(log: Arc::clone(&log), label: first)
+ (child)
+ concurrent_slot(log: Arc::clone(&log), label: second)
+
+ }
+}
+
+#[tokio::test]
+async fn components_across_control_flow_render_concurrently_in_source_order() {
+ let cx = empty_cx();
+ let __cx = &cx;
+ let log = Arc::new(Mutex::new(Vec::new()));
+ let result: Result = view! {
+
+ concurrent_slot(log: Arc::clone(&log), label: "a")
+ if true {
+ concurrent_slot(log: Arc::clone(&log), label: "b")
+ } else {
+ concurrent_slot(log: Arc::clone(&log), label: "skipped-if")
+ }
+ match Some("c") {
+ Some(label) => concurrent_slot(log: Arc::clone(&log), label: label),
+ None => concurrent_slot(log: Arc::clone(&log), label: "skipped-match"),
+ }
+ for label in ["d", "e"] {
+
+ }
+ concurrent_slot(log: Arc::clone(&log), label: "f")
+
+ };
+
+ assert_eq!(
+ *log.lock().unwrap(),
+ [
+ "enter a", "enter b", "enter c", "enter d", "enter e", "enter f", "exit a", "exit b",
+ "exit c", "exit d", "exit e", "exit f",
+ ],
+ );
+ assert_eq!(
+ result.unwrap().render(__cx),
+ "acf",
+ );
+}
+
+#[tokio::test]
+async fn component_loop_iterations_render_concurrently_in_iterator_order() {
+ let cx = empty_cx();
+ let __cx = &cx;
+ let log = Arc::new(Mutex::new(Vec::new()));
+ let result: Result = view! {
+
+ for label in ["a", "b", "c"] {
+ concurrent_slot(log: Arc::clone(&log), label: label)
+ }
+
+ };
+
+ assert_eq!(
+ *log.lock().unwrap(),
+ [
+ "enter a", "enter b", "enter c", "exit a", "exit b", "exit c"
+ ],
+ );
+ assert_eq!(
+ result.unwrap().render(__cx),
+ "abc",
+ );
+}
+
+#[tokio::test]
+async fn components_in_parent_and_child_views_render_concurrently() {
+ let cx = empty_cx();
+ let __cx = &cx;
+ let log = Arc::new(Mutex::new(Vec::new()));
+ let result: Result = view! {
+ concurrent_group(
+ log: Arc::clone(&log),
+ first: "parent-a",
+ second: "parent-b",
+ concurrent_group(
+ log: Arc::clone(&log),
+ first: "child-a",
+ second: "child-b",
+ concurrent_slot(log: Arc::clone(&log), label: "grandchild-a")
+ concurrent_slot(log: Arc::clone(&log), label: "grandchild-b")
+ )
+ )
+ };
+
+ assert_eq!(
+ *log.lock().unwrap(),
+ [
+ "enter grandchild-a",
+ "enter grandchild-b",
+ "enter child-a",
+ "enter child-b",
+ "enter parent-a",
+ "enter parent-b",
+ "exit grandchild-a",
+ "exit grandchild-b",
+ "exit child-a",
+ "exit child-b",
+ "exit parent-a",
+ "exit parent-b",
+ ],
+ );
+ assert_eq!(
+ result.unwrap().render(__cx),
+ "parent-achild-agrandchild-agrandchild-bchild-bparent-b",
+ );
+}
+
#[component]
async fn no_args_component() -> Result {
view! { "static"
}
diff --git a/crates/topcoat-view/src/lib.rs b/crates/topcoat-view/src/lib.rs
index b940f8985..edf94e0a1 100644
--- a/crates/topcoat-view/src/lib.rs
+++ b/crates/topcoat-view/src/lib.rs
@@ -12,6 +12,7 @@ mod props;
pub mod svg;
mod unescaped;
mod view;
+mod view_tree;
pub use attribute::*;
pub use class::*;
@@ -28,8 +29,10 @@ pub use view::*;
/// Macro helpers to shorten the generated source code.
#[doc(hidden)]
pub mod internal {
+ pub use futures_util::future::{join as __join, join_all as __join_all};
use topcoat_core::context::Cx;
+ pub use crate::view_tree::{ViewSlot as __ViewSlot, ViewTree as __ViewTree};
use crate::{
Attribute, AttributeKeyViewParts, AttributeValueViewParts, AttributeViewParts,
ElementNameViewParts, HtmlContext, NodeViewParts, PartsWriter, Unescaped, View, ViewParts,
diff --git a/crates/topcoat-view/src/view.rs b/crates/topcoat-view/src/view.rs
index 25bdf544b..0265dd094 100644
--- a/crates/topcoat-view/src/view.rs
+++ b/crates/topcoat-view/src/view.rs
@@ -1,5 +1,8 @@
use core::{fmt, fmt::Write as _};
-use std::borrow::Cow;
+use std::{
+ borrow::Cow,
+ sync::{Arc, Mutex},
+};
#[cfg(feature = "http")]
use http::{HeaderMap, StatusCode};
@@ -208,6 +211,9 @@ pub enum ViewPart {
inner: Box<[ViewPart]>,
size_hint: usize,
},
+ /// A framework-managed slot filled before the completed view is rendered.
+ #[doc(hidden)]
+ Slot(Arc>>),
/// A response status code recorded at render time; renders no content.
#[cfg(feature = "http")]
#[non_exhaustive]
@@ -277,6 +283,12 @@ impl ViewPart {
part.render(cx, f);
}
}
+ Self::Slot(inner) => inner
+ .lock()
+ .unwrap()
+ .as_ref()
+ .expect("view slot must be filled before rendering")
+ .render(cx, f),
#[cfg(feature = "http")]
Self::StatusCode(status_code) => f.record_status_code(*status_code),
#[cfg(feature = "http")]
@@ -322,6 +334,11 @@ impl ViewPart {
_ => value.len() + value.len() / 8,
},
Self::BoxDyn { size_hint, .. } | Self::BoxSlice { size_hint, .. } => *size_hint,
+ Self::Slot(inner) => inner
+ .lock()
+ .unwrap()
+ .as_ref()
+ .map_or(0, ViewPart::size_hint),
#[cfg(feature = "http")]
Self::StatusCode(_) | Self::Headers(_) => 0,
}
diff --git a/crates/topcoat-view/src/view_tree.rs b/crates/topcoat-view/src/view_tree.rs
new file mode 100644
index 000000000..5749c9907
--- /dev/null
+++ b/crates/topcoat-view/src/view_tree.rs
@@ -0,0 +1,97 @@
+use core::{future::Future, pin::Pin};
+use std::sync::{Arc, Mutex};
+
+use futures_util::future::join_all;
+use topcoat_core::error::{Error, Result};
+
+use crate::{View, ViewPart, ViewParts};
+
+type ViewFuture<'a> = Pin> + Send + 'a>>;
+
+enum ViewTreeNode<'a> {
+ Ready(View),
+ Pending(ViewFuture<'a>),
+}
+
+/// A view placeholder filled by generated component code before rendering.
+#[doc(hidden)]
+#[derive(Clone, Debug, Default)]
+pub struct ViewSlot {
+ part: Arc>>,
+}
+
+impl ViewSlot {
+ #[inline]
+ #[must_use]
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ #[inline]
+ #[must_use]
+ pub fn view(&self) -> View {
+ let mut parts = ViewParts::new();
+ parts.push_part(ViewPart::Slot(Arc::clone(&self.part)));
+ View::new(parts)
+ }
+
+ /// Fills the placeholder with its completed child view.
+ ///
+ /// # Panics
+ ///
+ /// Panics if the slot was already filled or its lock was poisoned.
+ #[inline]
+ pub fn fill(&self, view: View) {
+ let previous = self.part.lock().unwrap().replace(view.into_part());
+ assert!(previous.is_none(), "view slot must only be filled once");
+ }
+}
+
+/// Collects ready view segments and component futures before resolving them.
+///
+/// This is plumbing for generated `view!` code.
+#[doc(hidden)]
+#[derive(Default)]
+pub struct ViewTree<'a> {
+ nodes: Vec>,
+}
+
+impl<'a> ViewTree<'a> {
+ #[inline]
+ #[must_use]
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ #[inline]
+ pub fn push_view(&mut self, view: View) {
+ self.nodes.push(ViewTreeNode::Ready(view));
+ }
+
+ #[inline]
+ pub fn push_future(&mut self, future: impl Future