diff --git a/examples/benchmark.rs b/examples/benchmark.rs index 4fc8e72..2a73e05 100644 --- a/examples/benchmark.rs +++ b/examples/benchmark.rs @@ -1,15 +1,54 @@ -use std::sync::Arc; - use osui::prelude::*; +use std::{sync::mpsc, thread}; + pub fn main() { - let engine = Benchmark::new(Console::new()); - engine.run(app); + let engine = Arc::new(Benchmark::new(Console::new())); + + let (tx, rx) = mpsc::channel(); + + let max_threads = 32; + let mut handles = vec![]; + + for chunk in (0..500).collect::>().chunks(max_threads) { + for &items in chunk { + let engine = engine.clone(); + let tx = tx.clone(); + + let handle = thread::spawn(move || { + let res = engine + .run(BenchmarkApp { items }) + .expect("Failed to run engine"); + + tx.send((items, res)).expect("Failed to send result"); + }); + + handles.push(handle); + } + + for handle in handles.drain(..) { + handle.join().expect("Thread panicked"); + } + } + + drop(tx); + + let mut results: Vec<(usize, BenchmarkResult)> = rx.iter().collect(); + results.sort_by_key(|(items, _)| *items); + + for (items, bench) in results { + println!("Results for {items} items:\n{bench}"); + } } -fn app(cx: &Arc) -> View { +#[component] +fn BenchmarkApp(cx: &Arc, items: &usize) -> View { + let items = items.clone(); + rsx! { - "Hello, world!" + for _ in (0..items) { + "Hello, world!" + } } - .view(cx.clone()) + .view(&cx) } diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 9ba9489..ecef5bc 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -1,15 +1,14 @@ -use std::sync::Arc; - use osui::prelude::*; pub fn main() { - let console = Console::new(); - console.run(app); + let engine = Console::new(); + engine.run(App {}).expect("Failed to run engine"); } -fn app(cx: &Arc) -> View { +#[component] +fn App(cx: &Arc) -> View { rsx! { - "Hello, world!" + "Hello World" } - .view(cx.clone()) + .view(&cx) } diff --git a/examples/simple_benchmark.rs b/examples/simple_benchmark.rs new file mode 100644 index 0000000..1472f44 --- /dev/null +++ b/examples/simple_benchmark.rs @@ -0,0 +1,19 @@ +use osui::prelude::*; + +pub fn main() { + let engine = Benchmark::new(Console::new()); + let benchmark_result = engine.run(App {}).expect("Failed to run engine"); + println!("Avg: {} μs", benchmark_result.average); + println!("Min: {} μs", benchmark_result.min); + println!("Max: {} μs", benchmark_result.max); + println!("Tot: {} μs", benchmark_result.total); + println!("Tot Render: {} μs", benchmark_result.total_render); +} + +#[component] +fn App(cx: &Arc) -> View { + rsx! { + "Hello, world!" + } + .view(&cx) +} diff --git a/examples/simple_example.rs b/examples/simple_example.rs new file mode 100644 index 0000000..dd3c5fc --- /dev/null +++ b/examples/simple_example.rs @@ -0,0 +1,23 @@ +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 { "------ example" } + } + .view(&cx) +} + +#[component] +fn MyComponent(cx: &Arc, children: &Rsx) -> View { + rsx! { + @{children} + "Simple" + } + .view(&cx) +} diff --git a/macros/src/emit.rs b/macros/src/emit.rs index c4683c9..60884ca 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); + 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(); @@ -25,19 +29,35 @@ fn emit_deps(deps: &[Dep]) -> TokenStream { .collect() } -fn emit_node(node: &RsxNode) -> TokenStream { - match node { - RsxNode::Text(text) => quote! { - r.static_scope(|scope| scope.view(Arc::new(move |ctx| { - ctx.draw_text(Point { x: 0, y: 0 }, &format!(#text)) - }))); - }, +fn emit_deps_vec(deps: &[Dep]) -> TokenStream { + let deps = deps.iter().map(|d| { + let ident = &d.ident; - RsxNode::Component(name) => quote! { - r.static_scope(|scope| scope.view(Arc::new(move |ctx| { - scope.child(#name, None) - }))); - }, + quote! { + std::sync::Arc::new(#ident) as std::sync::Arc + } + }); + + quote! { + vec![ #(#deps),* ] + } +} + +fn emit_node_scope(node: &RsxNode) -> TokenStream { + match node { + RsxNode::Text(_) => { + let emit = emit_node(node); + quote! { + r.static_scope(move |scope| {#emit}); + } + } + + RsxNode::Component { .. } => { + let emit = emit_node(node); + quote! { + r.static_scope(move |scope| {#emit}); + } + } RsxNode::Mount(m) => quote! { #m.mount(); @@ -49,6 +69,7 @@ fn emit_node(node: &RsxNode) -> TokenStream { children, } => { let deps_emit = emit_deps(deps); + let deps_vec_emit = emit_deps_vec(deps); let kids = children.iter().map(emit_node); quote! { @@ -62,7 +83,7 @@ fn emit_node(node: &RsxNode) -> TokenStream { } else { scope.children.lock().unwrap().clear(); } - }); + }, #deps_vec_emit); } } } @@ -74,19 +95,80 @@ fn emit_node(node: &RsxNode) -> TokenStream { children, } => { let deps_emit = emit_deps(deps); + let deps_vec_emit = emit_deps_vec(deps); let kids = children.iter().map(emit_node); quote! { { #deps_emit + #[allow(unused_parens)] r.dynamic_scope(move |scope| { scope.children.lock().unwrap().clear(); for #pat in #expr { #(#kids)* } - }); + }, #deps_vec_emit); } } } + + RsxNode::Expr(expr) => quote! { + r.child(#expr); + }, + } +} + +fn emit_node(node: &RsxNode) -> TokenStream { + match node { + RsxNode::Text(text) => quote! { + scope.view(Arc::new(move |ctx| { + ctx.draw_text(Point { x: 0, y: 0 }, &format!(#text)) + })); + }, + + 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(); + }, + + RsxNode::If { .. } => panic!("Invalid if statement"), + + RsxNode::For { .. } => panic!("Invalid for loop"), + + RsxNode::Expr(expr) => quote! { + r.child(#expr); + }, } } diff --git a/macros/src/lib.rs b/macros/src/lib.rs index bb4e672..f13a496 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -1,10 +1,111 @@ use proc_macro::TokenStream; +use quote::quote; +use syn::{FnArg, ItemFn, Pat, ReturnType, Type, parse_macro_input}; mod emit; mod parse; #[proc_macro] pub fn rsx(input: TokenStream) -> TokenStream { - let ast = syn::parse_macro_input!(input as parse::RsxRoot); + let ast = parse_macro_input!(input as parse::RsxRoot); emit::emit_rsx(ast).into() } + +#[proc_macro_attribute] +pub fn component(_attr: TokenStream, item: TokenStream) -> TokenStream { + let input = parse_macro_input!(item as ItemFn); + + let name = &input.sig.ident; + let vis = &input.vis; + let body = &input.block; + + let return_ty = match &input.sig.output { + ReturnType::Type(_, ty) => ty, + _ => { + return syn::Error::new_spanned(&input.sig, "component must return View") + .to_compile_error() + .into(); + } + }; + + let mut inputs = input.sig.inputs.iter(); + + // ---- First param must be cx ---- + let cx = match inputs.next() { + Some(FnArg::Typed(pat)) => pat, + _ => { + return syn::Error::new_spanned(&input.sig, "first argument must be cx: &Arc") + .to_compile_error() + .into(); + } + }; + + let cx_ident = match &*cx.pat { + Pat::Ident(id) => &id.ident, + _ => unreachable!(), + }; + + let cx_ty = &cx.ty; + + // ---- Remaining params are props ---- + let mut struct_fields = Vec::new(); + let mut render_params = Vec::new(); + let mut call_args = Vec::new(); + + for arg in inputs { + let FnArg::Typed(pat) = arg else { continue }; + + let ident = match &*pat.pat { + Pat::Ident(id) => &id.ident, + _ => { + return syn::Error::new_spanned(pat, "unsupported prop pattern") + .to_compile_error() + .into(); + } + }; + + // Strip leading & + let owned_ty = match &*pat.ty { + Type::Reference(r) => &r.elem, + ty => ty, + }; + + struct_fields.push(quote! { + pub #ident: #owned_ty + }); + + render_params.push(quote! { + #ident: &#owned_ty + }); + + call_args.push(quote! { + &self.#ident + }); + } + + let expanded = quote! { + #vis struct #name { + #(#struct_fields,)* + } + + impl #name { + pub fn component( + #cx_ident: #cx_ty, + #(#render_params,)* + ) -> #return_ty { + #body + } + } + + impl ComponentImpl for #name { + fn call(&self, cx: &std::sync::Arc) -> #return_ty { + Self::component( + cx, + #(#call_args,)* + ) + } + } + }; + + expanded.into() +} diff --git a/macros/src/parse.rs b/macros/src/parse.rs index fc4ee19..7252ec3 100644 --- a/macros/src/parse.rs +++ b/macros/src/parse.rs @@ -1,7 +1,9 @@ use syn::braced; +use syn::parse::discouraged::Speculative; 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 +20,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(Ident), + Expr(Expr), + Component { + path: Path, + props: Vec, + children: Vec, + }, Mount(Ident), If { deps: Vec, @@ -70,6 +83,18 @@ fn parse_deps(input: ParseStream) -> Result> { impl Parse for RsxNode { fn parse(input: ParseStream) -> Result { + // @{ $expr } + if input.peek(Token![@]) { + input.parse::()?; + + let content; + braced!(content in input); + + let expr: Expr = content.parse()?; + return Ok(RsxNode::Expr(expr)); + } + + // !$ident if input.peek(Token![!]) { input.parse::()?; let mount: Ident = input.parse()?; @@ -78,13 +103,12 @@ impl Parse for RsxNode { let deps = parse_deps(input)?; + // %$dep if $expr { $rsx } if input.peek(Token![if]) { input.parse::()?; let cond: Expr = input.parse()?; - let content; braced!(content in input); - let children = parse_children(&content)?; return Ok(RsxNode::If { deps, @@ -93,16 +117,14 @@ impl Parse for RsxNode { }); } + // %$dep for $pat in $expr { $rsx } if input.peek(Token![for]) { input.parse::()?; - // Parse a possibly multi-pattern (e.g. with `|`) for the `for` binding. let pat: Pat = Pat::parse_multi(input)?; input.parse::()?; let expr: Expr = input.parse()?; - let content; braced!(content in input); - let children = parse_children(&content)?; return Ok(RsxNode::For { deps, @@ -112,14 +134,79 @@ impl Parse for RsxNode { }); } + // "%$dep for $pat in $expr { $rsx }" if input.peek(LitStr) { return Ok(RsxNode::Text(input.parse()?)); } - Ok(RsxNode::Component(input.parse()?)) + 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, + }) +} + +fn parse_props_and_children(input: ParseStream) -> Result<(Vec, Vec)> { + Ok((parse_props(input)?, RsxRoot::parse(input)?.nodes)) +} + +fn parse_props(input: ParseStream) -> Result> { + let mut props = Vec::new(); + + while !input.is_empty() { + let fork = input.fork(); + + let path: Path = match fork.parse() { + Ok(p) => p, + Err(_) => break, + }; + + if fork.peek(Token![:]) { + let name = path + .get_ident() + .cloned() + .ok_or_else(|| fork.error("prop name must be a single identifier"))?; + + fork.parse::()?; + let value: Expr = fork.parse()?; + + input.advance_to(&fork); + + props.push(RsxProp { name, value }); + + if input.peek(Token![,]) { + input.parse::()?; + } + } else { + // ❌ do not commit → nothing consumed + break; + } + } + + Ok(props) +} + fn parse_children(input: ParseStream) -> Result> { let mut nodes = Vec::new(); while !input.is_empty() { diff --git a/src/component.rs b/src/component/context.rs similarity index 80% rename from src/component.rs rename to src/component/context.rs index 90c910a..0a1a75d 100644 --- a/src/component.rs +++ b/src/component/context.rs @@ -5,19 +5,14 @@ use std::{ }; use crate::{ + component::EventHandler, engine::{Command, CommandExecutor}, render::DrawContext, state::{use_effect, HookDependency}, - View, ViewWrapper, + View, }; -pub type Component = Arc) -> View + Send + Sync>; -pub type EventHandler = Arc, &dyn Any) + Send + Sync>>; - -pub struct Scope { - pub children: Mutex, Option)>>, - executor: Arc, -} +use super::{scope::Scope, Component, ComponentImpl}; pub struct Context { component: Mutex, @@ -28,7 +23,7 @@ pub struct Context { } impl Context { - pub fn new) -> View + Send + Sync + 'static>( + pub fn new( component: F, executor: Arc, ) -> Arc { @@ -48,7 +43,7 @@ impl Context { move || { s.event_handlers.lock().unwrap().clear(); let c = s.component.lock().unwrap().clone(); - *s.view.lock().unwrap() = (c)(&s); + *s.view.lock().unwrap() = c.call(&s); } }); } @@ -62,7 +57,7 @@ impl Context { move || { s.event_handlers.lock().unwrap().clear(); let c = s.component.lock().unwrap().clone(); - *s.view.lock().unwrap() = (c)(&s); + *s.view.lock().unwrap() = c.call(&s); *done.lock().unwrap() = true; } }); @@ -185,32 +180,3 @@ impl Context { self.execute(crate::engine::commands::Stop) } } - -impl Scope { - pub fn new(executor: Arc) -> Arc { - Arc::new(Self { - children: Mutex::new(Vec::new()), - executor, - }) - } - - pub fn child) -> View + Send + Sync + 'static>( - self: &Arc, - child: F, - view_wrapper: Option, - ) { - let ctx = Context::new(child, self.executor.clone()); - - ctx.refresh(); - - self.children.lock().unwrap().push((ctx, view_wrapper)); - } - - pub fn view(self: &Arc, view: View) { - let ctx = Context::new(move |_| view.clone(), self.executor.clone()); - - ctx.refresh(); - - self.children.lock().unwrap().push((ctx, None)); - } -} diff --git a/src/component/mod.rs b/src/component/mod.rs new file mode 100644 index 0000000..f2f83a9 --- /dev/null +++ b/src/component/mod.rs @@ -0,0 +1,33 @@ +pub mod context; +pub mod scope; + +use std::{ + any::Any, + sync::{Arc, Mutex}, +}; + +use crate::View; + +use context::Context; + +pub type Component = Arc; +pub type EventHandler = Arc, &dyn Any) + Send + Sync>>; + +pub trait ComponentImpl: Send + Sync { + fn call(&self, cx: &Arc) -> View; +} + +impl ComponentImpl for View { + fn call(&self, _: &Arc) -> View { + self.clone() + } +} + +impl ComponentImpl for F +where + F: Fn(&Arc) -> View + Send + Sync, +{ + fn call(&self, ctx: &Arc) -> View { + self(ctx) + } +} diff --git a/src/component/scope.rs b/src/component/scope.rs new file mode 100644 index 0000000..6cfe6e5 --- /dev/null +++ b/src/component/scope.rs @@ -0,0 +1,39 @@ +use std::sync::{Arc, Mutex}; + +use crate::{engine::CommandExecutor, View, ViewWrapper}; + +use super::{context::Context, ComponentImpl}; + +pub struct Scope { + pub children: Mutex, Option)>>, + executor: Arc, +} + +impl Scope { + pub fn new(executor: Arc) -> Arc { + Arc::new(Self { + children: Mutex::new(Vec::new()), + executor, + }) + } + + pub fn child( + self: &Arc, + child: F, + view_wrapper: Option, + ) { + let ctx = Context::new(child, self.executor.clone()); + + ctx.refresh(); + + self.children.lock().unwrap().push((ctx, view_wrapper)); + } + + pub fn view(self: &Arc, view: View) { + let ctx = Context::new(view, self.executor.clone()); + + ctx.refresh(); + + self.children.lock().unwrap().push((ctx, None)); + } +} diff --git a/src/engine/benchmark.rs b/src/engine/benchmark.rs index df8c700..1843050 100644 --- a/src/engine/benchmark.rs +++ b/src/engine/benchmark.rs @@ -2,10 +2,20 @@ use std::{io::stdout, sync::Arc, time::Instant}; use crossterm::{cursor::MoveTo, execute, terminal::Clear}; -use crate::{prelude::Context, render::Area, DrawContext, View}; +use crate::component::{context::Context, ComponentImpl}; +use crate::{render::Area, DrawContext, View}; use super::Engine; +#[derive(Debug, Clone)] +pub struct BenchmarkResult { + pub average: u128, + pub min: u128, + pub max: u128, + pub total_render: u128, + pub total: u128, +} + pub struct Benchmark(T); impl Benchmark { @@ -14,19 +24,18 @@ impl Benchmark { } } -impl Engine for Benchmark { - fn run) -> View + Send + Sync + 'static>(&self, component: F) { - let mut times: Vec = Vec::new(); +impl Engine for Benchmark { + fn run(&self, component: F) -> crate::Result { + let mut times: Vec = Vec::new(); let cx = self.init(component); let start = Instant::now(); - for _ in 0..100 { + for _ in 0..40 { let start = Instant::now(); self.render(&cx); let end = Instant::now(); - times.push(end.duration_since(start).as_millis() as usize); - self.0.render_delay(); + times.push(end.duration_since(start).as_micros()); } let end = Instant::now(); @@ -35,40 +44,20 @@ impl Engine for Benchmark { execute!(stdout(), Clear(crossterm::terminal::ClearType::All)).unwrap(); execute!(stdout(), MoveTo(0, 0)).unwrap(); - println!( - "Average time: {}ms", - if times.len() > 0 { - times.iter().sum::() / times.len() + Ok(BenchmarkResult { + min: *times.iter().min().unwrap_or(&0), + max: *times.iter().max().unwrap_or(&0), + total: end.duration_since(start).as_micros(), + total_render: times.iter().sum::(), + average: if times.len() > 0 { + times.iter().sum::() / (times.len() as u128) } else { 0 - } - ); - println!("Min time: {}ms", times.iter().min().unwrap_or(&0)); - println!("Max time: {}ms", times.iter().max().unwrap_or(&0)); - println!( - "Median time: {}ms", - times.iter().nth(times.len() / 2).unwrap_or(&0) - ); - println!( - "90th percentile: {}ms", - times.iter().nth(times.len() * 90 / 100).unwrap_or(&0) - ); - println!( - "95th percentile: {}ms", - times.iter().nth(times.len() * 95 / 100).unwrap_or(&0) - ); - println!( - "99th percentile: {}ms", - times.iter().nth(times.len() * 99 / 100).unwrap_or(&0) - ); - - println!("Total time: {}ms", end.duration_since(start).as_millis()); + }, + }) } - fn init) -> View + Send + Sync + 'static>( - &self, - component: F, - ) -> Arc { + fn init(&self, component: F) -> Arc { self.0.init(component) } @@ -92,3 +81,17 @@ impl Engine for Benchmark { self.0.executor() } } + +impl std::fmt::Display for BenchmarkResult { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "- Average: {} µs\n\ + - Min: {} µs\n\ + - Max: {} µs\n\ + - Total Render: {} µs\n\ + - Total: {} µs", + self.average, self.min, self.max, self.total_render, self.total + ) + } +} diff --git a/src/engine/console.rs b/src/engine/console.rs index b8553a3..afa8cb2 100644 --- a/src/engine/console.rs +++ b/src/engine/console.rs @@ -5,9 +5,9 @@ use std::{ use crossterm::{cursor::MoveTo, execute, terminal::Clear}; +use crate::component::{context::Context, ComponentImpl}; use crate::{ engine::{commands, CommandExecutor}, - prelude::Context, render::Area, DrawContext, View, }; @@ -79,10 +79,9 @@ impl Engine for Console { )); } - fn init) -> View + Send + Sync + 'static>( - &self, - component: F, - ) -> Arc { + fn init(&self, component: C) -> Arc { + crossterm::execute!(std::io::stdout(), crossterm::cursor::Hide).unwrap(); + let cx = Context::new(component, self.executor.clone()); cx.refresh(); @@ -102,13 +101,15 @@ impl Engine for Console { self.executor.clone() } - fn run) -> View + Send + Sync + 'static>(&self, component: F) { + fn run(&self, component: F) -> crate::Result<()> { let cx = self.init(component); while self.executor.is_running() { self.render(&cx); self.render_delay(); } + + Ok(()) } } diff --git a/src/engine/mod.rs b/src/engine/mod.rs index bd28fb3..2ee53e5 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -7,25 +7,15 @@ pub use console::*; use std::{any::Any, sync::Arc}; -use crate::{prelude::Context, render::Area, DrawContext, View}; +use crate::component::{context::Context, ComponentImpl}; +use crate::{render::Area, DrawContext, View}; -pub trait Engine { - fn run) -> View + Send + Sync + 'static>(&self, component: F) { - let cx = self.init(component); - - loop { - self.render(&cx); - self.render_delay(); - } - } - - fn init) -> View + Send + Sync + 'static>( - &self, - component: F, - ) -> Arc; +pub trait Engine { + fn run(&self, component: C) -> crate::Result; + fn init(&self, component: C) -> Arc; fn render(&self, cx: &Arc); fn render_delay(&self) { - std::thread::sleep(std::time::Duration::from_millis(16)) + crate::sleep(16); } fn render_view(&self, area: &Area, view: &View) -> DrawContext; diff --git a/src/frontend.rs b/src/frontend.rs index bcabbb5..2689f75 100644 --- a/src/frontend.rs +++ b/src/frontend.rs @@ -1,19 +1,23 @@ use std::sync::Arc; -use crate::{ - component::{Context, Scope}, - state::HookDependency, - View, -}; +use crate::component::{context::Context, scope::Scope}; +use crate::{render::Point, state::HookDependency, View}; -pub enum RsxScope { - Static(Box) + Send + Sync>), - Dynamic( - Arc) + Send + Sync>, - Vec>, - ), +pub trait ToRsx { + fn to_rsx(&self) -> Rsx; } +#[derive(Clone)] +pub enum RsxScope { + Static(Arc) + Send + Sync>), + Dynamic( + Arc) + Send + Sync>, + Vec>, + ), + Child(Rsx), +} + +#[derive(Clone)] pub struct Rsx(Vec); impl Rsx { @@ -22,19 +26,23 @@ impl Rsx { } pub fn static_scope) + Send + Sync + 'static>(&mut self, scope: F) { - self.0.push(RsxScope::Static(Box::new(scope))); + self.0.push(RsxScope::Static(Arc::new(scope))); } pub fn dynamic_scope) + Send + Sync + 'static>( &mut self, drawer: F, - dependencies: Vec>, + dependencies: Vec>, ) { self.0 .push(RsxScope::Dynamic(Arc::new(drawer), dependencies)); } - pub fn view(&self, context: Arc) -> View { + pub fn child(&mut self, child: R) { + self.0.push(RsxScope::Child(child.to_rsx())); + } + + pub fn generate_children(&self, context: &Arc) { let executor = context.get_executor(); for scope in &self.0 { @@ -51,10 +59,16 @@ impl Rsx { &dependencies.iter().map(|d| d.as_ref()).collect::>(), ); } + RsxScope::Child(child) => child.generate_children(context), } } + } + pub fn view(&self, context: &Arc) -> View { let context = context.clone(); + + self.generate_children(&context); + Arc::new({ move |ctx| { context.draw_children(ctx); @@ -62,3 +76,19 @@ impl Rsx { }) } } + +impl ToRsx for &Rsx { + fn to_rsx(&self) -> Rsx { + Rsx(self.0.clone()) + } +} + +impl ToRsx for T { + fn to_rsx(&self) -> Rsx { + let s = self.to_string(); + Rsx(vec![RsxScope::Static(Arc::new(move |scope| { + let s = s.clone(); + scope.view(Arc::new(move |ctx| ctx.draw_text(Point { x: 0, y: 0 }, &s))) + }))]) + } +} diff --git a/src/lib.rs b/src/lib.rs index 7923bda..9d65be7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,12 +9,15 @@ pub mod render; pub mod state; pub mod prelude { - pub use crate::component::*; + pub use crate::component::{context::*, scope::*, *}; pub use crate::engine::*; + pub use crate::frontend::*; pub use crate::render::*; pub use crate::state::*; - pub use crate::View; - pub use osui_macros::rsx; + pub use crate::{sleep, Error, Result, View, ViewWrapper}; + pub use crossterm; + pub use osui_macros::{component, rsx}; + pub use std::sync::{Arc, Mutex}; } pub type View = Arc; @@ -31,3 +34,7 @@ impl From>> for Error { Error::PoisonError } } + +pub fn sleep(delay_ms: u64) { + std::thread::sleep(std::time::Duration::from_millis(delay_ms)); +} diff --git a/src/state.rs b/src/state.rs index 502e56c..fae81c6 100644 --- a/src/state.rs +++ b/src/state.rs @@ -5,7 +5,7 @@ use std::{ sync::{Arc, Mutex, MutexGuard}, }; -use crate::prelude::Context; +use crate::component::context::Context; #[derive(Clone)] pub struct HookEffect(Arc>); @@ -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); }