diff --git a/examples/simple_example.rs b/examples/simple_example.rs new file mode 100644 index 0000000..8fc4bb0 --- /dev/null +++ b/examples/simple_example.rs @@ -0,0 +1,21 @@ +use std::sync::Arc; + +use osui::prelude::*; + +pub fn main() { + let engine = Console::new(); + engine.run(App {}).expect("Failed to run engine"); +} + +#[component] +fn App(cx: &Arc) -> View { + rsx! { + MyComponent { "Hello world!" } + } + .view(cx.clone()) +} + +#[component] +fn MyComponent(cx: &Arc, children: &Rsx) -> View { + children.view(cx.clone()) +} diff --git a/macros/src/emit.rs b/macros/src/emit.rs index 64eec83..96a277f 100644 --- a/macros/src/emit.rs +++ b/macros/src/emit.rs @@ -3,7 +3,11 @@ use proc_macro2::TokenStream; use quote::quote; pub fn emit_rsx(root: RsxRoot) -> TokenStream { - let nodes = root.nodes.iter().map(emit_node_scope); + emit_rsx_vec(&root.nodes) +} + +pub fn emit_rsx_vec(nodes: &Vec) -> TokenStream { + let nodes = nodes.iter().map(emit_node_scope); quote! {{ let mut r = osui::frontend::Rsx::new(); @@ -55,7 +59,7 @@ fn emit_node_scope(node: &RsxNode) -> TokenStream { } } - RsxNode::Component(_) => { + RsxNode::Component { .. } => { let emit = emit_node(node); quote! { r.static_scope(move |scope| {#emit}); @@ -125,9 +129,38 @@ fn emit_node(node: &RsxNode) -> TokenStream { })); }, - RsxNode::Component(expr) => quote! { - scope.child(#expr, None); - }, + RsxNode::Component { + path, + props, + children, + } => { + let prop_inits = props.iter().map(|p| { + let name = &p.name; + let value = &p.value; + quote! { #name: #value } + }); + + let emit_children = emit_rsx_vec(children); + + let component_expr = if children.len() > 0 { + quote! { + #path { + #(#prop_inits,)* + children: #emit_children + } + } + } else { + quote! { + #path { + #(#prop_inits,)* + } + } + }; + + quote! { + scope.child(#component_expr, None); + } + } RsxNode::Mount(m) => quote! { #m.mount(); diff --git a/macros/src/parse.rs b/macros/src/parse.rs index d97c6f7..ec49e72 100644 --- a/macros/src/parse.rs +++ b/macros/src/parse.rs @@ -1,7 +1,8 @@ use syn::braced; use syn::{ - Expr, Ident, LitStr, Pat, Result, Token, + Expr, Ident, LitStr, Pat, Path, Result, Token, parse::{Parse, ParseStream}, + token::Brace, }; pub struct RsxRoot { @@ -18,9 +19,20 @@ impl Parse for RsxRoot { } } +/// A single prop: `ident: expr`. +pub struct RsxProp { + pub name: Ident, + pub value: Expr, +} + pub enum RsxNode { Text(LitStr), - Component(Expr), + /// Component with optional props and children: `Path { prop: expr, "child", ... }` or bare `Path`. + Component { + path: Path, + props: Vec, + children: Vec, + }, Mount(Ident), If { deps: Vec, @@ -116,11 +128,80 @@ impl Parse for RsxNode { return Ok(RsxNode::Text(input.parse()?)); } - // Component: path (e.g. HelloWorld) or struct init (e.g. HelloWorld { t: 10 }) - Ok(RsxNode::Component(input.parse()?)) + // Component: Path or Path { props, children } + parse_component_invocation(input) } } +/// Parses Path or Path { ... } into RsxNode::Component. +fn parse_component_invocation(input: ParseStream) -> Result { + let path: Path = input.parse()?; + + if !input.peek(Brace) { + return Ok(RsxNode::Component { + path, + props: Vec::new(), + children: Vec::new(), + }); + } + + let content; + braced!(content in input); + + let (props, children) = parse_props_and_children(&content)?; + + Ok(RsxNode::Component { + path, + props, + children, + }) +} + +/// Parses comma-separated "ident: expr" (props) and RsxNode (children) inside braces. +fn parse_props_and_children(input: ParseStream) -> Result<(Vec, Vec)> { + let mut props = Vec::new(); + let mut children = Vec::new(); + + while !input.is_empty() { + if input.peek(LitStr) { + children.push(input.parse()?); + } else { + let path: Path = input.parse()?; + + if input.peek(Token![:]) { + let name = path + .get_ident() + .cloned() + .ok_or_else(|| input.error("prop name must be a single identifier"))?; + input.parse::()?; + let value: Expr = input.parse()?; + props.push(RsxProp { name, value }); + } else if input.peek(Brace) { + let nested_content; + braced!(nested_content in input); + let (nested_props, nested_children) = parse_props_and_children(&nested_content)?; + children.push(RsxNode::Component { + path, + props: nested_props, + children: nested_children, + }); + } else { + children.push(RsxNode::Component { + path, + props: Vec::new(), + children: Vec::new(), + }); + } + } + + if !input.is_empty() { + input.parse::()?; + } + } + + Ok((props, children)) +} + fn parse_children(input: ParseStream) -> Result> { let mut nodes = Vec::new(); while !input.is_empty() { diff --git a/src/lib.rs b/src/lib.rs index b282e53..4b3d469 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,7 @@ pub mod state; pub mod prelude { pub use crate::component::*; pub use crate::engine::*; + pub use crate::frontend::*; pub use crate::render::*; pub use crate::state::*; pub use crate::View; diff --git a/src/state.rs b/src/state.rs index 502e56c..339c4b2 100644 --- a/src/state.rs +++ b/src/state.rs @@ -115,11 +115,11 @@ impl DerefMut for Inner<'_, T> { } } -pub trait HookDependency { +pub trait HookDependency: Send + Sync { fn on_update(&self, hook: HookEffect); } -impl HookDependency for State { +impl HookDependency for State { fn on_update(&self, hook: HookEffect) { self.dependents.lock().unwrap().push(hook); }