Port example 07
This commit is contained in:
@@ -37,3 +37,7 @@ path = "example/04_composition_and_context.rs"
|
|||||||
[[example]]
|
[[example]]
|
||||||
name = "05_async_runtime_io"
|
name = "05_async_runtime_io"
|
||||||
path = "example/05_async_runtime_io.rs"
|
path = "example/05_async_runtime_io.rs"
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "07_children_and_slots"
|
||||||
|
path = "example/07_children_and_slots.rs"
|
||||||
|
|||||||
282
lib/ruin_app/example/07_children_and_slots.rs
Normal file
282
lib/ruin_app/example/07_children_and_slots.rs
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
use ruin_app::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Project {
|
||||||
|
id: u64,
|
||||||
|
name: &'static str,
|
||||||
|
status: &'static str,
|
||||||
|
summary: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ruin_runtime::async_main]
|
||||||
|
async fn main() -> ruin_app::Result<()> {
|
||||||
|
App::new()
|
||||||
|
.window(
|
||||||
|
Window::new()
|
||||||
|
.title("RUIN Children and Slots")
|
||||||
|
.app_id("dev.ruin.children-and-slots")
|
||||||
|
.size(1180.0, 820.0),
|
||||||
|
)
|
||||||
|
.mount(view! {
|
||||||
|
ChildrenAndSlotsExample() {}
|
||||||
|
})
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn ChildrenAndSlotsExample() -> impl IntoView {
|
||||||
|
let active_project = use_signal(|| 1_u64);
|
||||||
|
let dialog_open = use_signal(|| false);
|
||||||
|
let projects = [
|
||||||
|
Project {
|
||||||
|
id: 1,
|
||||||
|
name: "Renderer clip recovery",
|
||||||
|
status: "Ready for validation",
|
||||||
|
summary: "Nested clip propagation now preserves empty intersections all the way down the tree.",
|
||||||
|
},
|
||||||
|
Project {
|
||||||
|
id: 2,
|
||||||
|
name: "Runtime I/O dashboard",
|
||||||
|
status: "Shipped",
|
||||||
|
summary: "Filesystem snapshots and local TCP-backed endpoint previews now update immediately after async work completes.",
|
||||||
|
},
|
||||||
|
Project {
|
||||||
|
id: 3,
|
||||||
|
name: "Children contract support",
|
||||||
|
status: "In progress",
|
||||||
|
summary: "Custom components can now accept unnamed child content and typed child-like slot props without faking overlay APIs.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let active = projects
|
||||||
|
.iter()
|
||||||
|
.find(|project| project.id == active_project.get())
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| projects[0].clone());
|
||||||
|
|
||||||
|
view! {
|
||||||
|
column(gap = 18.0, padding = 22.0, background = surfaces::canvas()) {
|
||||||
|
text(role = TextRole::Heading(1), size = 30.0, weight = FontWeight::Semibold) {
|
||||||
|
"Children and slots"
|
||||||
|
}
|
||||||
|
|
||||||
|
text(color = colors::muted(), wrap = TextWrap::Word) {
|
||||||
|
"This honest slice implements unnamed child contracts plus typed child-like slot props. It does not pretend modal overlays or keyed list primitives exist yet; the dialog below is rendered inline on purpose."
|
||||||
|
}
|
||||||
|
|
||||||
|
SplitLayout(
|
||||||
|
sidebar = vec![
|
||||||
|
view! {
|
||||||
|
text(role = TextRole::Heading(2), size = 22.0, weight = FontWeight::Semibold) {
|
||||||
|
"Views"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
IntoView::into_view(FilterChip::builder().selected(active.id == 1).children("Rendering")),
|
||||||
|
IntoView::into_view(FilterChip::builder().selected(active.id == 2).children("Runtime I/O")),
|
||||||
|
IntoView::into_view(FilterChip::builder().selected(active.id == 3).children("Framework")),
|
||||||
|
view! {
|
||||||
|
text(color = colors::muted(), wrap = TextWrap::Word) {
|
||||||
|
"The sidebar itself is passed as a typed child-view slot, while the chips use a text-only child contract."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
) {
|
||||||
|
column(gap = 16.0) {
|
||||||
|
CardFrame(
|
||||||
|
title = view! {
|
||||||
|
text(role = TextRole::Heading(2), size = 24.0, weight = FontWeight::Semibold) {
|
||||||
|
"Workspace focus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toolbar = vec![
|
||||||
|
view! {
|
||||||
|
button(on_press = {
|
||||||
|
let dialog_open = dialog_open.clone();
|
||||||
|
move |_| {
|
||||||
|
let _ = dialog_open.set(true);
|
||||||
|
}
|
||||||
|
}) { "Open review dialog" }
|
||||||
|
},
|
||||||
|
view! {
|
||||||
|
button(on_press = {
|
||||||
|
let active_project = active_project.clone();
|
||||||
|
move |_| {
|
||||||
|
let next = if active_project.get() == 3 { 1 } else { active_project.get() + 1 };
|
||||||
|
let _ = active_project.set(next);
|
||||||
|
}
|
||||||
|
}) { "Cycle project" }
|
||||||
|
},
|
||||||
|
]
|
||||||
|
) {
|
||||||
|
text(color = colors::muted()) {
|
||||||
|
("Active project: ", active.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
text(color = colors::muted(), wrap = TextWrap::Word) {
|
||||||
|
active.summary
|
||||||
|
}
|
||||||
|
|
||||||
|
column(gap = 10.0) {
|
||||||
|
button(on_press = {
|
||||||
|
let active_project = active_project.clone();
|
||||||
|
move |_| {
|
||||||
|
let _ = active_project.set(1);
|
||||||
|
}
|
||||||
|
}) { "Show rendering work" }
|
||||||
|
|
||||||
|
button(on_press = {
|
||||||
|
let active_project = active_project.clone();
|
||||||
|
move |_| {
|
||||||
|
let _ = active_project.set(2);
|
||||||
|
}
|
||||||
|
}) { "Show runtime I/O work" }
|
||||||
|
|
||||||
|
button(on_press = {
|
||||||
|
let active_project = active_project.clone();
|
||||||
|
move |_| {
|
||||||
|
let _ = active_project.set(3);
|
||||||
|
}
|
||||||
|
}) { "Show framework work" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InlineDialog(
|
||||||
|
open = dialog_open.get(),
|
||||||
|
title = view! {
|
||||||
|
text(role = TextRole::Heading(2), size = 22.0, weight = FontWeight::Semibold) {
|
||||||
|
"Inline review dialog"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = vec![
|
||||||
|
view! {
|
||||||
|
button(on_press = {
|
||||||
|
let dialog_open = dialog_open.clone();
|
||||||
|
move |_| {
|
||||||
|
let _ = dialog_open.set(false);
|
||||||
|
}
|
||||||
|
}) { "Dismiss" }
|
||||||
|
},
|
||||||
|
view! {
|
||||||
|
button(on_press = {
|
||||||
|
let dialog_open = dialog_open.clone();
|
||||||
|
move |_| {
|
||||||
|
eprintln!("example07: accepted review for {}", active.name);
|
||||||
|
let _ = dialog_open.set(false);
|
||||||
|
}
|
||||||
|
}) { "Accept" }
|
||||||
|
},
|
||||||
|
]
|
||||||
|
) {
|
||||||
|
text(color = colors::muted(), wrap = TextWrap::Word) {
|
||||||
|
"This body arrives through the unnamed child contract. The title and action row are separate typed slot props."
|
||||||
|
}
|
||||||
|
|
||||||
|
text(color = colors::muted()) {
|
||||||
|
("Selected item status: ", active.status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn SplitLayout(sidebar: ChildViews, children: ChildViews) -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
row(gap = 18.0) {
|
||||||
|
block(
|
||||||
|
width = 280.0,
|
||||||
|
gap = 12.0,
|
||||||
|
padding = 16.0,
|
||||||
|
background = surfaces::raised(),
|
||||||
|
border_radius = 16.0,
|
||||||
|
) {
|
||||||
|
sidebar
|
||||||
|
}
|
||||||
|
|
||||||
|
block(
|
||||||
|
flex = 1.0,
|
||||||
|
gap = 16.0,
|
||||||
|
padding = 16.0,
|
||||||
|
background = surfaces::raised(),
|
||||||
|
border_radius = 16.0,
|
||||||
|
) {
|
||||||
|
children
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn FilterChip(selected: bool, children: TextValue) -> impl IntoView {
|
||||||
|
let background = if selected {
|
||||||
|
surfaces::interactive()
|
||||||
|
} else {
|
||||||
|
surfaces::interactive_muted()
|
||||||
|
};
|
||||||
|
let border = if selected {
|
||||||
|
colors::text()
|
||||||
|
} else {
|
||||||
|
colors::muted()
|
||||||
|
};
|
||||||
|
|
||||||
|
view! {
|
||||||
|
block(
|
||||||
|
padding = Edges::symmetric(14.0, 10.0),
|
||||||
|
background = background,
|
||||||
|
border = (1.0, border),
|
||||||
|
border_radius = 999.0,
|
||||||
|
) {
|
||||||
|
text(weight = FontWeight::Medium) {
|
||||||
|
children
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn CardFrame(title: View, toolbar: ChildViews, children: ChildViews) -> impl IntoView {
|
||||||
|
let header = view! {
|
||||||
|
row(gap = 12.0) {
|
||||||
|
block(flex = 1.0) {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
|
||||||
|
row(gap = 10.0) {
|
||||||
|
toolbar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut body = vec![header];
|
||||||
|
body.extend(children.into_vec());
|
||||||
|
block()
|
||||||
|
.gap(14.0)
|
||||||
|
.padding(18.0)
|
||||||
|
.background(surfaces::canvas())
|
||||||
|
.border_radius(16.0)
|
||||||
|
.children(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn InlineDialog(open: bool, title: View, actions: ChildViews, children: ChildViews) -> impl IntoView {
|
||||||
|
if open {
|
||||||
|
let actions_row = view! {
|
||||||
|
row(gap = 10.0) {
|
||||||
|
actions
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut body = vec![title];
|
||||||
|
body.extend(children.into_vec());
|
||||||
|
body.push(actions_row);
|
||||||
|
block()
|
||||||
|
.gap(14.0)
|
||||||
|
.padding(18.0)
|
||||||
|
.background(surfaces::raised())
|
||||||
|
.border((1.0, colors::text()))
|
||||||
|
.border_radius(16.0)
|
||||||
|
.children(body)
|
||||||
|
} else {
|
||||||
|
column().children(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
//! This crate is intentionally low-level. It is the substrate that a future proc-macro-driven
|
//! This crate is intentionally low-level. It is the substrate that a future proc-macro-driven
|
||||||
//! component system can expand to, not the final ergonomic authoring API.
|
//! component system can expand to, not the final ergonomic authoring API.
|
||||||
|
|
||||||
|
extern crate self as ruin_app;
|
||||||
|
|
||||||
use std::any::{Any, TypeId, type_name};
|
use std::any::{Any, TypeId, type_name};
|
||||||
use std::cell::{Cell as StdCell, RefCell};
|
use std::cell::{Cell as StdCell, RefCell};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -496,6 +498,19 @@ pub trait Children {
|
|||||||
fn into_views(self) -> Vec<View>;
|
fn into_views(self) -> Vec<View>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub struct ChildViews(Vec<View>);
|
||||||
|
|
||||||
|
impl ChildViews {
|
||||||
|
pub fn from_children(children: impl Children) -> Self {
|
||||||
|
Self(children.into_views())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_vec(self) -> Vec<View> {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Children for () {
|
impl Children for () {
|
||||||
fn into_views(self) -> Vec<View> {
|
fn into_views(self) -> Vec<View> {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
@@ -514,6 +529,12 @@ impl<T: IntoView> Children for Vec<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Children for ChildViews {
|
||||||
|
fn into_views(self) -> Vec<View> {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
macro_rules! impl_children_tuple {
|
macro_rules! impl_children_tuple {
|
||||||
($($name:ident),+ $(,)?) => {
|
($($name:ident),+ $(,)?) => {
|
||||||
#[allow(non_camel_case_types)]
|
#[allow(non_camel_case_types)]
|
||||||
@@ -538,6 +559,25 @@ pub trait TextChildren {
|
|||||||
fn into_text(self) -> String;
|
fn into_text(self) -> String;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||||
|
pub struct TextValue(String);
|
||||||
|
|
||||||
|
impl TextValue {
|
||||||
|
pub fn from_text(children: impl TextChildren) -> Self {
|
||||||
|
Self(children.into_text())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_string(self) -> String {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextChildren for TextValue {
|
||||||
|
fn into_text(self) -> String {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl TextChildren for &'static str {
|
impl TextChildren for &'static str {
|
||||||
fn into_text(self) -> String {
|
fn into_text(self) -> String {
|
||||||
self.to_string()
|
self.to_string()
|
||||||
@@ -1911,12 +1951,12 @@ fn build_focused_ancestor_chain(
|
|||||||
|
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
pub use crate::{
|
pub use crate::{
|
||||||
App, BlockWidget, ButtonBuilder, Children, Component, ContainerBuilder, ContextKey,
|
App, BlockWidget, ButtonBuilder, ChildViews, Children, Component, ContainerBuilder,
|
||||||
FocusScope, FontWeight, IntoBorder, IntoEdges, IntoView, Key, Memo, Mountable, Pending,
|
ContextKey, FocusScope, FontWeight, IntoBorder, IntoEdges, IntoView, Key, Memo, Mountable,
|
||||||
Ready, Resource, ResourceState, Result, ScrollBoxBuilder, ScrollBoxWidget, Shortcut,
|
Pending, Ready, Resource, ResourceState, Result, ScrollBoxBuilder, ScrollBoxWidget,
|
||||||
ShortcutScope, Signal, TextBuilder, TextChildren, TextRole, View, WidgetRef, Window, block,
|
Shortcut, ShortcutScope, Signal, TextBuilder, TextChildren, TextRole, TextValue, View,
|
||||||
button, colors, column, component, context_provider, provide, row, scroll_box, surfaces,
|
WidgetRef, Window, block, button, colors, column, component, context_provider, provide,
|
||||||
text, use_context, use_effect, use_memo, use_resource, use_shortcut,
|
row, scroll_box, surfaces, text, use_context, use_effect, use_memo, use_resource, use_shortcut,
|
||||||
use_shortcut_with_context, use_signal, use_widget_ref, use_window_title, view,
|
use_shortcut_with_context, use_signal, use_widget_ref, use_window_title, view,
|
||||||
};
|
};
|
||||||
pub use ruin_ui::{
|
pub use ruin_ui::{
|
||||||
@@ -1949,6 +1989,15 @@ mod tests {
|
|||||||
type Value = NamedValue;
|
type Value = NamedValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn ContractProbe(label: TextValue, actions: ChildViews, children: ChildViews) -> impl IntoView {
|
||||||
|
column().children((
|
||||||
|
text().children(label),
|
||||||
|
row().children(actions),
|
||||||
|
block().children(children),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn use_context_distinguishes_marker_types_for_same_value_type() {
|
fn use_context_distinguishes_marker_types_for_same_value_type() {
|
||||||
let seen_outer = Rc::new(RefCell::new(None::<NamedValue>));
|
let seen_outer = Rc::new(RefCell::new(None::<NamedValue>));
|
||||||
@@ -1991,6 +2040,29 @@ mod tests {
|
|||||||
assert_eq!(*seen_value.borrow(), Some(NamedValue("inner")));
|
assert_eq!(*seen_value.borrow(), Some(NamedValue("inner")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn components_accept_child_contracts_and_child_like_slot_props() {
|
||||||
|
let render = render_with_context(Rc::new(RenderState::default()), || {
|
||||||
|
IntoView::into_view(view! {
|
||||||
|
ContractProbe(
|
||||||
|
label = "slot label",
|
||||||
|
actions = (
|
||||||
|
text().children("action a"),
|
||||||
|
text().children("action b"),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
text() { "body child" }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
let debug = format!("{:?}", render.view.element());
|
||||||
|
|
||||||
|
assert!(debug.contains("slot label"), "{debug}");
|
||||||
|
assert!(debug.contains("action a"), "{debug}");
|
||||||
|
assert!(debug.contains("action b"), "{debug}");
|
||||||
|
assert!(debug.contains("body child"), "{debug}");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn key_dispatch_prefers_the_nearest_focused_ancestor_handler() {
|
fn key_dispatch_prefers_the_nearest_focused_ancestor_handler() {
|
||||||
let outer_id = ElementId::new(41);
|
let outer_id = ElementId::new(41);
|
||||||
|
|||||||
@@ -50,16 +50,31 @@ fn expand_component(mut function: ItemFn) -> Result<proc_macro2::TokenStream> {
|
|||||||
.attrs
|
.attrs
|
||||||
.push(syn::parse_quote!(#[allow(non_snake_case)]));
|
.push(syn::parse_quote!(#[allow(non_snake_case)]));
|
||||||
|
|
||||||
let props = function
|
let mut inputs = function
|
||||||
.sig
|
.sig
|
||||||
.inputs
|
.inputs
|
||||||
.iter()
|
.iter()
|
||||||
.map(parse_prop)
|
.map(parse_prop)
|
||||||
.collect::<Result<Vec<_>>>()?;
|
.collect::<Result<Vec<_>>>()?;
|
||||||
|
let child_contract = match inputs.last() {
|
||||||
|
Some(prop) if prop.ident == "children" => {
|
||||||
|
let prop = inputs.pop().expect("last input should exist");
|
||||||
|
let kind = parse_children_contract_kind(&prop.ty)?;
|
||||||
|
Some(ChildContract {
|
||||||
|
ty: prop.ty,
|
||||||
|
kind,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let props = inputs;
|
||||||
|
|
||||||
let builder_name = format_ident!("__{}Builder", component_name);
|
let builder_name = format_ident!("__{}Builder", component_name);
|
||||||
|
let child_field_ident = child_contract.as_ref().map(|_| format_ident!("children"));
|
||||||
|
|
||||||
let builder_tokens = if props.is_empty() {
|
let builder_tokens = if props.is_empty() {
|
||||||
|
match child_contract.as_ref() {
|
||||||
|
None => {
|
||||||
quote! {
|
quote! {
|
||||||
#vis struct #builder_name;
|
#vis struct #builder_name;
|
||||||
|
|
||||||
@@ -69,6 +84,26 @@ fn expand_component(mut function: ItemFn) -> Result<proc_macro2::TokenStream> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
Some(contract) => {
|
||||||
|
let child_arg_ty = child_builder_arg_type_tokens(contract.kind);
|
||||||
|
let child_value = wrap_special_value_tokens(contract.kind, &format_ident!("children"));
|
||||||
|
let child_field_ident = child_field_ident
|
||||||
|
.as_ref()
|
||||||
|
.expect("child field ident should exist");
|
||||||
|
quote! {
|
||||||
|
#vis struct #builder_name;
|
||||||
|
|
||||||
|
impl #builder_name {
|
||||||
|
#vis fn children(self, children: #child_arg_ty) -> #component_name {
|
||||||
|
#component_name {
|
||||||
|
#child_field_ident: #child_value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let missing_names = props
|
let missing_names = props
|
||||||
.iter()
|
.iter()
|
||||||
@@ -98,9 +133,10 @@ fn expand_component(mut function: ItemFn) -> Result<proc_macro2::TokenStream> {
|
|||||||
|
|
||||||
let builder_methods = props.iter().enumerate().map(|(index, prop)| {
|
let builder_methods = props.iter().enumerate().map(|(index, prop)| {
|
||||||
let value_ident = &prop.ident;
|
let value_ident = &prop.ident;
|
||||||
let value_ty = &prop.ty;
|
|
||||||
let missing_at_index = &missing_names[index];
|
let missing_at_index = &missing_names[index];
|
||||||
let present_at_index = &present_names[index];
|
let present_at_index = &present_names[index];
|
||||||
|
let setter_arg_ty = prop_builder_arg_type_tokens(prop);
|
||||||
|
let stored_value = wrap_special_value_tokens(prop.kind, value_ident);
|
||||||
|
|
||||||
let impl_generics = state_idents
|
let impl_generics = state_idents
|
||||||
.iter()
|
.iter()
|
||||||
@@ -138,7 +174,7 @@ fn expand_component(mut function: ItemFn) -> Result<proc_macro2::TokenStream> {
|
|||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(current, field_ident)| {
|
.map(|(current, field_ident)| {
|
||||||
if current == index {
|
if current == index {
|
||||||
quote! { #field_ident: #present_at_index(#value_ident) }
|
quote! { #field_ident: #present_at_index(#stored_value) }
|
||||||
} else {
|
} else {
|
||||||
quote! { #field_ident: self.#field_ident }
|
quote! { #field_ident: self.#field_ident }
|
||||||
}
|
}
|
||||||
@@ -148,7 +184,7 @@ fn expand_component(mut function: ItemFn) -> Result<proc_macro2::TokenStream> {
|
|||||||
if impl_generics.is_empty() {
|
if impl_generics.is_empty() {
|
||||||
quote! {
|
quote! {
|
||||||
impl #builder_name<#(#self_builder_args),*> {
|
impl #builder_name<#(#self_builder_args),*> {
|
||||||
#vis fn #value_ident(self, #value_ident: #value_ty) -> #builder_name<#(#next_builder_args),*> {
|
#vis fn #value_ident(self, #value_ident: #setter_arg_ty) -> #builder_name<#(#next_builder_args),*> {
|
||||||
#builder_name {
|
#builder_name {
|
||||||
#(#field_initializers),*
|
#(#field_initializers),*
|
||||||
}
|
}
|
||||||
@@ -158,7 +194,7 @@ fn expand_component(mut function: ItemFn) -> Result<proc_macro2::TokenStream> {
|
|||||||
} else {
|
} else {
|
||||||
quote! {
|
quote! {
|
||||||
impl<#(#impl_generics),*> #builder_name<#(#self_builder_args),*> {
|
impl<#(#impl_generics),*> #builder_name<#(#self_builder_args),*> {
|
||||||
#vis fn #value_ident(self, #value_ident: #value_ty) -> #builder_name<#(#next_builder_args),*> {
|
#vis fn #value_ident(self, #value_ident: #setter_arg_ty) -> #builder_name<#(#next_builder_args),*> {
|
||||||
#builder_name {
|
#builder_name {
|
||||||
#(#field_initializers),*
|
#(#field_initializers),*
|
||||||
}
|
}
|
||||||
@@ -179,6 +215,59 @@ fn expand_component(mut function: ItemFn) -> Result<proc_macro2::TokenStream> {
|
|||||||
let field_extractors = field_idents
|
let field_extractors = field_idents
|
||||||
.iter()
|
.iter()
|
||||||
.map(|field_ident| quote! { #field_ident: builder.#field_ident.0 });
|
.map(|field_ident| quote! { #field_ident: builder.#field_ident.0 });
|
||||||
|
let component_render_args = props
|
||||||
|
.iter()
|
||||||
|
.map(|prop| {
|
||||||
|
let ident = &prop.ident;
|
||||||
|
quote! { ::core::clone::Clone::clone(&self.#ident) }
|
||||||
|
})
|
||||||
|
.chain(child_field_ident.iter().map(|ident| {
|
||||||
|
quote! { ::core::clone::Clone::clone(&self.#ident) }
|
||||||
|
}))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let children_method = match child_contract.as_ref() {
|
||||||
|
None => quote! {
|
||||||
|
#vis fn children(self, _children: ()) -> #component_name {
|
||||||
|
#component_name::from_builder(self)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Some(contract) => {
|
||||||
|
let child_arg_ty = child_builder_arg_type_tokens(contract.kind);
|
||||||
|
let child_value =
|
||||||
|
wrap_special_value_tokens(contract.kind, &format_ident!("children"));
|
||||||
|
quote! {
|
||||||
|
#vis fn children(self, children: #child_arg_ty) -> #component_name {
|
||||||
|
#component_name::from_builder(self, #child_value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let from_builder = match child_contract.as_ref() {
|
||||||
|
None => quote! {
|
||||||
|
fn from_builder(builder: #builder_name<#(#ready_builder_args),*>) -> Self {
|
||||||
|
Self {
|
||||||
|
#(#field_extractors),*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Some(contract) => {
|
||||||
|
let child_ty = &contract.ty;
|
||||||
|
let child_field_ident = child_field_ident
|
||||||
|
.as_ref()
|
||||||
|
.expect("child field ident should exist");
|
||||||
|
quote! {
|
||||||
|
fn from_builder(
|
||||||
|
builder: #builder_name<#(#ready_builder_args),*>,
|
||||||
|
#child_field_ident: #child_ty,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
#(#field_extractors),*,
|
||||||
|
#child_field_ident,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
quote! {
|
quote! {
|
||||||
#(#marker_structs)*
|
#(#marker_structs)*
|
||||||
@@ -190,17 +279,11 @@ fn expand_component(mut function: ItemFn) -> Result<proc_macro2::TokenStream> {
|
|||||||
#(#builder_methods)*
|
#(#builder_methods)*
|
||||||
|
|
||||||
impl #builder_name<#(#ready_builder_args),*> {
|
impl #builder_name<#(#ready_builder_args),*> {
|
||||||
#vis fn children(self, _children: ()) -> #component_name {
|
#children_method
|
||||||
#component_name::from_builder(self)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl #component_name {
|
impl #component_name {
|
||||||
fn from_builder(builder: #builder_name<#(#ready_builder_args),*>) -> Self {
|
#from_builder
|
||||||
Self {
|
|
||||||
#(#field_extractors),*
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ::ruin_app::Component for #component_name {
|
impl ::ruin_app::Component for #component_name {
|
||||||
@@ -214,26 +297,58 @@ fn expand_component(mut function: ItemFn) -> Result<proc_macro2::TokenStream> {
|
|||||||
|
|
||||||
fn render(&self) -> ::ruin_app::View {
|
fn render(&self) -> ::ruin_app::View {
|
||||||
::ruin_app::IntoView::into_view(#render_name(
|
::ruin_app::IntoView::into_view(#render_name(
|
||||||
#(::core::clone::Clone::clone(&self.#field_idents)),*
|
#(#component_render_args),*
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let render_call = if props.is_empty() {
|
let render_args = props
|
||||||
quote! { #render_name() }
|
.iter()
|
||||||
} else {
|
.map(|prop| {
|
||||||
let field_idents = props.iter().map(|prop| &prop.ident);
|
let ident = &prop.ident;
|
||||||
quote! { #render_name(#(::core::clone::Clone::clone(&self.#field_idents)),*) }
|
quote! { ::core::clone::Clone::clone(&self.#ident) }
|
||||||
};
|
})
|
||||||
|
.chain(child_field_ident.iter().map(|ident| {
|
||||||
|
quote! { ::core::clone::Clone::clone(&self.#ident) }
|
||||||
|
}))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let render_call = quote! { #render_name(#(#render_args),*) };
|
||||||
|
|
||||||
let component_tokens = if props.is_empty() {
|
let component_tokens = match (props.is_empty(), child_contract.as_ref()) {
|
||||||
|
(true, None) => {
|
||||||
quote! {
|
quote! {
|
||||||
#vis struct #component_name;
|
#vis struct #component_name;
|
||||||
|
|
||||||
#builder_tokens
|
#builder_tokens
|
||||||
|
|
||||||
|
impl ::ruin_app::Component for #component_name {
|
||||||
|
type Builder = #builder_name;
|
||||||
|
|
||||||
|
fn builder() -> Self::Builder {
|
||||||
|
#builder_name
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&self) -> ::ruin_app::View {
|
||||||
|
::ruin_app::IntoView::into_view(#render_call)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let field_idents = props
|
||||||
|
.iter()
|
||||||
|
.map(|prop| prop.ident.clone())
|
||||||
|
.chain(child_field_ident.iter().cloned())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let field_types = props
|
||||||
|
.iter()
|
||||||
|
.map(|prop| prop.ty.clone())
|
||||||
|
.chain(child_contract.iter().map(|contract| contract.ty.clone()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let component_impl = if props.is_empty() {
|
||||||
|
quote! {
|
||||||
impl ::ruin_app::Component for #component_name {
|
impl ::ruin_app::Component for #component_name {
|
||||||
type Builder = #builder_name;
|
type Builder = #builder_name;
|
||||||
|
|
||||||
@@ -247,14 +362,17 @@ fn expand_component(mut function: ItemFn) -> Result<proc_macro2::TokenStream> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let field_idents = props.iter().map(|prop| &prop.ident);
|
quote! {}
|
||||||
let field_types = props.iter().map(|prop| &prop.ty);
|
};
|
||||||
quote! {
|
quote! {
|
||||||
#vis struct #component_name {
|
#vis struct #component_name {
|
||||||
#( #field_idents: #field_types ),*
|
#( #field_idents: #field_types ),*
|
||||||
}
|
}
|
||||||
|
|
||||||
#builder_tokens
|
#builder_tokens
|
||||||
|
|
||||||
|
#component_impl
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -333,7 +451,8 @@ fn validate_component_function(function: &ItemFn) -> Result<()> {
|
|||||||
"components cannot be variadic",
|
"components cannot be variadic",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
for input in &signature.inputs {
|
let input_count = signature.inputs.len();
|
||||||
|
for (index, input) in signature.inputs.iter().enumerate() {
|
||||||
let FnArg::Typed(typed) = input else {
|
let FnArg::Typed(typed) = input else {
|
||||||
return Err(Error::new_spanned(
|
return Err(Error::new_spanned(
|
||||||
input,
|
input,
|
||||||
@@ -347,11 +466,14 @@ fn validate_component_function(function: &ItemFn) -> Result<()> {
|
|||||||
));
|
));
|
||||||
};
|
};
|
||||||
if ident == "children" {
|
if ident == "children" {
|
||||||
|
if index + 1 != input_count {
|
||||||
return Err(Error::new_spanned(
|
return Err(Error::new_spanned(
|
||||||
ident,
|
ident,
|
||||||
"`children` is reserved for the generated builder finalizer",
|
"component `children` contracts must be the final parameter",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
parse_children_contract_kind(typed.ty.as_ref())?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match &signature.output {
|
match &signature.output {
|
||||||
@@ -366,6 +488,7 @@ fn validate_component_function(function: &ItemFn) -> Result<()> {
|
|||||||
struct Prop {
|
struct Prop {
|
||||||
ident: Ident,
|
ident: Ident,
|
||||||
ty: Type,
|
ty: Type,
|
||||||
|
kind: SpecialValueKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_prop(input: &FnArg) -> Result<Prop> {
|
fn parse_prop(input: &FnArg) -> Result<Prop> {
|
||||||
@@ -385,9 +508,83 @@ fn parse_prop(input: &FnArg) -> Result<Prop> {
|
|||||||
Ok(Prop {
|
Ok(Prop {
|
||||||
ident: ident.clone(),
|
ident: ident.clone(),
|
||||||
ty: (*typed.ty).clone(),
|
ty: (*typed.ty).clone(),
|
||||||
|
kind: parse_special_value_kind(typed.ty.as_ref()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct ChildContract {
|
||||||
|
ty: Type,
|
||||||
|
kind: SpecialValueKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Eq, PartialEq)]
|
||||||
|
enum SpecialValueKind {
|
||||||
|
Plain,
|
||||||
|
ChildViews,
|
||||||
|
TextValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_special_value_kind(ty: &Type) -> SpecialValueKind {
|
||||||
|
match terminal_type_ident(ty).as_deref() {
|
||||||
|
Some("ChildViews") => SpecialValueKind::ChildViews,
|
||||||
|
Some("TextValue") => SpecialValueKind::TextValue,
|
||||||
|
_ => SpecialValueKind::Plain,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_children_contract_kind(ty: &Type) -> Result<SpecialValueKind> {
|
||||||
|
match parse_special_value_kind(ty) {
|
||||||
|
SpecialValueKind::ChildViews => Ok(SpecialValueKind::ChildViews),
|
||||||
|
SpecialValueKind::TextValue => Ok(SpecialValueKind::TextValue),
|
||||||
|
SpecialValueKind::Plain => Err(Error::new_spanned(
|
||||||
|
ty,
|
||||||
|
"component `children` contracts must use `ruin_app::ChildViews` or `ruin_app::TextValue`",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn terminal_type_ident(ty: &Type) -> Option<String> {
|
||||||
|
let Type::Path(type_path) = ty else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
type_path
|
||||||
|
.path
|
||||||
|
.segments
|
||||||
|
.last()
|
||||||
|
.map(|segment| segment.ident.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prop_builder_arg_type_tokens(prop: &Prop) -> proc_macro2::TokenStream {
|
||||||
|
match prop.kind {
|
||||||
|
SpecialValueKind::Plain => {
|
||||||
|
let ty = &prop.ty;
|
||||||
|
quote! { #ty }
|
||||||
|
}
|
||||||
|
SpecialValueKind::ChildViews => quote! { impl ::ruin_app::Children },
|
||||||
|
SpecialValueKind::TextValue => quote! { impl ::ruin_app::TextChildren },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn child_builder_arg_type_tokens(kind: SpecialValueKind) -> proc_macro2::TokenStream {
|
||||||
|
match kind {
|
||||||
|
SpecialValueKind::Plain => quote! { () },
|
||||||
|
SpecialValueKind::ChildViews => quote! { impl ::ruin_app::Children },
|
||||||
|
SpecialValueKind::TextValue => quote! { impl ::ruin_app::TextChildren },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wrap_special_value_tokens(
|
||||||
|
kind: SpecialValueKind,
|
||||||
|
ident: &Ident,
|
||||||
|
) -> proc_macro2::TokenStream {
|
||||||
|
match kind {
|
||||||
|
SpecialValueKind::Plain => quote! { #ident },
|
||||||
|
SpecialValueKind::ChildViews => quote! { ::ruin_app::ChildViews::from_children(#ident) },
|
||||||
|
SpecialValueKind::TextValue => quote! { ::ruin_app::TextValue::from_text(#ident) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct ViewRoot {
|
struct ViewRoot {
|
||||||
root: Node,
|
root: Node,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user