Proc macros for components, views
This commit is contained in:
10
Cargo.lock
generated
10
Cargo.lock
generated
@@ -1606,6 +1606,15 @@ version = "0.20.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
|
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ruin-app-proc-macros"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruin-runtime"
|
name = "ruin-runtime"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -1631,6 +1640,7 @@ dependencies = [
|
|||||||
name = "ruin_app"
|
name = "ruin_app"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"ruin-app-proc-macros",
|
||||||
"ruin-runtime",
|
"ruin-runtime",
|
||||||
"ruin_reactivity",
|
"ruin_reactivity",
|
||||||
"ruin_ui",
|
"ruin_ui",
|
||||||
|
|||||||
@@ -6,9 +6,14 @@ edition = "2024"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
ruin_reactivity = { path = "../reactivity" }
|
ruin_reactivity = { path = "../reactivity" }
|
||||||
ruin_runtime = { package = "ruin-runtime", path = "../runtime" }
|
ruin_runtime = { package = "ruin-runtime", path = "../runtime" }
|
||||||
|
ruin_app_proc_macros = { package = "ruin-app-proc-macros", path = "../ruin_app_proc_macros" }
|
||||||
ruin_ui = { path = "../ui" }
|
ruin_ui = { path = "../ui" }
|
||||||
ruin_ui_platform_wayland = { path = "../ui_platform_wayland" }
|
ruin_ui_platform_wayland = { path = "../ui_platform_wayland" }
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "00_bootstrap_and_counter_raw"
|
name = "00_bootstrap_and_counter_raw"
|
||||||
path = "example/00_bootstrap_and_counter_raw.rs"
|
path = "example/00_bootstrap_and_counter_raw.rs"
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "00_bootstrap_and_counter"
|
||||||
|
path = "example/00_bootstrap_and_counter.rs"
|
||||||
|
|||||||
95
lib/ruin_app/example/00_bootstrap_and_counter.rs
Normal file
95
lib/ruin_app/example/00_bootstrap_and_counter.rs
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
use ruin_app::prelude::*;
|
||||||
|
use ruin_ui::Border;
|
||||||
|
|
||||||
|
#[ruin_runtime::async_main]
|
||||||
|
async fn main() -> ruin_app::Result<()> {
|
||||||
|
let title = "RUIN Counter";
|
||||||
|
|
||||||
|
App::new()
|
||||||
|
.window(
|
||||||
|
Window::new()
|
||||||
|
.title(title)
|
||||||
|
.app_id("dev.ruin.counter")
|
||||||
|
.size(960.0, 640.0),
|
||||||
|
)
|
||||||
|
.mount(view! {
|
||||||
|
CounterApp(title = title) {}
|
||||||
|
})
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn CounterApp(title: &'static str) -> impl IntoView {
|
||||||
|
let count = use_signal(|| 0_i32);
|
||||||
|
let doubled = use_memo({
|
||||||
|
let count = count.clone();
|
||||||
|
move || count.get() * 2
|
||||||
|
});
|
||||||
|
|
||||||
|
use_window_title({
|
||||||
|
let count = count.clone();
|
||||||
|
move || format!("{title} ({})", count.get())
|
||||||
|
});
|
||||||
|
|
||||||
|
view! {
|
||||||
|
column(gap = 16.0, padding = 24.0) {
|
||||||
|
text(role = TextRole::Heading(1), size = 32.0, weight = FontWeight::Semibold) {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
|
||||||
|
CounterActions(count = count.clone()) {}
|
||||||
|
|
||||||
|
block(
|
||||||
|
padding = 16.0,
|
||||||
|
gap = 8.0,
|
||||||
|
background = surfaces::raised(),
|
||||||
|
border_radius = 12.0,
|
||||||
|
border = Border::new(2.0, Color::rgb(0, 0, 0))
|
||||||
|
) {
|
||||||
|
text(size = 18.0) { "count = "; count.clone() }
|
||||||
|
text(color = colors::muted()) { "double = "; doubled.clone() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn CounterActions(count: Signal<i32>) -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
row(gap = 8.0) {
|
||||||
|
button(
|
||||||
|
on_press = {
|
||||||
|
let count = count.clone();
|
||||||
|
move |_| {
|
||||||
|
count.update(|value| *value -= 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
"-1"
|
||||||
|
}
|
||||||
|
|
||||||
|
button(
|
||||||
|
on_press = {
|
||||||
|
let count = count.clone();
|
||||||
|
move |_| {
|
||||||
|
let _ = count.set(0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
"Reset"
|
||||||
|
}
|
||||||
|
|
||||||
|
button(
|
||||||
|
on_press = {
|
||||||
|
let count = count.clone();
|
||||||
|
move |_| {
|
||||||
|
count.update(|value| *value += 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
"+1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,13 +12,15 @@ use std::rc::Rc;
|
|||||||
|
|
||||||
use ruin_reactivity::effect;
|
use ruin_reactivity::effect;
|
||||||
use ruin_ui::{
|
use ruin_ui::{
|
||||||
Color, CursorIcon, Edges, Element, ElementId, InteractionTree, LayoutSnapshot, PlatformEvent,
|
Border, Color, CursorIcon, Edges, Element, ElementId, InteractionTree, LayoutSnapshot,
|
||||||
PointerButton, PointerEvent, PointerRouter, RoutedPointerEvent, RoutedPointerEventKind,
|
PlatformEvent, PointerButton, PointerEvent, PointerRouter, RoutedPointerEvent,
|
||||||
TextFontFamily, TextSpan, TextSpanWeight, TextStyle, TextSystem, UiSize, WindowController,
|
RoutedPointerEventKind, TextFontFamily, TextSpan, TextSpanWeight, TextStyle, TextSystem,
|
||||||
WindowSpec, WindowUpdate, layout_snapshot_with_text_system,
|
UiSize, WindowController, WindowSpec, WindowUpdate, layout_snapshot_with_text_system,
|
||||||
};
|
};
|
||||||
use ruin_ui_platform_wayland::start_wayland_ui;
|
use ruin_ui_platform_wayland::start_wayland_ui;
|
||||||
|
|
||||||
|
pub use ruin_app_proc_macros::{component, view};
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, Box<dyn Error>>;
|
pub type Result<T> = std::result::Result<T, Box<dyn Error>>;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -564,6 +566,11 @@ impl ContainerBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn border(mut self, border: Border) -> Self {
|
||||||
|
self.element = self.element.border(border.width, border.color);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn width(mut self, width: f32) -> Self {
|
pub fn width(mut self, width: f32) -> Self {
|
||||||
self.element = self.element.width(width);
|
self.element = self.element.width(width);
|
||||||
self
|
self
|
||||||
@@ -949,7 +956,8 @@ pub mod prelude {
|
|||||||
pub use crate::{
|
pub use crate::{
|
||||||
App, ButtonBuilder, Children, Component, ContainerBuilder, FontWeight, IntoEdges, IntoView,
|
App, ButtonBuilder, Children, Component, ContainerBuilder, FontWeight, IntoEdges, IntoView,
|
||||||
Memo, Mountable, Result, Signal, TextBuilder, TextChildren, TextRole, View, Window, block,
|
Memo, Mountable, Result, Signal, TextBuilder, TextChildren, TextRole, View, Window, block,
|
||||||
button, colors, column, row, surfaces, text, use_memo, use_signal, use_window_title,
|
button, colors, column, component, row, surfaces, text, use_memo, use_signal,
|
||||||
|
use_window_title, view,
|
||||||
};
|
};
|
||||||
pub use ruin_ui::{
|
pub use ruin_ui::{
|
||||||
Color, CursorIcon, Edges, Element, ElementId, PointerButton, PointerEventKind,
|
Color, CursorIcon, Edges, Element, ElementId, PointerButton, PointerEventKind,
|
||||||
|
|||||||
12
lib/ruin_app_proc_macros/Cargo.toml
Normal file
12
lib/ruin_app_proc_macros/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[package]
|
||||||
|
name = "ruin-app-proc-macros"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
proc-macro2 = "1"
|
||||||
|
quote = "1"
|
||||||
|
syn = { version = "2", features = ["full"] }
|
||||||
490
lib/ruin_app_proc_macros/src/lib.rs
Normal file
490
lib/ruin_app_proc_macros/src/lib.rs
Normal file
@@ -0,0 +1,490 @@
|
|||||||
|
use proc_macro::TokenStream;
|
||||||
|
use proc_macro2::Span;
|
||||||
|
use quote::{format_ident, quote};
|
||||||
|
use syn::parse::{Parse, ParseStream};
|
||||||
|
use syn::punctuated::Punctuated;
|
||||||
|
use syn::{
|
||||||
|
Error, Expr, FnArg, Ident, ItemFn, Pat, PatIdent, Path, Result, ReturnType, Token, Type,
|
||||||
|
braced, parenthesized, parse_macro_input,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn component(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
|
if !proc_macro2::TokenStream::from(attr).is_empty() {
|
||||||
|
return Error::new(Span::call_site(), "#[component] takes no arguments")
|
||||||
|
.to_compile_error()
|
||||||
|
.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
let function = parse_macro_input!(item as ItemFn);
|
||||||
|
match expand_component(function) {
|
||||||
|
Ok(tokens) => tokens.into(),
|
||||||
|
Err(error) => error.to_compile_error().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proc_macro]
|
||||||
|
pub fn view(input: TokenStream) -> TokenStream {
|
||||||
|
let root = parse_macro_input!(input as ViewRoot);
|
||||||
|
expand_node(&root.root).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expand_component(mut function: ItemFn) -> Result<proc_macro2::TokenStream> {
|
||||||
|
validate_component_function(&function)?;
|
||||||
|
|
||||||
|
let vis = function.vis.clone();
|
||||||
|
let component_name = function.sig.ident.clone();
|
||||||
|
let render_name = format_ident!("__ruin_app_render_{}", component_name);
|
||||||
|
function.sig.ident = render_name.clone();
|
||||||
|
function
|
||||||
|
.attrs
|
||||||
|
.push(syn::parse_quote!(#[allow(non_snake_case)]));
|
||||||
|
|
||||||
|
let props = function
|
||||||
|
.sig
|
||||||
|
.inputs
|
||||||
|
.iter()
|
||||||
|
.map(parse_prop)
|
||||||
|
.collect::<Result<Vec<_>>>()?;
|
||||||
|
|
||||||
|
let builder_name = format_ident!("__{}Builder", component_name);
|
||||||
|
|
||||||
|
let builder_tokens = if props.is_empty() {
|
||||||
|
quote! {
|
||||||
|
#vis struct #builder_name;
|
||||||
|
|
||||||
|
impl #builder_name {
|
||||||
|
#vis fn children(self, _children: ()) -> #component_name {
|
||||||
|
#component_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let missing_names = props
|
||||||
|
.iter()
|
||||||
|
.map(|prop| format_ident!("__{}__{}_Missing", component_name, prop.ident))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let present_names = props
|
||||||
|
.iter()
|
||||||
|
.map(|prop| format_ident!("__{}__{}_Present", component_name, prop.ident))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let state_idents = (0..props.len())
|
||||||
|
.map(|index| format_ident!("P{index}"))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let field_idents = props.iter().map(|prop| &prop.ident).collect::<Vec<_>>();
|
||||||
|
let marker_structs = props
|
||||||
|
.iter()
|
||||||
|
.zip(missing_names.iter())
|
||||||
|
.zip(present_names.iter())
|
||||||
|
.map(|((prop, missing), present)| {
|
||||||
|
let ty = &prop.ty;
|
||||||
|
quote! {
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
#vis struct #missing;
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
#vis struct #present(#ty);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let builder_methods = props.iter().enumerate().map(|(index, prop)| {
|
||||||
|
let value_ident = &prop.ident;
|
||||||
|
let value_ty = &prop.ty;
|
||||||
|
let missing_at_index = &missing_names[index];
|
||||||
|
let present_at_index = &present_names[index];
|
||||||
|
|
||||||
|
let impl_generics = state_idents
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(current, _)| *current != index)
|
||||||
|
.map(|(_, ident)| ident);
|
||||||
|
let impl_generics = impl_generics.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let self_builder_args = state_idents
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(current, ident)| {
|
||||||
|
if current == index {
|
||||||
|
quote! { #missing_at_index }
|
||||||
|
} else {
|
||||||
|
quote! { #ident }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let next_builder_args = state_idents
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(current, ident)| {
|
||||||
|
if current == index {
|
||||||
|
quote! { #present_at_index }
|
||||||
|
} else {
|
||||||
|
quote! { #ident }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let field_initializers = field_idents
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(current, field_ident)| {
|
||||||
|
if current == index {
|
||||||
|
quote! { #field_ident: #present_at_index(#value_ident) }
|
||||||
|
} else {
|
||||||
|
quote! { #field_ident: self.#field_ident }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if impl_generics.is_empty() {
|
||||||
|
quote! {
|
||||||
|
impl #builder_name<#(#self_builder_args),*> {
|
||||||
|
#vis fn #value_ident(self, #value_ident: #value_ty) -> #builder_name<#(#next_builder_args),*> {
|
||||||
|
#builder_name {
|
||||||
|
#(#field_initializers),*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quote! {
|
||||||
|
impl<#(#impl_generics),*> #builder_name<#(#self_builder_args),*> {
|
||||||
|
#vis fn #value_ident(self, #value_ident: #value_ty) -> #builder_name<#(#next_builder_args),*> {
|
||||||
|
#builder_name {
|
||||||
|
#(#field_initializers),*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let ready_builder_args = present_names
|
||||||
|
.iter()
|
||||||
|
.map(|present| quote! { #present })
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let missing_builder_args = missing_names
|
||||||
|
.iter()
|
||||||
|
.map(|missing| quote! { #missing })
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let field_extractors = field_idents
|
||||||
|
.iter()
|
||||||
|
.map(|field_ident| quote! { #field_ident: builder.#field_ident.0 });
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
#(#marker_structs)*
|
||||||
|
|
||||||
|
#vis struct #builder_name<#(#state_idents),*> {
|
||||||
|
#(#field_idents: #state_idents),*
|
||||||
|
}
|
||||||
|
|
||||||
|
#(#builder_methods)*
|
||||||
|
|
||||||
|
impl #builder_name<#(#ready_builder_args),*> {
|
||||||
|
#vis fn children(self, _children: ()) -> #component_name {
|
||||||
|
#component_name::from_builder(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl #component_name {
|
||||||
|
fn from_builder(builder: #builder_name<#(#ready_builder_args),*>) -> Self {
|
||||||
|
Self {
|
||||||
|
#(#field_extractors),*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ::ruin_app::Component for #component_name {
|
||||||
|
type Builder = #builder_name<#(#missing_builder_args),*>;
|
||||||
|
|
||||||
|
fn builder() -> Self::Builder {
|
||||||
|
#builder_name {
|
||||||
|
#(#field_idents: #missing_names),*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&self) -> ::ruin_app::View {
|
||||||
|
::ruin_app::IntoView::into_view(#render_name(
|
||||||
|
#(::core::clone::Clone::clone(&self.#field_idents)),*
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let render_call = if props.is_empty() {
|
||||||
|
quote! { #render_name() }
|
||||||
|
} else {
|
||||||
|
let field_idents = props.iter().map(|prop| &prop.ident);
|
||||||
|
quote! { #render_name(#(::core::clone::Clone::clone(&self.#field_idents)),*) }
|
||||||
|
};
|
||||||
|
|
||||||
|
let component_tokens = if props.is_empty() {
|
||||||
|
quote! {
|
||||||
|
#vis struct #component_name;
|
||||||
|
|
||||||
|
#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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let field_idents = props.iter().map(|prop| &prop.ident);
|
||||||
|
let field_types = props.iter().map(|prop| &prop.ty);
|
||||||
|
quote! {
|
||||||
|
#vis struct #component_name {
|
||||||
|
#( #field_idents: #field_types ),*
|
||||||
|
}
|
||||||
|
|
||||||
|
#builder_tokens
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(quote! {
|
||||||
|
#function
|
||||||
|
|
||||||
|
#component_tokens
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_component_function(function: &ItemFn) -> Result<()> {
|
||||||
|
let signature = &function.sig;
|
||||||
|
|
||||||
|
if signature.asyncness.is_some() {
|
||||||
|
return Err(Error::new_spanned(
|
||||||
|
signature.asyncness,
|
||||||
|
"components must be synchronous functions",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if !signature.generics.params.is_empty() || signature.generics.where_clause.is_some() {
|
||||||
|
return Err(Error::new_spanned(
|
||||||
|
&signature.generics,
|
||||||
|
"generic components are not supported yet",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if signature.constness.is_some() {
|
||||||
|
return Err(Error::new_spanned(
|
||||||
|
signature.fn_token,
|
||||||
|
"components cannot be const",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if signature.unsafety.is_some() {
|
||||||
|
return Err(Error::new_spanned(
|
||||||
|
signature.fn_token,
|
||||||
|
"components cannot be unsafe",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if signature.abi.is_some() {
|
||||||
|
return Err(Error::new_spanned(
|
||||||
|
&signature.abi,
|
||||||
|
"components cannot declare an ABI",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if signature.variadic.is_some() {
|
||||||
|
return Err(Error::new_spanned(
|
||||||
|
&signature.variadic,
|
||||||
|
"components cannot be variadic",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
for input in &signature.inputs {
|
||||||
|
let FnArg::Typed(typed) = input else {
|
||||||
|
return Err(Error::new_spanned(
|
||||||
|
input,
|
||||||
|
"methods with `self` are not supported by #[component]",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
let Pat::Ident(PatIdent { ident, .. }) = typed.pat.as_ref() else {
|
||||||
|
return Err(Error::new_spanned(
|
||||||
|
&typed.pat,
|
||||||
|
"component props must be simple identifier bindings",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
if ident == "children" {
|
||||||
|
return Err(Error::new_spanned(
|
||||||
|
ident,
|
||||||
|
"`children` is reserved for the generated builder finalizer",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match &signature.output {
|
||||||
|
ReturnType::Default => Err(Error::new_spanned(
|
||||||
|
&signature.ident,
|
||||||
|
"components must return a view-like value",
|
||||||
|
)),
|
||||||
|
ReturnType::Type(_, _) => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Prop {
|
||||||
|
ident: Ident,
|
||||||
|
ty: Type,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_prop(input: &FnArg) -> Result<Prop> {
|
||||||
|
let FnArg::Typed(typed) = input else {
|
||||||
|
return Err(Error::new_spanned(
|
||||||
|
input,
|
||||||
|
"methods with `self` are not supported by #[component]",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
let Pat::Ident(PatIdent { ident, .. }) = typed.pat.as_ref() else {
|
||||||
|
return Err(Error::new_spanned(
|
||||||
|
&typed.pat,
|
||||||
|
"component props must be simple identifier bindings",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Prop {
|
||||||
|
ident: ident.clone(),
|
||||||
|
ty: (*typed.ty).clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ViewRoot {
|
||||||
|
root: Node,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parse for ViewRoot {
|
||||||
|
fn parse(input: ParseStream<'_>) -> Result<Self> {
|
||||||
|
let root = input.parse::<Node>()?;
|
||||||
|
if !input.is_empty() {
|
||||||
|
return Err(input.error("view! expects a single root node"));
|
||||||
|
}
|
||||||
|
Ok(Self { root })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Node {
|
||||||
|
path: Path,
|
||||||
|
props: Vec<Property>,
|
||||||
|
children: Vec<Child>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Property {
|
||||||
|
name: Ident,
|
||||||
|
value: Expr,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Child {
|
||||||
|
Node(Node),
|
||||||
|
Expr(Expr),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parse for Node {
|
||||||
|
fn parse(input: ParseStream<'_>) -> Result<Self> {
|
||||||
|
let path = input.parse::<Path>()?;
|
||||||
|
|
||||||
|
let props_content;
|
||||||
|
parenthesized!(props_content in input);
|
||||||
|
let props = Punctuated::<Property, Token![,]>::parse_terminated(&props_content)?
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let children_content;
|
||||||
|
braced!(children_content in input);
|
||||||
|
let mut children = Vec::new();
|
||||||
|
while !children_content.is_empty() {
|
||||||
|
if looks_like_node(&children_content)? {
|
||||||
|
children.push(Child::Node(children_content.parse()?));
|
||||||
|
} else {
|
||||||
|
let expr = children_content.parse::<Expr>()?;
|
||||||
|
if children_content.peek(Token![;]) {
|
||||||
|
let _ = children_content.parse::<Token![;]>()?;
|
||||||
|
}
|
||||||
|
children.push(Child::Expr(expr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
path,
|
||||||
|
props,
|
||||||
|
children,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parse for Property {
|
||||||
|
fn parse(input: ParseStream<'_>) -> Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
name: input.parse()?,
|
||||||
|
value: {
|
||||||
|
let _ = input.parse::<Token![=]>()?;
|
||||||
|
input.parse()?
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn looks_like_node(input: ParseStream<'_>) -> Result<bool> {
|
||||||
|
let fork = input.fork();
|
||||||
|
if fork.parse::<Path>().is_err() {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
if !fork.peek(syn::token::Paren) {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
let props_content;
|
||||||
|
parenthesized!(props_content in fork);
|
||||||
|
let _ = Punctuated::<Property, Token![,]>::parse_terminated(&props_content)?;
|
||||||
|
Ok(fork.peek(syn::token::Brace))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expand_node(node: &Node) -> proc_macro2::TokenStream {
|
||||||
|
let path = &node.path;
|
||||||
|
let prop_calls = node.props.iter().map(|property| {
|
||||||
|
let name = &property.name;
|
||||||
|
let value = &property.value;
|
||||||
|
quote! { .#name(#value) }
|
||||||
|
});
|
||||||
|
let children = expand_children(&node.children);
|
||||||
|
|
||||||
|
if is_component_path(path) {
|
||||||
|
quote! {
|
||||||
|
#path::builder()
|
||||||
|
#(#prop_calls)*
|
||||||
|
.children(#children)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quote! {
|
||||||
|
::ruin_app::#path()
|
||||||
|
#(#prop_calls)*
|
||||||
|
.children(#children)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expand_children(children: &[Child]) -> proc_macro2::TokenStream {
|
||||||
|
match children {
|
||||||
|
[] => quote! { () },
|
||||||
|
[child] => expand_child(child),
|
||||||
|
many => {
|
||||||
|
let items = many.iter().map(expand_child);
|
||||||
|
quote! { ( #(#items),* ) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expand_child(child: &Child) -> proc_macro2::TokenStream {
|
||||||
|
match child {
|
||||||
|
Child::Node(node) => expand_node(node),
|
||||||
|
Child::Expr(expr) => quote! { #expr },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_component_path(path: &Path) -> bool {
|
||||||
|
path.segments
|
||||||
|
.last()
|
||||||
|
.map(|segment| segment.ident.to_string())
|
||||||
|
.and_then(|name| name.chars().next())
|
||||||
|
.map(|ch| ch.is_ascii_uppercase())
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user