From 059f8f07c1c39a48f6964f59a83b0cd7f9bfe16b Mon Sep 17 00:00:00 2001 From: Leo dev Date: Thu, 29 Jan 2026 09:59:06 +0100 Subject: [PATCH 01/23] Nanosecond benchmarking --- src/engine/benchmark.rs | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/src/engine/benchmark.rs b/src/engine/benchmark.rs index df8c700..ce096d3 100644 --- a/src/engine/benchmark.rs +++ b/src/engine/benchmark.rs @@ -16,7 +16,7 @@ impl Benchmark { impl Engine for Benchmark { fn run) -> View + Send + Sync + 'static>(&self, component: F) { - let mut times: Vec = Vec::new(); + let mut times: Vec = Vec::new(); let cx = self.init(component); let start = Instant::now(); @@ -25,7 +25,7 @@ impl Engine for Benchmark { let start = Instant::now(); self.render(&cx); let end = Instant::now(); - times.push(end.duration_since(start).as_millis() as usize); + times.push(end.duration_since(start).as_nanos() as u64); self.0.render_delay(); } @@ -36,33 +36,27 @@ impl Engine for Benchmark { execute!(stdout(), MoveTo(0, 0)).unwrap(); println!( - "Average time: {}ms", + "Average time: {} nanoseconds", if times.len() > 0 { - times.iter().sum::() / times.len() + times.iter().sum::() / (times.len() as u64) } else { 0 } ); - println!("Min time: {}ms", times.iter().min().unwrap_or(&0)); - println!("Max time: {}ms", times.iter().max().unwrap_or(&0)); + println!("Min time: {} nanoseconds", times.iter().min().unwrap_or(&0)); + println!("Max time: {} nanoseconds", times.iter().max().unwrap_or(&0)); println!( - "Median time: {}ms", + "Median time: {} nanoseconds", times.iter().nth(times.len() / 2).unwrap_or(&0) ); println!( - "90th percentile: {}ms", - times.iter().nth(times.len() * 90 / 100).unwrap_or(&0) + "Total time: {} nanoseconds", + end.duration_since(start).as_nanos() ); println!( - "95th percentile: {}ms", - times.iter().nth(times.len() * 95 / 100).unwrap_or(&0) + "Total render time: {} nanoseconds", + times.iter().sum::() ); - 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>( From 74c4a976dd9f3663f698846609ab39c0784f3c66 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Thu, 29 Jan 2026 10:38:07 +0100 Subject: [PATCH 02/23] Separated simple benchmark --- examples/benchmark.rs | 2 +- examples/simple_benchmark.rs | 15 ++++++ src/engine/mod.rs | 4 +- src/engine/simple_benchmark.rs | 88 ++++++++++++++++++++++++++++++++++ 4 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 examples/simple_benchmark.rs create mode 100644 src/engine/simple_benchmark.rs diff --git a/examples/benchmark.rs b/examples/benchmark.rs index 4fc8e72..44ee985 100644 --- a/examples/benchmark.rs +++ b/examples/benchmark.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use osui::prelude::*; pub fn main() { - let engine = Benchmark::new(Console::new()); + let engine = SimpleBenchmark::new(Console::new()); engine.run(app); } diff --git a/examples/simple_benchmark.rs b/examples/simple_benchmark.rs new file mode 100644 index 0000000..44ee985 --- /dev/null +++ b/examples/simple_benchmark.rs @@ -0,0 +1,15 @@ +use std::sync::Arc; + +use osui::prelude::*; + +pub fn main() { + let engine = SimpleBenchmark::new(Console::new()); + engine.run(app); +} + +fn app(cx: &Arc) -> View { + rsx! { + "Hello, world!" + } + .view(cx.clone()) +} diff --git a/src/engine/mod.rs b/src/engine/mod.rs index bd28fb3..dd24824 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -1,8 +1,8 @@ -pub mod benchmark; +pub mod simple_benchmark; pub mod commands; pub mod console; -pub use benchmark::*; +pub use simple_benchmark::*; pub use console::*; use std::{any::Any, sync::Arc}; diff --git a/src/engine/simple_benchmark.rs b/src/engine/simple_benchmark.rs new file mode 100644 index 0000000..0893af4 --- /dev/null +++ b/src/engine/simple_benchmark.rs @@ -0,0 +1,88 @@ +use std::{io::stdout, sync::Arc, time::Instant}; + +use crossterm::{cursor::MoveTo, execute, terminal::Clear}; + +use crate::{prelude::Context, render::Area, DrawContext, View}; + +use super::Engine; + +pub struct SimpleBenchmark(T); + +impl SimpleBenchmark { + pub fn new(engine: T) -> Self { + Self(engine) + } +} + +impl Engine for SimpleBenchmark { + fn run) -> View + Send + Sync + 'static>(&self, component: F) { + let mut times: Vec = Vec::new(); + let cx = self.init(component); + + let start = Instant::now(); + + for _ in 0..100 { + let start = Instant::now(); + self.render(&cx); + let end = Instant::now(); + times.push(end.duration_since(start).as_nanos() as u64); + self.0.render_delay(); + } + + let end = Instant::now(); + + execute!(stdout(), Clear(crossterm::terminal::ClearType::Purge)).unwrap(); + execute!(stdout(), Clear(crossterm::terminal::ClearType::All)).unwrap(); + execute!(stdout(), MoveTo(0, 0)).unwrap(); + + println!( + "Average time: {} nanoseconds", + if times.len() > 0 { + times.iter().sum::() / (times.len() as u64) + } else { + 0 + } + ); + println!("Min time: {} nanoseconds", times.iter().min().unwrap_or(&0)); + println!("Max time: {} nanoseconds", times.iter().max().unwrap_or(&0)); + println!( + "Median time: {} nanoseconds", + times.iter().nth(times.len() / 2).unwrap_or(&0) + ); + println!( + "Total time: {} nanoseconds", + end.duration_since(start).as_nanos() + ); + println!( + "Total render time: {} nanoseconds", + times.iter().sum::() + ); + } + + fn init) -> View + Send + Sync + 'static>( + &self, + component: F, + ) -> Arc { + self.0.init(component) + } + + fn draw_context(&self, ctx: &DrawContext) { + self.0.draw_context(ctx) + } + + fn render(&self, cx: &Arc) { + self.0.render(cx) + } + + fn render_view(&self, area: &Area, view: &View) -> DrawContext { + self.0.render_view(area, view) + } + + fn render_delay(&self) { + self.0.render_delay(); + } + + fn executor(&self) -> Arc { + self.0.executor() + } +} From ccbbf29538d2c743f0dee33dfb9c77ed6e357572 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Thu, 29 Jan 2026 10:57:17 +0100 Subject: [PATCH 03/23] Separated simple benchmark --- examples/benchmark.rs | 2 +- src/engine/mod.rs | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/benchmark.rs b/examples/benchmark.rs index 44ee985..4fc8e72 100644 --- a/examples/benchmark.rs +++ b/examples/benchmark.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use osui::prelude::*; pub fn main() { - let engine = SimpleBenchmark::new(Console::new()); + let engine = Benchmark::new(Console::new()); engine.run(app); } diff --git a/src/engine/mod.rs b/src/engine/mod.rs index dd24824..adcafd7 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -1,9 +1,11 @@ -pub mod simple_benchmark; +pub mod benchmark; pub mod commands; pub mod console; +pub mod simple_benchmark; -pub use simple_benchmark::*; +pub use benchmark::*; pub use console::*; +pub use simple_benchmark::*; use std::{any::Any, sync::Arc}; From 2d79b1dfbbf041a3f4015d5557aaf00ceb2c77ca Mon Sep 17 00:00:00 2001 From: Leo dev Date: Fri, 30 Jan 2026 14:19:24 +0100 Subject: [PATCH 04/23] Implemented engine return --- examples/benchmark.rs | 2 +- examples/hello_world.rs | 4 ++-- examples/simple_benchmark.rs | 2 +- src/engine/benchmark.rs | 31 ++++++++++++++++--------------- src/engine/console.rs | 7 ++++++- src/engine/mod.rs | 14 +++++--------- src/engine/simple_benchmark.rs | 7 ++++++- 7 files changed, 37 insertions(+), 30 deletions(-) diff --git a/examples/benchmark.rs b/examples/benchmark.rs index 4fc8e72..6acd250 100644 --- a/examples/benchmark.rs +++ b/examples/benchmark.rs @@ -4,7 +4,7 @@ use osui::prelude::*; pub fn main() { let engine = Benchmark::new(Console::new()); - engine.run(app); + engine.run(app).expect("Failed to run engine"); } fn app(cx: &Arc) -> View { diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 9ba9489..eb1056f 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -3,8 +3,8 @@ 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 { diff --git a/examples/simple_benchmark.rs b/examples/simple_benchmark.rs index 44ee985..6927c9f 100644 --- a/examples/simple_benchmark.rs +++ b/examples/simple_benchmark.rs @@ -4,7 +4,7 @@ use osui::prelude::*; pub fn main() { let engine = SimpleBenchmark::new(Console::new()); - engine.run(app); + engine.run(app).expect("Failed to run engine"); } fn app(cx: &Arc) -> View { diff --git a/src/engine/benchmark.rs b/src/engine/benchmark.rs index ce096d3..95487c1 100644 --- a/src/engine/benchmark.rs +++ b/src/engine/benchmark.rs @@ -15,7 +15,10 @@ impl Benchmark { } impl Engine for Benchmark { - fn run) -> View + Send + Sync + 'static>(&self, component: F) { + fn run) -> View + Send + Sync + 'static>( + &self, + component: F, + ) -> crate::Result<()> { let mut times: Vec = Vec::new(); let cx = self.init(component); @@ -25,7 +28,7 @@ impl Engine for Benchmark { let start = Instant::now(); self.render(&cx); let end = Instant::now(); - times.push(end.duration_since(start).as_nanos() as u64); + times.push(end.duration_since(start).as_micros() as u64); self.0.render_delay(); } @@ -36,27 +39,25 @@ impl Engine for Benchmark { execute!(stdout(), MoveTo(0, 0)).unwrap(); println!( - "Average time: {} nanoseconds", + "Average time: {} µs", if times.len() > 0 { times.iter().sum::() / (times.len() as u64) } else { 0 } ); - println!("Min time: {} nanoseconds", times.iter().min().unwrap_or(&0)); - println!("Max time: {} nanoseconds", times.iter().max().unwrap_or(&0)); + println!("Min time: {} µs", times.iter().min().unwrap_or(&0)); + println!("Max time: {} µs", times.iter().max().unwrap_or(&0)); + println!("Total time: {} µs", end.duration_since(start).as_micros()); + println!("Total render time: {} µs", times.iter().sum::()); println!( - "Median time: {} nanoseconds", - times.iter().nth(times.len() / 2).unwrap_or(&0) - ); - println!( - "Total time: {} nanoseconds", - end.duration_since(start).as_nanos() - ); - println!( - "Total render time: {} nanoseconds", - times.iter().sum::() + "\n{},{},{}", + times.iter().min().unwrap_or(&0), + times.iter().max().unwrap_or(&0), + end.duration_since(start).as_micros() ); + + Ok(()) } fn init) -> View + Send + Sync + 'static>( diff --git a/src/engine/console.rs b/src/engine/console.rs index b8553a3..592ca01 100644 --- a/src/engine/console.rs +++ b/src/engine/console.rs @@ -102,13 +102,18 @@ impl Engine for Console { self.executor.clone() } - fn run) -> View + Send + Sync + 'static>(&self, component: F) { + fn run) -> View + Send + Sync + 'static>( + &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 adcafd7..05d9f2d 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -11,15 +11,11 @@ use std::{any::Any, sync::Arc}; use crate::{prelude::Context, 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(); - } - } +pub trait Engine { + fn run) -> View + Send + Sync + 'static>( + &self, + component: F, + ) -> crate::Result; fn init) -> View + Send + Sync + 'static>( &self, diff --git a/src/engine/simple_benchmark.rs b/src/engine/simple_benchmark.rs index 0893af4..267fa9e 100644 --- a/src/engine/simple_benchmark.rs +++ b/src/engine/simple_benchmark.rs @@ -15,7 +15,10 @@ impl SimpleBenchmark { } impl Engine for SimpleBenchmark { - fn run) -> View + Send + Sync + 'static>(&self, component: F) { + fn run) -> View + Send + Sync + 'static>( + &self, + component: F, + ) -> crate::Result<()> { let mut times: Vec = Vec::new(); let cx = self.init(component); @@ -57,6 +60,8 @@ impl Engine for SimpleBenchmark { "Total render time: {} nanoseconds", times.iter().sum::() ); + + Ok(()) } fn init) -> View + Send + Sync + 'static>( From 41e7b2a10c844386edb3ead9abc87132be50214a Mon Sep 17 00:00:00 2001 From: Leo dev Date: Fri, 30 Jan 2026 15:08:01 +0100 Subject: [PATCH 05/23] Benchmarking --- examples/benchmark.rs | 26 +++++++++- examples/simple_benchmark.rs | 9 +++- src/engine/benchmark.rs | 44 ++++++++-------- src/engine/mod.rs | 2 - src/engine/simple_benchmark.rs | 93 ---------------------------------- 5 files changed, 53 insertions(+), 121 deletions(-) delete mode 100644 src/engine/simple_benchmark.rs diff --git a/examples/benchmark.rs b/examples/benchmark.rs index 6acd250..bbea75e 100644 --- a/examples/benchmark.rs +++ b/examples/benchmark.rs @@ -4,12 +4,34 @@ use osui::prelude::*; pub fn main() { let engine = Benchmark::new(Console::new()); - engine.run(app).expect("Failed to run engine"); + + let mut benchmark_result = Vec::new(); + + for flat in 0..10 { + // for nest in 0..10 { + benchmark_result.push( + engine + .run(move |cx| { + // let sync_nest = use_state(nest); + cx.scope().child(app, None); + // use_sync_effect(cx, &sync_nest, |s| s.get_dl(), &[&sync_nest]); + + rsx! { + app + } + .view(cx.clone()) + }) + .expect("Failed to run engine"), + ); + // } + } } fn app(cx: &Arc) -> View { + // let nest = use_sync_state(cx, 0, |v| *v); + rsx! { - "Hello, world!" + "hello_world" } .view(cx.clone()) } diff --git a/examples/simple_benchmark.rs b/examples/simple_benchmark.rs index 6927c9f..811120e 100644 --- a/examples/simple_benchmark.rs +++ b/examples/simple_benchmark.rs @@ -3,8 +3,13 @@ use std::sync::Arc; use osui::prelude::*; pub fn main() { - let engine = SimpleBenchmark::new(Console::new()); - engine.run(app).expect("Failed to run engine"); + 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); } fn app(cx: &Arc) -> View { diff --git a/src/engine/benchmark.rs b/src/engine/benchmark.rs index 95487c1..da40612 100644 --- a/src/engine/benchmark.rs +++ b/src/engine/benchmark.rs @@ -6,6 +6,15 @@ use crate::{prelude::Context, 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,12 +23,12 @@ impl Benchmark { } } -impl Engine for Benchmark { +impl Engine for Benchmark { fn run) -> View + Send + Sync + 'static>( &self, component: F, - ) -> crate::Result<()> { - let mut times: Vec = Vec::new(); + ) -> crate::Result { + let mut times: Vec = Vec::new(); let cx = self.init(component); let start = Instant::now(); @@ -28,7 +37,7 @@ impl Engine for Benchmark { let start = Instant::now(); self.render(&cx); let end = Instant::now(); - times.push(end.duration_since(start).as_micros() as u64); + times.push(end.duration_since(start).as_micros()); self.0.render_delay(); } @@ -38,26 +47,17 @@ impl Engine for Benchmark { execute!(stdout(), Clear(crossterm::terminal::ClearType::All)).unwrap(); execute!(stdout(), MoveTo(0, 0)).unwrap(); - println!( - "Average time: {} µs", - if times.len() > 0 { - times.iter().sum::() / (times.len() as u64) + 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: {} µs", times.iter().min().unwrap_or(&0)); - println!("Max time: {} µs", times.iter().max().unwrap_or(&0)); - println!("Total time: {} µs", end.duration_since(start).as_micros()); - println!("Total render time: {} µs", times.iter().sum::()); - println!( - "\n{},{},{}", - times.iter().min().unwrap_or(&0), - times.iter().max().unwrap_or(&0), - end.duration_since(start).as_micros() - ); - - Ok(()) + }, + }) } fn init) -> View + Send + Sync + 'static>( diff --git a/src/engine/mod.rs b/src/engine/mod.rs index 05d9f2d..b2f039f 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -1,11 +1,9 @@ pub mod benchmark; pub mod commands; pub mod console; -pub mod simple_benchmark; pub use benchmark::*; pub use console::*; -pub use simple_benchmark::*; use std::{any::Any, sync::Arc}; diff --git a/src/engine/simple_benchmark.rs b/src/engine/simple_benchmark.rs deleted file mode 100644 index 267fa9e..0000000 --- a/src/engine/simple_benchmark.rs +++ /dev/null @@ -1,93 +0,0 @@ -use std::{io::stdout, sync::Arc, time::Instant}; - -use crossterm::{cursor::MoveTo, execute, terminal::Clear}; - -use crate::{prelude::Context, render::Area, DrawContext, View}; - -use super::Engine; - -pub struct SimpleBenchmark(T); - -impl SimpleBenchmark { - pub fn new(engine: T) -> Self { - Self(engine) - } -} - -impl Engine for SimpleBenchmark { - fn run) -> View + Send + Sync + 'static>( - &self, - component: F, - ) -> crate::Result<()> { - let mut times: Vec = Vec::new(); - let cx = self.init(component); - - let start = Instant::now(); - - for _ in 0..100 { - let start = Instant::now(); - self.render(&cx); - let end = Instant::now(); - times.push(end.duration_since(start).as_nanos() as u64); - self.0.render_delay(); - } - - let end = Instant::now(); - - execute!(stdout(), Clear(crossterm::terminal::ClearType::Purge)).unwrap(); - execute!(stdout(), Clear(crossterm::terminal::ClearType::All)).unwrap(); - execute!(stdout(), MoveTo(0, 0)).unwrap(); - - println!( - "Average time: {} nanoseconds", - if times.len() > 0 { - times.iter().sum::() / (times.len() as u64) - } else { - 0 - } - ); - println!("Min time: {} nanoseconds", times.iter().min().unwrap_or(&0)); - println!("Max time: {} nanoseconds", times.iter().max().unwrap_or(&0)); - println!( - "Median time: {} nanoseconds", - times.iter().nth(times.len() / 2).unwrap_or(&0) - ); - println!( - "Total time: {} nanoseconds", - end.duration_since(start).as_nanos() - ); - println!( - "Total render time: {} nanoseconds", - times.iter().sum::() - ); - - Ok(()) - } - - fn init) -> View + Send + Sync + 'static>( - &self, - component: F, - ) -> Arc { - self.0.init(component) - } - - fn draw_context(&self, ctx: &DrawContext) { - self.0.draw_context(ctx) - } - - fn render(&self, cx: &Arc) { - self.0.render(cx) - } - - fn render_view(&self, area: &Area, view: &View) -> DrawContext { - self.0.render_view(area, view) - } - - fn render_delay(&self) { - self.0.render_delay(); - } - - fn executor(&self) -> Arc { - self.0.executor() - } -} From 41f4ad1022c4f516ac8cf5edefd26ee47df7d72c Mon Sep 17 00:00:00 2001 From: Leo dev Date: Fri, 30 Jan 2026 17:03:18 +0100 Subject: [PATCH 06/23] Fixed rsx! for loops and if statements --- examples/benchmark.rs | 37 --------------------- macros/src/emit.rs | 77 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 62 insertions(+), 52 deletions(-) delete mode 100644 examples/benchmark.rs diff --git a/examples/benchmark.rs b/examples/benchmark.rs deleted file mode 100644 index bbea75e..0000000 --- a/examples/benchmark.rs +++ /dev/null @@ -1,37 +0,0 @@ -use std::sync::Arc; - -use osui::prelude::*; - -pub fn main() { - let engine = Benchmark::new(Console::new()); - - let mut benchmark_result = Vec::new(); - - for flat in 0..10 { - // for nest in 0..10 { - benchmark_result.push( - engine - .run(move |cx| { - // let sync_nest = use_state(nest); - cx.scope().child(app, None); - // use_sync_effect(cx, &sync_nest, |s| s.get_dl(), &[&sync_nest]); - - rsx! { - app - } - .view(cx.clone()) - }) - .expect("Failed to run engine"), - ); - // } - } -} - -fn app(cx: &Arc) -> View { - // let nest = use_sync_state(cx, 0, |v| *v); - - rsx! { - "hello_world" - } - .view(cx.clone()) -} diff --git a/macros/src/emit.rs b/macros/src/emit.rs index c4683c9..6ad9c85 100644 --- a/macros/src/emit.rs +++ b/macros/src/emit.rs @@ -3,7 +3,7 @@ use proc_macro2::TokenStream; use quote::quote; pub fn emit_rsx(root: RsxRoot) -> TokenStream { - let nodes = root.nodes.iter().map(emit_node); + let nodes = root.nodes.iter().map(emit_node_scope); quote! {{ let mut r = osui::frontend::Rsx::new(); @@ -25,19 +25,42 @@ 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) - }))); - }, + if let Some(pat) = &d.pat { + quote! {{ + let #pat = #ident.clone(); + Box::new(#pat) as Box + }} + } else { + quote! { + Box::new(#ident.clone()) as Box + } + } + }); + + quote! { + vec![ #(#deps),* ] + } +} + +fn emit_node_scope(node: &RsxNode) -> TokenStream { + match node { + RsxNode::Text(_) => { + let emit = emit_node(node); + quote! { + r.static_scope(|scope| {#emit}); + } + } + + RsxNode::Component(_) => { + let emit = emit_node(node); + quote! { + r.static_scope(|scope| {#emit}); + } + } RsxNode::Mount(m) => quote! { #m.mount(); @@ -49,6 +72,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 +86,7 @@ fn emit_node(node: &RsxNode) -> TokenStream { } else { scope.children.lock().unwrap().clear(); } - }); + }, #deps_vec_emit); } } } @@ -74,6 +98,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! { @@ -84,9 +109,31 @@ fn emit_node(node: &RsxNode) -> TokenStream { for #pat in #expr { #(#kids)* } - }); + }, #deps_vec_emit); } } } } } + +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(name) => quote! { + scope.child(#name, None); + }, + + RsxNode::Mount(m) => quote! { + #m.mount(); + }, + + RsxNode::If { .. } => panic!("Invalid if statement"), + + RsxNode::For { .. } => panic!("Invalid for loop"), + } +} From c9a7e3c835c9f8519a3deae636b618d52485cde9 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Fri, 30 Jan 2026 17:15:12 +0100 Subject: [PATCH 07/23] Improved benchmark --- examples/benchmark.rs | 25 +++++++++++++++++++++++++ macros/src/emit.rs | 1 + 2 files changed, 26 insertions(+) create mode 100644 examples/benchmark.rs diff --git a/examples/benchmark.rs b/examples/benchmark.rs new file mode 100644 index 0000000..24a5d2c --- /dev/null +++ b/examples/benchmark.rs @@ -0,0 +1,25 @@ +use std::sync::Arc; + +use osui::prelude::*; + +pub fn main() { + let engine = Benchmark::new(Console::new()); + let mut benchmark_result = Vec::new(); + + for items in 0..10 { + benchmark_result.push( + engine + .run(move |cx| { + rsx! { + for _ in (0..items) { + "Hello, world!" + } + } + .view(cx.clone()) + }) + .expect("Failed to run engine"), + ); + } + + println!("{benchmark_result:?}"); +} diff --git a/macros/src/emit.rs b/macros/src/emit.rs index 6ad9c85..f41e3bc 100644 --- a/macros/src/emit.rs +++ b/macros/src/emit.rs @@ -104,6 +104,7 @@ fn emit_node_scope(node: &RsxNode) -> TokenStream { quote! { { #deps_emit + #[allow(unused_parens)] r.dynamic_scope(move |scope| { scope.children.lock().unwrap().clear(); for #pat in #expr { From 340b8c6f4d851e8d59c7e8fd1fe9f61c4c7543cb Mon Sep 17 00:00:00 2001 From: Leo dev Date: Fri, 30 Jan 2026 17:35:58 +0100 Subject: [PATCH 08/23] Multithreaded benchmark --- examples/benchmark.rs | 47 +++++++++++++++++++++++++++++++++-------- src/engine/benchmark.rs | 14 ++++++++++++ 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/examples/benchmark.rs b/examples/benchmark.rs index 24a5d2c..ae15e3a 100644 --- a/examples/benchmark.rs +++ b/examples/benchmark.rs @@ -1,14 +1,24 @@ -use std::sync::Arc; +use std::{ + collections::{HashMap, HashSet}, + sync::{Arc, Mutex}, +}; use osui::prelude::*; pub fn main() { - let engine = Benchmark::new(Console::new()); - let mut benchmark_result = Vec::new(); + let engine = Arc::new(Benchmark::new(Console::new())); + let benchmark_result = Arc::new(Mutex::new(HashMap::new())); + let benchmark_running = Arc::new(Mutex::new(HashSet::new())); - for items in 0..10 { - benchmark_result.push( - engine + for items in 0..1000 { + let engine = engine.clone(); + let benchmark_result = benchmark_result.clone(); + let benchmark_running = benchmark_running.clone(); + + std::thread::spawn(move || { + benchmark_running.lock().unwrap().insert(items); + + let res = engine .run(move |cx| { rsx! { for _ in (0..items) { @@ -17,9 +27,28 @@ pub fn main() { } .view(cx.clone()) }) - .expect("Failed to run engine"), - ); + .expect("Failed to run engine"); + + benchmark_result.lock().unwrap().insert(items, res); + benchmark_running.lock().unwrap().remove(&items); + }); } - println!("{benchmark_result:?}"); + while !benchmark_running.lock().unwrap().is_empty() { + std::thread::sleep(std::time::Duration::from_millis(500)); + } + + let r = benchmark_result.lock().unwrap().clone(); + let mut bench_results = r.iter().collect::>(); + + bench_results.sort_by_key(|(_, bench)| bench.total_render); + + println!( + "{}", + bench_results + .iter() + .map(|(key, bench)| format!("Results for {key} items:\n{bench}")) + .collect::>() + .join("\n") + ); } diff --git a/src/engine/benchmark.rs b/src/engine/benchmark.rs index da40612..90a9fcf 100644 --- a/src/engine/benchmark.rs +++ b/src/engine/benchmark.rs @@ -87,3 +87,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: {} ns\n\ + - Min: {} ns\n\ + - Max: {} ns\n\ + - Total Render: {} ns\n\ + - Total: {} ns", + self.average, self.min, self.max, self.total_render, self.total + ) + } +} From 5e2e9952d96f854163b1f5f9d1d77a48004d706e Mon Sep 17 00:00:00 2001 From: Leo dev Date: Fri, 30 Jan 2026 17:39:15 +0100 Subject: [PATCH 09/23] Benchmark result in microseconds --- src/engine/benchmark.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/engine/benchmark.rs b/src/engine/benchmark.rs index 90a9fcf..80adfbb 100644 --- a/src/engine/benchmark.rs +++ b/src/engine/benchmark.rs @@ -92,11 +92,11 @@ impl std::fmt::Display for BenchmarkResult { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "- Average: {} ns\n\ - - Min: {} ns\n\ - - Max: {} ns\n\ - - Total Render: {} ns\n\ - - Total: {} ns", + "- 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 ) } From 1526daaba257b6b26d6091a76c3c2a50b3533c38 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Fri, 30 Jan 2026 18:07:28 +0100 Subject: [PATCH 10/23] Chunked threads --- examples/benchmark.rs | 75 ++++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 40 deletions(-) diff --git a/examples/benchmark.rs b/examples/benchmark.rs index ae15e3a..c06152f 100644 --- a/examples/benchmark.rs +++ b/examples/benchmark.rs @@ -1,54 +1,49 @@ -use std::{ - collections::{HashMap, HashSet}, - sync::{Arc, Mutex}, -}; +use std::{sync::mpsc, sync::Arc, thread}; use osui::prelude::*; pub fn main() { let engine = Arc::new(Benchmark::new(Console::new())); - let benchmark_result = Arc::new(Mutex::new(HashMap::new())); - let benchmark_running = Arc::new(Mutex::new(HashSet::new())); - for items in 0..1000 { - let engine = engine.clone(); - let benchmark_result = benchmark_result.clone(); - let benchmark_running = benchmark_running.clone(); + let (tx, rx) = mpsc::channel(); - std::thread::spawn(move || { - benchmark_running.lock().unwrap().insert(items); + let max_threads = 128; + let mut handles = vec![]; - let res = engine - .run(move |cx| { - rsx! { - for _ in (0..items) { - "Hello, world!" + 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(move |cx| { + rsx! { + for _ in (0..items) { + "Hello, world!" + } } - } - .view(cx.clone()) - }) - .expect("Failed to run engine"); + .view(cx.clone()) + }) + .expect("Failed to run engine"); - benchmark_result.lock().unwrap().insert(items, res); - benchmark_running.lock().unwrap().remove(&items); - }); + tx.send((items, res)).expect("Failed to send result"); + }); + + handles.push(handle); + } + + for handle in handles.drain(..) { + handle.join().expect("Thread panicked"); + } } - while !benchmark_running.lock().unwrap().is_empty() { - std::thread::sleep(std::time::Duration::from_millis(500)); + 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}"); } - - let r = benchmark_result.lock().unwrap().clone(); - let mut bench_results = r.iter().collect::>(); - - bench_results.sort_by_key(|(_, bench)| bench.total_render); - - println!( - "{}", - bench_results - .iter() - .map(|(key, bench)| format!("Results for {key} items:\n{bench}")) - .collect::>() - .join("\n") - ); } From 2799faaa73cdf0f8b5f87190e7cf7d536e454539 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Fri, 30 Jan 2026 18:23:46 +0100 Subject: [PATCH 11/23] Small stuff --- examples/benchmark.rs | 3 ++- src/engine/benchmark.rs | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/benchmark.rs b/examples/benchmark.rs index c06152f..2346ff4 100644 --- a/examples/benchmark.rs +++ b/examples/benchmark.rs @@ -7,7 +7,7 @@ pub fn main() { let (tx, rx) = mpsc::channel(); - let max_threads = 128; + let max_threads = 32; let mut handles = vec![]; for chunk in (0..500).collect::>().chunks(max_threads) { @@ -46,4 +46,5 @@ pub fn main() { for (items, bench) in results { println!("Results for {items} items:\n{bench}"); } + } diff --git a/src/engine/benchmark.rs b/src/engine/benchmark.rs index 80adfbb..99a7722 100644 --- a/src/engine/benchmark.rs +++ b/src/engine/benchmark.rs @@ -33,12 +33,11 @@ impl Engine for Benchmark { 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_micros()); - self.0.render_delay(); } let end = Instant::now(); From 1f64b4f6e22be29af6a25bd149e028ec50ca2f5b Mon Sep 17 00:00:00 2001 From: Leo dev Date: Fri, 30 Jan 2026 19:02:26 +0100 Subject: [PATCH 12/23] Component params --- examples/benchmark.rs | 24 +++++++++++++++--------- examples/hello_world.rs | 14 +++++++++----- examples/simple_benchmark.rs | 14 +++++++++----- src/component.rs | 31 +++++++++++++++++++++++++------ src/engine/benchmark.rs | 16 +++++++--------- src/engine/console.rs | 12 +++--------- src/engine/mod.rs | 17 +++++++---------- 7 files changed, 75 insertions(+), 53 deletions(-) diff --git a/examples/benchmark.rs b/examples/benchmark.rs index 2346ff4..110860d 100644 --- a/examples/benchmark.rs +++ b/examples/benchmark.rs @@ -17,14 +17,7 @@ pub fn main() { let handle = thread::spawn(move || { let res = engine - .run(move |cx| { - rsx! { - for _ in (0..items) { - "Hello, world!" - } - } - .view(cx.clone()) - }) + .run(BenchmarkApp(items)) .expect("Failed to run engine"); tx.send((items, res)).expect("Failed to send result"); @@ -46,5 +39,18 @@ pub fn main() { for (items, bench) in results { println!("Results for {items} items:\n{bench}"); } - +} + +struct BenchmarkApp(pub usize); + +impl ComponentImpl for BenchmarkApp { + fn call(&self, cx: &Arc) -> View { + let items = self.0.clone(); + rsx! { + for _ in (0..items) { + "Hello, world!" + } + } + .view(cx.clone()) + } } diff --git a/examples/hello_world.rs b/examples/hello_world.rs index eb1056f..9807544 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -4,12 +4,16 @@ use osui::prelude::*; pub fn main() { let engine = Console::new(); - engine.run(app).expect("Failed to run engine"); + engine.run(App).expect("Failed to run engine"); } -fn app(cx: &Arc) -> View { - rsx! { - "Hello, world!" +struct App; + +impl ComponentImpl for App { + fn call(&self, cx: &Arc) -> View { + rsx! { + "Hello, world!" + } + .view(cx.clone()) } - .view(cx.clone()) } diff --git a/examples/simple_benchmark.rs b/examples/simple_benchmark.rs index 811120e..ece76c0 100644 --- a/examples/simple_benchmark.rs +++ b/examples/simple_benchmark.rs @@ -4,7 +4,7 @@ use osui::prelude::*; pub fn main() { let engine = Benchmark::new(Console::new()); - let benchmark_result = engine.run(app).expect("Failed to run engine"); + 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); @@ -12,9 +12,13 @@ pub fn main() { println!("Tot Render: {} μs", benchmark_result.total_render); } -fn app(cx: &Arc) -> View { - rsx! { - "Hello, world!" +struct App; + +impl ComponentImpl for App { + fn call(&self, cx: &Arc) -> View { + rsx! { + "Hello, world!" + } + .view(cx.clone()) } - .view(cx.clone()) } diff --git a/src/component.rs b/src/component.rs index 90c910a..9928804 100644 --- a/src/component.rs +++ b/src/component.rs @@ -11,9 +11,13 @@ use crate::{ View, ViewWrapper, }; -pub type Component = Arc) -> View + Send + Sync>; +pub type Component = Arc; pub type EventHandler = Arc, &dyn Any) + Send + Sync>>; +pub trait ComponentImpl: Send + Sync { + fn call(&self, cx: &Arc) -> View; +} + pub struct Scope { pub children: Mutex, Option)>>, executor: Arc, @@ -28,7 +32,7 @@ pub struct Context { } impl Context { - pub fn new) -> View + Send + Sync + 'static>( + pub fn new( component: F, executor: Arc, ) -> Arc { @@ -48,7 +52,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 +66,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; } }); @@ -194,7 +198,7 @@ impl Scope { }) } - pub fn child) -> View + Send + Sync + 'static>( + pub fn child( self: &Arc, child: F, view_wrapper: Option, @@ -207,10 +211,25 @@ impl Scope { } pub fn view(self: &Arc, view: View) { - let ctx = Context::new(move |_| view.clone(), self.executor.clone()); + let ctx = Context::new(view, self.executor.clone()); ctx.refresh(); self.children.lock().unwrap().push((ctx, None)); } } + +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/engine/benchmark.rs b/src/engine/benchmark.rs index 99a7722..a371bf5 100644 --- a/src/engine/benchmark.rs +++ b/src/engine/benchmark.rs @@ -2,7 +2,11 @@ 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::{ + prelude::{ComponentImpl, Context}, + render::Area, + DrawContext, View, +}; use super::Engine; @@ -24,10 +28,7 @@ impl Benchmark { } impl Engine for Benchmark { - fn run) -> View + Send + Sync + 'static>( - &self, - component: F, - ) -> crate::Result { + fn run(&self, component: F) -> crate::Result { let mut times: Vec = Vec::new(); let cx = self.init(component); @@ -59,10 +60,7 @@ impl Engine for Benchmark { }) } - fn init) -> View + Send + Sync + 'static>( - &self, - component: F, - ) -> Arc { + fn init(&self, component: F) -> Arc { self.0.init(component) } diff --git a/src/engine/console.rs b/src/engine/console.rs index 592ca01..7f82d54 100644 --- a/src/engine/console.rs +++ b/src/engine/console.rs @@ -7,7 +7,7 @@ use crossterm::{cursor::MoveTo, execute, terminal::Clear}; use crate::{ engine::{commands, CommandExecutor}, - prelude::Context, + prelude::{ComponentImpl, Context}, render::Area, DrawContext, View, }; @@ -79,10 +79,7 @@ impl Engine for Console { )); } - fn init) -> View + Send + Sync + 'static>( - &self, - component: F, - ) -> Arc { + fn init(&self, component: C) -> Arc { let cx = Context::new(component, self.executor.clone()); cx.refresh(); @@ -102,10 +99,7 @@ impl Engine for Console { self.executor.clone() } - fn run) -> View + Send + Sync + 'static>( - &self, - component: F, - ) -> crate::Result<()> { + fn run(&self, component: F) -> crate::Result<()> { let cx = self.init(component); while self.executor.is_running() { diff --git a/src/engine/mod.rs b/src/engine/mod.rs index b2f039f..c0f5525 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -7,18 +7,15 @@ pub use console::*; use std::{any::Any, sync::Arc}; -use crate::{prelude::Context, render::Area, DrawContext, View}; +use crate::{ + prelude::{ComponentImpl, Context}, + render::Area, + DrawContext, View, +}; pub trait Engine { - fn run) -> View + Send + Sync + 'static>( - &self, - component: F, - ) -> crate::Result; - - fn init) -> View + Send + Sync + 'static>( - &self, - component: F, - ) -> Arc; + 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)) From e85257a10e684bf5e5709640f5c76ede3b6c66d6 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Fri, 30 Jan 2026 19:29:52 +0100 Subject: [PATCH 13/23] component generator --- examples/benchmark.rs | 18 +++--- examples/hello_world.rs | 15 ++--- examples/simple_benchmark.rs | 15 ++--- macros/src/emit.rs | 4 +- macros/src/lib.rs | 103 ++++++++++++++++++++++++++++++++++- src/lib.rs | 2 +- 6 files changed, 125 insertions(+), 32 deletions(-) diff --git a/examples/benchmark.rs b/examples/benchmark.rs index 110860d..6b2e8a2 100644 --- a/examples/benchmark.rs +++ b/examples/benchmark.rs @@ -17,7 +17,7 @@ pub fn main() { let handle = thread::spawn(move || { let res = engine - .run(BenchmarkApp(items)) + .run(BenchmarkApp { items }) .expect("Failed to run engine"); tx.send((items, res)).expect("Failed to send result"); @@ -41,16 +41,14 @@ pub fn main() { } } -struct BenchmarkApp(pub usize); +#[component] +fn BenchmarkApp(cx: &Arc, items: &usize) -> View { + let items = items.clone(); -impl ComponentImpl for BenchmarkApp { - fn call(&self, cx: &Arc) -> View { - let items = self.0.clone(); - rsx! { - for _ in (0..items) { - "Hello, world!" - } + rsx! { + for _ in (0..items) { + "Hello, world!" } - .view(cx.clone()) } + .view(cx.clone()) } diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 9807544..7bb27da 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -4,16 +4,13 @@ use osui::prelude::*; pub fn main() { let engine = Console::new(); - engine.run(App).expect("Failed to run engine"); + engine.run(App {}).expect("Failed to run engine"); } -struct App; - -impl ComponentImpl for App { - fn call(&self, cx: &Arc) -> View { - rsx! { - "Hello, world!" - } - .view(cx.clone()) +#[component] +fn App(cx: &Arc) -> View { + rsx! { + "Hello World" } + .view(cx.clone()) } diff --git a/examples/simple_benchmark.rs b/examples/simple_benchmark.rs index ece76c0..539eeaf 100644 --- a/examples/simple_benchmark.rs +++ b/examples/simple_benchmark.rs @@ -4,7 +4,7 @@ use osui::prelude::*; pub fn main() { let engine = Benchmark::new(Console::new()); - let benchmark_result = engine.run(App).expect("Failed to run engine"); + 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); @@ -12,13 +12,10 @@ pub fn main() { println!("Tot Render: {} μs", benchmark_result.total_render); } -struct App; - -impl ComponentImpl for App { - fn call(&self, cx: &Arc) -> View { - rsx! { - "Hello, world!" - } - .view(cx.clone()) +#[component] +fn App(cx: &Arc) -> View { + rsx! { + "Hello, world!" } + .view(cx.clone()) } diff --git a/macros/src/emit.rs b/macros/src/emit.rs index f41e3bc..e47f393 100644 --- a/macros/src/emit.rs +++ b/macros/src/emit.rs @@ -51,14 +51,14 @@ fn emit_node_scope(node: &RsxNode) -> TokenStream { RsxNode::Text(_) => { let emit = emit_node(node); quote! { - r.static_scope(|scope| {#emit}); + r.static_scope(move |scope| {#emit}); } } RsxNode::Component(_) => { let emit = emit_node(node); quote! { - r.static_scope(|scope| {#emit}); + r.static_scope(move |scope| {#emit}); } } 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/src/lib.rs b/src/lib.rs index 7923bda..b282e53 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,7 +14,7 @@ pub mod prelude { pub use crate::render::*; pub use crate::state::*; pub use crate::View; - pub use osui_macros::rsx; + pub use osui_macros::{component, rsx}; } pub type View = Arc; From 91f0878edcd2c8016ddc3dd27591d57ea10e6b56 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Fri, 30 Jan 2026 21:14:41 +0100 Subject: [PATCH 14/23] rsx params --- macros/src/emit.rs | 4 ++-- macros/src/parse.rs | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/macros/src/emit.rs b/macros/src/emit.rs index e47f393..64eec83 100644 --- a/macros/src/emit.rs +++ b/macros/src/emit.rs @@ -125,8 +125,8 @@ fn emit_node(node: &RsxNode) -> TokenStream { })); }, - RsxNode::Component(name) => quote! { - scope.child(#name, None); + RsxNode::Component(expr) => quote! { + scope.child(#expr, None); }, RsxNode::Mount(m) => quote! { diff --git a/macros/src/parse.rs b/macros/src/parse.rs index fc4ee19..d97c6f7 100644 --- a/macros/src/parse.rs +++ b/macros/src/parse.rs @@ -20,7 +20,7 @@ impl Parse for RsxRoot { pub enum RsxNode { Text(LitStr), - Component(Ident), + Component(Expr), Mount(Ident), If { deps: Vec, @@ -116,6 +116,7 @@ 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()?)) } } From 03191b0a5714949dfa8406230426e990656c31d9 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Fri, 30 Jan 2026 22:54:56 +0100 Subject: [PATCH 15/23] Rsx children --- examples/simple_example.rs | 21 +++++++++ macros/src/emit.rs | 43 +++++++++++++++--- macros/src/parse.rs | 89 ++++++++++++++++++++++++++++++++++++-- src/lib.rs | 1 + src/state.rs | 4 +- 5 files changed, 147 insertions(+), 11 deletions(-) create mode 100644 examples/simple_example.rs 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); } From 74e33fa4c2607ca5dd0e15fcd0565162445a87ea Mon Sep 17 00:00:00 2001 From: Leo dev Date: Fri, 30 Jan 2026 23:00:02 +0100 Subject: [PATCH 16/23] Frontend generate children --- examples/benchmark.rs | 2 +- examples/hello_world.rs | 2 +- examples/simple_benchmark.rs | 2 +- examples/simple_example.rs | 4 ++-- src/frontend.rs | 9 ++++++++- 5 files changed, 13 insertions(+), 6 deletions(-) diff --git a/examples/benchmark.rs b/examples/benchmark.rs index 6b2e8a2..4d45b4f 100644 --- a/examples/benchmark.rs +++ b/examples/benchmark.rs @@ -50,5 +50,5 @@ fn BenchmarkApp(cx: &Arc, items: &usize) -> View { "Hello, world!" } } - .view(cx.clone()) + .view(&cx) } diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 7bb27da..0a9850d 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -12,5 +12,5 @@ fn App(cx: &Arc) -> View { rsx! { "Hello World" } - .view(cx.clone()) + .view(&cx) } diff --git a/examples/simple_benchmark.rs b/examples/simple_benchmark.rs index 539eeaf..e902b10 100644 --- a/examples/simple_benchmark.rs +++ b/examples/simple_benchmark.rs @@ -17,5 +17,5 @@ fn App(cx: &Arc) -> View { rsx! { "Hello, world!" } - .view(cx.clone()) + .view(&cx) } diff --git a/examples/simple_example.rs b/examples/simple_example.rs index 8fc4bb0..fc453ff 100644 --- a/examples/simple_example.rs +++ b/examples/simple_example.rs @@ -12,10 +12,10 @@ fn App(cx: &Arc) -> View { rsx! { MyComponent { "Hello world!" } } - .view(cx.clone()) + .view(&cx) } #[component] fn MyComponent(cx: &Arc, children: &Rsx) -> View { - children.view(cx.clone()) + children.view(&cx) } diff --git a/src/frontend.rs b/src/frontend.rs index bcabbb5..11a10f2 100644 --- a/src/frontend.rs +++ b/src/frontend.rs @@ -12,6 +12,7 @@ pub enum RsxScope { Arc) + Send + Sync>, Vec>, ), + Child(Rsx), } pub struct Rsx(Vec); @@ -34,7 +35,7 @@ impl Rsx { .push(RsxScope::Dynamic(Arc::new(drawer), dependencies)); } - pub fn view(&self, context: Arc) -> View { + pub fn generate_children(&self, context: &Arc) { let executor = context.get_executor(); for scope in &self.0 { @@ -51,10 +52,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); From e31c17ba41f6b6fe489617dab268c2f3563b761c Mon Sep 17 00:00:00 2001 From: Leo dev Date: Fri, 30 Jan 2026 23:13:05 +0100 Subject: [PATCH 17/23] Rsx children scope placing --- examples/simple_example.rs | 6 +++++- macros/src/emit.rs | 8 ++++++++ macros/src/parse.rs | 20 +++++++++++++------- src/frontend.rs | 24 ++++++++++++++++++++---- 4 files changed, 46 insertions(+), 12 deletions(-) diff --git a/examples/simple_example.rs b/examples/simple_example.rs index fc453ff..dbd0ca1 100644 --- a/examples/simple_example.rs +++ b/examples/simple_example.rs @@ -17,5 +17,9 @@ fn App(cx: &Arc) -> View { #[component] fn MyComponent(cx: &Arc, children: &Rsx) -> View { - children.view(&cx) + rsx! { + "Simple" + @{children} + } + .view(&cx) } diff --git a/macros/src/emit.rs b/macros/src/emit.rs index 96a277f..6595c93 100644 --- a/macros/src/emit.rs +++ b/macros/src/emit.rs @@ -118,6 +118,10 @@ fn emit_node_scope(node: &RsxNode) -> TokenStream { } } } + + RsxNode::Expr(expr) => quote! { + r.child(#expr); + }, } } @@ -169,5 +173,9 @@ fn emit_node(node: &RsxNode) -> TokenStream { RsxNode::If { .. } => panic!("Invalid if statement"), RsxNode::For { .. } => panic!("Invalid for loop"), + + RsxNode::Expr(expr) => quote! { + r.child(#expr); + }, } } diff --git a/macros/src/parse.rs b/macros/src/parse.rs index ec49e72..29a8d9a 100644 --- a/macros/src/parse.rs +++ b/macros/src/parse.rs @@ -27,7 +27,7 @@ pub struct RsxProp { pub enum RsxNode { Text(LitStr), - /// Component with optional props and children: `Path { prop: expr, "child", ... }` or bare `Path`. + Expr(Expr), Component { path: Path, props: Vec, @@ -82,6 +82,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)); + } + + // --- existing code --- if input.peek(Token![!]) { input.parse::()?; let mount: Ident = input.parse()?; @@ -93,10 +105,8 @@ impl Parse for RsxNode { 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, @@ -107,14 +117,11 @@ impl Parse for RsxNode { 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, @@ -128,7 +135,6 @@ impl Parse for RsxNode { return Ok(RsxNode::Text(input.parse()?)); } - // Component: Path or Path { props, children } parse_component_invocation(input) } } diff --git a/src/frontend.rs b/src/frontend.rs index 11a10f2..23c2124 100644 --- a/src/frontend.rs +++ b/src/frontend.rs @@ -6,15 +6,21 @@ use crate::{ View, }; +pub trait ToRsx { + fn to_rsx(&self) -> Rsx; +} + +#[derive(Clone)] pub enum RsxScope { - Static(Box) + Send + Sync>), + Static(Arc) + Send + Sync>), Dynamic( Arc) + Send + Sync>, - Vec>, + Vec>, ), Child(Rsx), } +#[derive(Clone)] pub struct Rsx(Vec); impl Rsx { @@ -23,18 +29,22 @@ 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 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(); @@ -69,3 +79,9 @@ impl Rsx { }) } } + +impl ToRsx for &Rsx { + fn to_rsx(&self) -> Rsx { + Rsx(self.0.clone()) + } +} From bf6fa42d960c757f5eeb46d267586cd7729863df Mon Sep 17 00:00:00 2001 From: Leo dev Date: Fri, 30 Jan 2026 23:14:10 +0100 Subject: [PATCH 18/23] Fixed simple example --- examples/simple_example.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/simple_example.rs b/examples/simple_example.rs index dbd0ca1..20dc025 100644 --- a/examples/simple_example.rs +++ b/examples/simple_example.rs @@ -10,7 +10,7 @@ pub fn main() { #[component] fn App(cx: &Arc) -> View { rsx! { - MyComponent { "Hello world!" } + MyComponent { "------ example" } } .view(&cx) } @@ -18,8 +18,8 @@ fn App(cx: &Arc) -> View { #[component] fn MyComponent(cx: &Arc, children: &Rsx) -> View { rsx! { - "Simple" @{children} + "Simple" } .view(&cx) } From 4e2d4662d1614487cb4e927edc9aaa51f30895ea Mon Sep 17 00:00:00 2001 From: Leo dev Date: Fri, 30 Jan 2026 23:25:33 +0100 Subject: [PATCH 19/23] Display to rsx --- src/frontend.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/frontend.rs b/src/frontend.rs index 23c2124..1bb3fa5 100644 --- a/src/frontend.rs +++ b/src/frontend.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use crate::{ component::{Context, Scope}, + render::Point, state::HookDependency, View, }; @@ -85,3 +86,13 @@ impl ToRsx for &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))) + }))]) + } +} From eefa41318c8f90794c6d03d7c9adf392232524d9 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Fri, 30 Jan 2026 23:36:54 +0100 Subject: [PATCH 20/23] Refactored component --- src/{component.rs => component/context.rs} | 59 ++-------------------- src/component/mod.rs | 33 ++++++++++++ src/component/scope.rs | 39 ++++++++++++++ src/engine/benchmark.rs | 7 +-- src/engine/console.rs | 2 +- src/engine/mod.rs | 7 +-- src/frontend.rs | 8 +-- src/lib.rs | 2 +- src/state.rs | 2 +- 9 files changed, 84 insertions(+), 75 deletions(-) rename src/{component.rs => component/context.rs} (79%) create mode 100644 src/component/mod.rs create mode 100644 src/component/scope.rs diff --git a/src/component.rs b/src/component/context.rs similarity index 79% rename from src/component.rs rename to src/component/context.rs index 9928804..0a1a75d 100644 --- a/src/component.rs +++ b/src/component/context.rs @@ -5,23 +5,14 @@ use std::{ }; use crate::{ + component::EventHandler, engine::{Command, CommandExecutor}, render::DrawContext, state::{use_effect, HookDependency}, - View, ViewWrapper, + View, }; -pub type Component = Arc; -pub type EventHandler = Arc, &dyn Any) + Send + Sync>>; - -pub trait ComponentImpl: Send + Sync { - fn call(&self, cx: &Arc) -> View; -} - -pub struct Scope { - pub children: Mutex, Option)>>, - executor: Arc, -} +use super::{scope::Scope, Component, ComponentImpl}; pub struct Context { component: Mutex, @@ -189,47 +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( - 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)); - } -} - -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/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 a371bf5..1843050 100644 --- a/src/engine/benchmark.rs +++ b/src/engine/benchmark.rs @@ -2,11 +2,8 @@ use std::{io::stdout, sync::Arc, time::Instant}; use crossterm::{cursor::MoveTo, execute, terminal::Clear}; -use crate::{ - prelude::{ComponentImpl, Context}, - render::Area, - DrawContext, View, -}; +use crate::component::{context::Context, ComponentImpl}; +use crate::{render::Area, DrawContext, View}; use super::Engine; diff --git a/src/engine/console.rs b/src/engine/console.rs index 7f82d54..6aa29ce 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::{ComponentImpl, Context}, render::Area, DrawContext, View, }; diff --git a/src/engine/mod.rs b/src/engine/mod.rs index c0f5525..b55f187 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -7,11 +7,8 @@ pub use console::*; use std::{any::Any, sync::Arc}; -use crate::{ - prelude::{ComponentImpl, Context}, - render::Area, - DrawContext, View, -}; +use crate::component::{context::Context, ComponentImpl}; +use crate::{render::Area, DrawContext, View}; pub trait Engine { fn run(&self, component: C) -> crate::Result; diff --git a/src/frontend.rs b/src/frontend.rs index 1bb3fa5..2689f75 100644 --- a/src/frontend.rs +++ b/src/frontend.rs @@ -1,11 +1,7 @@ use std::sync::Arc; -use crate::{ - component::{Context, Scope}, - render::Point, - state::HookDependency, - View, -}; +use crate::component::{context::Context, scope::Scope}; +use crate::{render::Point, state::HookDependency, View}; pub trait ToRsx { fn to_rsx(&self) -> Rsx; diff --git a/src/lib.rs b/src/lib.rs index 4b3d469..93448c1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,7 @@ 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::*; diff --git a/src/state.rs b/src/state.rs index 339c4b2..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>); From ab9548920c4deabfad6d600ba3c1bde1e3589776 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sat, 31 Jan 2026 00:09:14 +0100 Subject: [PATCH 21/23] Improved rsx parsing --- macros/src/emit.rs | 11 ++----- macros/src/parse.rs | 71 ++++++++++++++++++++++----------------------- 2 files changed, 37 insertions(+), 45 deletions(-) diff --git a/macros/src/emit.rs b/macros/src/emit.rs index 6595c93..60884ca 100644 --- a/macros/src/emit.rs +++ b/macros/src/emit.rs @@ -33,15 +33,8 @@ fn emit_deps_vec(deps: &[Dep]) -> TokenStream { let deps = deps.iter().map(|d| { let ident = &d.ident; - if let Some(pat) = &d.pat { - quote! {{ - let #pat = #ident.clone(); - Box::new(#pat) as Box - }} - } else { - quote! { - Box::new(#ident.clone()) as Box - } + quote! { + std::sync::Arc::new(#ident) as std::sync::Arc } }); diff --git a/macros/src/parse.rs b/macros/src/parse.rs index 29a8d9a..7252ec3 100644 --- a/macros/src/parse.rs +++ b/macros/src/parse.rs @@ -1,4 +1,5 @@ use syn::braced; +use syn::parse::discouraged::Speculative; use syn::{ Expr, Ident, LitStr, Pat, Path, Result, Token, parse::{Parse, ParseStream}, @@ -82,7 +83,7 @@ fn parse_deps(input: ParseStream) -> Result> { impl Parse for RsxNode { fn parse(input: ParseStream) -> Result { - // --- @{ expr } --- + // @{ $expr } if input.peek(Token![@]) { input.parse::()?; @@ -93,7 +94,7 @@ impl Parse for RsxNode { return Ok(RsxNode::Expr(expr)); } - // --- existing code --- + // !$ident if input.peek(Token![!]) { input.parse::()?; let mount: Ident = input.parse()?; @@ -102,6 +103,7 @@ 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()?; @@ -115,6 +117,7 @@ impl Parse for RsxNode { }); } + // %$dep for $pat in $expr { $rsx } if input.peek(Token![for]) { input.parse::()?; let pat: Pat = Pat::parse_multi(input)?; @@ -131,6 +134,7 @@ impl Parse for RsxNode { }); } + // "%$dep for $pat in $expr { $rsx }" if input.peek(LitStr) { return Ok(RsxNode::Text(input.parse()?)); } @@ -163,49 +167,44 @@ fn parse_component_invocation(input: ParseStream) -> Result { }) } -/// Parses comma-separated "ident: expr" (props) and RsxNode (children) inside braces. 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(); - let mut children = Vec::new(); while !input.is_empty() { - if input.peek(LitStr) { - children.push(input.parse()?); - } else { - let path: Path = input.parse()?; + let fork = input.fork(); - 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(), - }); + 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::()?; } - } - - if !input.is_empty() { - input.parse::()?; + } else { + // ❌ do not commit → nothing consumed + break; } } - Ok((props, children)) + Ok(props) } fn parse_children(input: ParseStream) -> Result> { From 85342d641f59dce6b2876112421cd9053e4b1efd Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sat, 31 Jan 2026 00:15:40 +0100 Subject: [PATCH 22/23] Improved prelude --- examples/benchmark.rs | 4 ++-- examples/hello_world.rs | 2 -- examples/simple_benchmark.rs | 2 -- examples/simple_example.rs | 2 -- src/engine/mod.rs | 2 +- src/lib.rs | 7 ++++++- 6 files changed, 9 insertions(+), 10 deletions(-) diff --git a/examples/benchmark.rs b/examples/benchmark.rs index 4d45b4f..2a73e05 100644 --- a/examples/benchmark.rs +++ b/examples/benchmark.rs @@ -1,7 +1,7 @@ -use std::{sync::mpsc, sync::Arc, thread}; - use osui::prelude::*; +use std::{sync::mpsc, thread}; + pub fn main() { let engine = Arc::new(Benchmark::new(Console::new())); diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 0a9850d..ecef5bc 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -1,5 +1,3 @@ -use std::sync::Arc; - use osui::prelude::*; pub fn main() { diff --git a/examples/simple_benchmark.rs b/examples/simple_benchmark.rs index e902b10..1472f44 100644 --- a/examples/simple_benchmark.rs +++ b/examples/simple_benchmark.rs @@ -1,5 +1,3 @@ -use std::sync::Arc; - use osui::prelude::*; pub fn main() { diff --git a/examples/simple_example.rs b/examples/simple_example.rs index 20dc025..dd3c5fc 100644 --- a/examples/simple_example.rs +++ b/examples/simple_example.rs @@ -1,5 +1,3 @@ -use std::sync::Arc; - use osui::prelude::*; pub fn main() { diff --git a/src/engine/mod.rs b/src/engine/mod.rs index b55f187..2ee53e5 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -15,7 +15,7 @@ pub trait Engine { 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/lib.rs b/src/lib.rs index 93448c1..0bb0a1d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,8 +14,9 @@ pub mod prelude { pub use crate::frontend::*; pub use crate::render::*; pub use crate::state::*; - pub use crate::View; + pub use crate::{sleep, Error, Result, View, ViewWrapper}; pub use osui_macros::{component, rsx}; + pub use std::sync::{Arc, Mutex}; } pub type View = Arc; @@ -32,3 +33,7 @@ impl From>> for Error { Error::PoisonError } } + +pub fn sleep(delay_ms: u64) { + std::thread::sleep(std::time::Duration::from_millis(delay_ms)); +} From e5812315c65cd81a6b3cb0f4e50f24c6c7c11c75 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sat, 31 Jan 2026 00:23:10 +0100 Subject: [PATCH 23/23] Hide cursor in engine and crossterm in prelude --- src/engine/console.rs | 2 ++ src/lib.rs | 1 + 2 files changed, 3 insertions(+) diff --git a/src/engine/console.rs b/src/engine/console.rs index 6aa29ce..afa8cb2 100644 --- a/src/engine/console.rs +++ b/src/engine/console.rs @@ -80,6 +80,8 @@ impl Engine for Console { } 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(); diff --git a/src/lib.rs b/src/lib.rs index 0bb0a1d..9d65be7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,6 +15,7 @@ pub mod prelude { pub use crate::render::*; pub use crate::state::*; pub use crate::{sleep, Error, Result, View, ViewWrapper}; + pub use crossterm; pub use osui_macros::{component, rsx}; pub use std::sync::{Arc, Mutex}; }