From b23c8c68dcc7cc0af5077f9784aa95bfecb072a8 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sun, 1 Feb 2026 10:11:41 +0100 Subject: [PATCH 01/16] Text dependencies --- examples/hello_world.rs | 12 +++++++++++- macros/src/emit.rs | 22 +++++++++++++++------- macros/src/parse.rs | 22 +++++++++++++++++++--- 3 files changed, 45 insertions(+), 11 deletions(-) diff --git a/examples/hello_world.rs b/examples/hello_world.rs index ecef5bc..19fdab5 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -8,7 +8,17 @@ pub fn main() { #[component] fn App(cx: &Arc) -> View { rsx! { - "Hello World" + Card { content: "Hello World".to_string() } + } + .view(&cx) +} + +#[component] +fn Card(cx: &Arc, content: String) -> View { + let content = content.clone(); + + rsx! { + %ref content "----\n{content}\n----" } .view(&cx) } diff --git a/macros/src/emit.rs b/macros/src/emit.rs index 91f8b4f..09a7f97 100644 --- a/macros/src/emit.rs +++ b/macros/src/emit.rs @@ -38,7 +38,7 @@ fn emit_deps(deps: &[Dep]) -> TokenStream { /// Emits a Vec of dependencies as HookDependency trait objects fn emit_deps_vec(deps: &[Dep]) -> TokenStream { - let deps = deps.iter().map(|d| { + let deps = deps.iter().filter(|d| d.is_dep).map(|d| { let ident = &d.ident; quote! { @@ -54,9 +54,12 @@ fn emit_deps_vec(deps: &[Dep]) -> TokenStream { /// Emits code for a single node within a scope fn emit_node_scope(node: &RsxNode) -> TokenStream { match node { - RsxNode::Text(_) => { + RsxNode::Text { deps, .. } => { let emit = emit_node(node); + let deps_emit = emit_deps(deps); + quote! { + #deps_emit r.static_scope(move |scope| {#emit}); } } @@ -129,11 +132,16 @@ fn emit_node_scope(node: &RsxNode) -> TokenStream { 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::Text { text, deps } => { + let deps_emit = emit_deps(deps); + + quote! { + scope.view(Arc::new({ + #deps_emit + move |ctx| ctx.draw_text(Point { x: 0, y: 0 }, &format!(#text)) + })); + } + } RsxNode::Component { path, diff --git a/macros/src/parse.rs b/macros/src/parse.rs index 0d474e2..2908dc2 100644 --- a/macros/src/parse.rs +++ b/macros/src/parse.rs @@ -37,7 +37,7 @@ pub struct RsxProp { /// AST node representing different RSX constructs pub enum RsxNode { /// String literal: `"text"` - Text(LitStr), + Text { text: LitStr, deps: Vec }, /// Expression node: `{expr}` Expr(Expr), /// Component instantiation: `Component { prop: value, ... }` @@ -76,6 +76,7 @@ pub enum RsxNode { pub struct Dep { pub ident: Ident, pub pat: Option, + pub is_dep: bool, } fn parse_deps(input: ParseStream) -> Result> { @@ -84,7 +85,15 @@ fn parse_deps(input: ParseStream) -> Result> { if input.peek(Token![%]) { input.parse::()?; loop { + let is_ref = if input.peek(Token![ref]) { + input.parse::()?; + true + } else { + false + }; + let ident: Ident = input.parse()?; + let pat = if input.peek(Token![as]) { input.parse::()?; // Patterns in syn 2.0 are parsed via `Pat::parse_multi` @@ -94,7 +103,11 @@ fn parse_deps(input: ParseStream) -> Result> { None }; - deps.push(Dep { ident, pat }); + deps.push(Dep { + ident, + pat, + is_dep: !is_ref, + }); if !input.peek(Token![,]) { break; @@ -161,7 +174,10 @@ impl Parse for RsxNode { // "%$dep for $pat in $expr { $rsx }" if input.peek(LitStr) { - return Ok(RsxNode::Text(input.parse()?)); + return Ok(RsxNode::Text { + text: input.parse()?, + deps, + }); } parse_component_invocation(input) From b141e134d4095b31366e13bc6bd014791f5b4ef5 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sun, 1 Feb 2026 10:21:28 +0100 Subject: [PATCH 02/16] Direct text dependencies --- examples/hello_world.rs | 2 +- macros/src/parse.rs | 55 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 19fdab5..78cfc92 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -18,7 +18,7 @@ fn Card(cx: &Arc, content: String) -> View { let content = content.clone(); rsx! { - %ref content "----\n{content}\n----" + "----\n{content}\n----" } .view(&cx) } diff --git a/macros/src/parse.rs b/macros/src/parse.rs index 2908dc2..0bb9f23 100644 --- a/macros/src/parse.rs +++ b/macros/src/parse.rs @@ -2,6 +2,7 @@ //! //! Parses RSX syntax into an AST that can be emitted as Rust code. +use proc_macro2::Span; use syn::braced; use syn::parse::discouraged::Speculative; use syn::{ @@ -174,10 +175,22 @@ impl Parse for RsxNode { // "%$dep for $pat in $expr { $rsx }" if input.peek(LitStr) { - return Ok(RsxNode::Text { - text: input.parse()?, - deps, - }); + let text = input.parse()?; + + let mut deps = deps; + + deps.append( + &mut extract_vars_from_lit(&text) + .into_iter() + .map(|ident| Dep { + ident, + pat: None, + is_dep: false, + }) + .collect(), + ); + + return Ok(RsxNode::Text { text, deps }); } parse_component_invocation(input) @@ -255,3 +268,37 @@ fn parse_children(input: ParseStream) -> Result> { } Ok(nodes) } + +fn extract_vars_from_lit(lit: &LitStr) -> Vec { + let s = lit.value(); + let mut vars = Vec::new(); + + let mut chars = s.chars().peekable(); + while let Some(c) = chars.next() { + if c == '{' { + // skip escaped {{ + if chars.peek() == Some(&'{') { + chars.next(); + continue; + } + + let mut name = String::new(); + while let Some(&ch) = chars.peek() { + chars.next(); + if ch == '}' { + break; + } + name.push(ch); + } + + if !name.is_empty() { + vars.push(Ident::new( + &name.split_once(':').unwrap_or((&name, &name)).0, + Span::call_site(), + )); + } + } + } + + vars +} From 5e4b78f353c245afde7bb3a3c67afe60a9630ef1 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sun, 1 Feb 2026 10:23:34 +0100 Subject: [PATCH 03/16] Removed excess clone in Card component --- examples/hello_world.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 78cfc92..512e8e7 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -15,8 +15,6 @@ fn App(cx: &Arc) -> View { #[component] fn Card(cx: &Arc, content: String) -> View { - let content = content.clone(); - rsx! { "----\n{content}\n----" } From c3519fa6ed6f3e238b8828325c712b4a24677ef9 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sun, 1 Feb 2026 10:31:37 +0100 Subject: [PATCH 04/16] Improved Card by dynamicifying the bars --- examples/hello_world.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 512e8e7..f3616dd 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -15,8 +15,10 @@ fn App(cx: &Arc) -> View { #[component] fn Card(cx: &Arc, content: String) -> View { + let bar = "-".repeat(content.len()); + rsx! { - "----\n{content}\n----" + "{bar}\n{content}\n{bar}" } .view(&cx) } From 039e751dd1bb088d95aa968c3b82aeac8d7d7ed6 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sun, 1 Feb 2026 11:01:20 +0100 Subject: [PATCH 05/16] Removed dupes of deps in {} --- macros/src/parse.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/macros/src/parse.rs b/macros/src/parse.rs index 0bb9f23..2cfc37c 100644 --- a/macros/src/parse.rs +++ b/macros/src/parse.rs @@ -2,6 +2,8 @@ //! //! Parses RSX syntax into an AST that can be emitted as Rust code. +use std::collections::HashSet; + use proc_macro2::Span; use syn::braced; use syn::parse::discouraged::Speculative; @@ -269,9 +271,9 @@ fn parse_children(input: ParseStream) -> Result> { Ok(nodes) } -fn extract_vars_from_lit(lit: &LitStr) -> Vec { +fn extract_vars_from_lit(lit: &LitStr) -> HashSet { let s = lit.value(); - let mut vars = Vec::new(); + let mut vars = HashSet::new(); let mut chars = s.chars().peekable(); while let Some(c) = chars.next() { @@ -292,7 +294,7 @@ fn extract_vars_from_lit(lit: &LitStr) -> Vec { } if !name.is_empty() { - vars.push(Ident::new( + vars.insert(Ident::new( &name.split_once(':').unwrap_or((&name, &name)).0, Span::call_site(), )); From 8c4576396b2f34e4d0d0def237c18c46f7b98d9d Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sun, 1 Feb 2026 13:39:37 +0100 Subject: [PATCH 06/16] Improved state structure --- src/state/effect.rs | 55 +++++++++++++++ src/state/mod.rs | 44 ++++++++++++ src/state/mount.rs | 58 ++++++++++++++++ src/{ => state}/state.rs | 142 +++------------------------------------ 4 files changed, 168 insertions(+), 131 deletions(-) create mode 100644 src/state/effect.rs create mode 100644 src/state/mod.rs create mode 100644 src/state/mount.rs rename src/{ => state}/state.rs (52%) diff --git a/src/state/effect.rs b/src/state/effect.rs new file mode 100644 index 0000000..fc6d2e5 --- /dev/null +++ b/src/state/effect.rs @@ -0,0 +1,55 @@ +//! # Effect hooks module +//! +//! Provides React-like use_effect hooks for managing effects. +//! This module includes use_effect and use_sync_effect. + +use std::sync::{Arc, Mutex}; + +use super::{state::State, HookDependency, HookEffect}; +use crate::component::context::Context; + +/// Executes a function when dependencies change +/// +/// Similar to React's useEffect. The provided function is executed +/// when any of the dependencies change. +pub fn use_effect(f: F, dependencies: &[&dyn HookDependency]) { + let f = Arc::new(Mutex::new(f)); + let hook = HookEffect(Arc::new(Mutex::new({ + let f = f.clone(); + move || { + let f = f.clone(); + std::thread::spawn(move || (f.lock().unwrap())()); + } + }))); + + for d in dependencies { + d.on_update(hook.clone()); + } +} + +/// Synchronizes state changes back to the context as events +/// +/// Creates an effect that emits an event whenever the state changes. +/// The encoder function converts state values to events. +pub fn use_sync_effect< + T: Send + Sync + 'static, + Ev: Send + Sync + 'static, + E: Fn(&State) -> Ev + Send + Sync + 'static, +>( + cx: &Arc, + state: &State, + encoder: E, + deps: &[&dyn HookDependency], +) { + use_effect( + { + let state = state.clone(); + let cx = cx.clone(); + move || { + let ev = encoder(&state); + cx.emit_event(ev); + } + }, + deps, + ); +} diff --git a/src/state/mod.rs b/src/state/mod.rs new file mode 100644 index 0000000..0dc2c0e --- /dev/null +++ b/src/state/mod.rs @@ -0,0 +1,44 @@ +//! # State Management Module +//! +//! Provides React-like hooks for managing component state and side effects. +//! This module includes useState, useEffect, useMount, and state synchronization hooks. + +use std::{ + fmt::{Debug, Formatter, Result as FmtResult}, + sync::{Arc, Mutex}, +}; + +mod effect; +mod mount; +mod state; +pub use effect::*; +pub use mount::*; +pub use state::*; + +/// Effect callback that can be triggered by state changes +#[derive(Clone)] +pub struct HookEffect(Arc>); + +/// Trait for values that can be tracked as dependencies in hooks +pub trait HookDependency: Send + Sync { + /// Register an effect to be triggered on updates + fn on_update(&self, hook: HookEffect); +} + +impl HookEffect { + /// Creates a new effect from a function + pub fn new(f: F) -> Self { + Self(Arc::new(Mutex::new(f))) + } + + /// Executes the effect function + pub fn call(&self) { + (self.0.lock().unwrap())() + } +} + +impl Debug for HookEffect { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!(f, "HookEffect") + } +} diff --git a/src/state/mount.rs b/src/state/mount.rs new file mode 100644 index 0000000..468a4ec --- /dev/null +++ b/src/state/mount.rs @@ -0,0 +1,58 @@ +//! # Mount hooks module +//! +//! Provides React-like Mount hooks for managing effects. +//! This module includes Mount, use_mount and use_mount_manual. + +use std::{ + fmt::Debug, + sync::{Arc, Mutex}, +}; + +use super::{HookDependency, HookEffect}; + +/// Mount lifecycle hook +/// +/// Tracks whether a component has been mounted and executes +/// any pending mount effects. +#[derive(Debug, Clone)] +pub struct Mount(Arc>, Arc>>); + +/// Creates a mount lifecycle hook +/// +/// Returns a Mount that tracks component lifecycle and executes +/// effects after mounting. +pub fn use_mount() -> Mount { + Mount(Arc::new(Mutex::new(true)), Arc::new(Mutex::new(Vec::new()))) +} + +/// Creates a manual mount lifecycle hook +/// +/// Similar to use_mount but the component starts as unmounted. +/// Must call .mount() to trigger mounted effects. +pub fn use_mount_manual() -> Mount { + Mount( + Arc::new(Mutex::new(false)), + Arc::new(Mutex::new(Vec::new())), + ) +} + +impl HookDependency for Mount { + fn on_update(&self, hook: HookEffect) { + if *self.0.lock().unwrap() { + hook.call(); + } else { + self.1.lock().unwrap().push(hook); + } + } +} + +impl Mount { + /// Mark the component as mounted and execute pending effects + pub fn mount(&self) { + *self.0.lock().unwrap() = true; + for hook_effect in self.1.lock().unwrap().iter() { + hook_effect.call(); + } + self.1.lock().unwrap().clear(); + } +} diff --git a/src/state.rs b/src/state/state.rs similarity index 52% rename from src/state.rs rename to src/state/state.rs index 8e4c7a7..d909cb8 100644 --- a/src/state.rs +++ b/src/state/state.rs @@ -1,7 +1,7 @@ -//! # State Management Module +//! # State hooks module //! -//! Provides React-like hooks for managing component state and side effects. -//! This module includes useState, useEffect, useMount, and state synchronization hooks. +//! Provides React-like use_state hooks for managing states. +//! This module includes use_state and use_sync_state. use std::{ any::Any, @@ -10,12 +10,9 @@ use std::{ sync::{Arc, Mutex, MutexGuard}, }; +use super::{HookDependency, HookEffect}; use crate::component::context::Context; -/// Effect callback that can be triggered by state changes -#[derive(Clone)] -pub struct HookEffect(Arc>); - /// State holder for reactive values /// /// Similar to React's useState hook. Holds a value and tracks dependents @@ -38,23 +35,6 @@ pub struct Inner<'a, T> { updated: bool, } -/// Mount lifecycle hook -/// -/// Tracks whether a component has been mounted and executes -/// any pending mount effects. -#[derive(Debug, Clone)] -pub struct Mount(Arc>, Arc>>); - -/// Creates a new state value -/// -/// Returns a State that can be read and written from multiple threads. -pub fn use_state(v: T) -> State { - State { - value: Arc::new(Mutex::new(v)), - dependents: Arc::new(Mutex::new(Vec::new())), - } -} - impl State { /// Gets a cloned copy of the state value /// @@ -106,24 +86,6 @@ impl Display for State { } } -impl Debug for HookEffect { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - write!(f, "HookEffect") - } -} - -impl HookEffect { - /// Creates a new effect from a function - pub fn new(f: F) -> Self { - Self(Arc::new(Mutex::new(f))) - } - - /// Executes the effect function - pub fn call(&self) { - (self.0.lock().unwrap())() - } -} - /// Inner implements Deref for read access and DerefMut for write access /// On drop after mutation, automatically triggers dependent effects impl Drop for Inner<'_, T> { @@ -150,77 +112,12 @@ impl DerefMut for Inner<'_, T> { } } -/// Trait for values that can be tracked as dependencies in hooks -pub trait HookDependency: Send + Sync { - /// Register an effect to be triggered on updates - fn on_update(&self, hook: HookEffect); -} - impl HookDependency for State { fn on_update(&self, hook: HookEffect) { self.dependents.lock().unwrap().push(hook); } } -impl HookDependency for Mount { - fn on_update(&self, hook: HookEffect) { - if *self.0.lock().unwrap() { - hook.call(); - } else { - self.1.lock().unwrap().push(hook); - } - } -} - -impl Mount { - /// Mark the component as mounted and execute pending effects - pub fn mount(&self) { - *self.0.lock().unwrap() = true; - for hook_effect in self.1.lock().unwrap().iter() { - hook_effect.call(); - } - self.1.lock().unwrap().clear(); - } -} - -/// Executes a function when dependencies change -/// -/// Similar to React's useEffect. The provided function is executed -/// when any of the dependencies change. -pub fn use_effect(f: F, dependencies: &[&dyn HookDependency]) { - let f = Arc::new(Mutex::new(f)); - let hook = HookEffect(Arc::new(Mutex::new({ - let f = f.clone(); - move || { - let f = f.clone(); - std::thread::spawn(move || (f.lock().unwrap())()); - } - }))); - - for d in dependencies { - d.on_update(hook.clone()); - } -} - -/// Creates a mount lifecycle hook -/// -/// Returns a Mount that tracks component lifecycle and executes -/// effects after mounting. -pub fn use_mount() -> Mount { - Mount(Arc::new(Mutex::new(true)), Arc::new(Mutex::new(Vec::new()))) -} - -/// Creates a manual mount lifecycle hook -/// -/// Similar to use_mount but the component starts as unmounted. -/// Must call .mount() to trigger mounted effects. -pub fn use_mount_manual() -> Mount { - Mount( - Arc::new(Mutex::new(false)), - Arc::new(Mutex::new(Vec::new())), - ) -} - /// Synchronizes state with events from the context /// /// Creates state that is automatically updated when events are emitted @@ -244,29 +141,12 @@ pub fn use_sync_state< state } -/// Synchronizes state changes back to the context as events +/// Creates a new state value /// -/// Creates an effect that emits an event whenever the state changes. -/// The encoder function converts state values to events. -pub fn use_sync_effect< - T: Send + Sync + 'static, - Ev: Send + Sync + 'static, - E: Fn(&State) -> Ev + Send + Sync + 'static, ->( - cx: &Arc, - state: &State, - encoder: E, - deps: &[&dyn HookDependency], -) { - use_effect( - { - let state = state.clone(); - let cx = cx.clone(); - move || { - let ev = encoder(&state); - cx.emit_event(ev); - } - }, - deps, - ); +/// Returns a State that can be read and written from multiple threads. +pub fn use_state(v: T) -> State { + State { + value: Arc::new(Mutex::new(v)), + dependents: Arc::new(Mutex::new(Vec::new())), + } } From d40a032bc15854fbaf55d85419ca1c1fe7167217 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sun, 1 Feb 2026 13:40:22 +0100 Subject: [PATCH 07/16] Improved state structure --- src/component/context.rs | 2 +- src/frontend.rs | 2 +- src/{state => hooks}/effect.rs | 0 src/{state => hooks}/mod.rs | 2 +- src/{state => hooks}/mount.rs | 0 src/{state => hooks}/state.rs | 0 src/lib.rs | 4 ++-- 7 files changed, 5 insertions(+), 5 deletions(-) rename src/{state => hooks}/effect.rs (100%) rename src/{state => hooks}/mod.rs (96%) rename src/{state => hooks}/mount.rs (100%) rename src/{state => hooks}/state.rs (100%) diff --git a/src/component/context.rs b/src/component/context.rs index 7402f3d..92fec14 100644 --- a/src/component/context.rs +++ b/src/component/context.rs @@ -16,7 +16,7 @@ use crate::{ component::EventHandler, engine::{Command, CommandExecutor}, render::DrawContext, - state::{use_effect, HookDependency}, + hooks::{use_effect, HookDependency}, View, }; diff --git a/src/frontend.rs b/src/frontend.rs index c094956..e050b59 100644 --- a/src/frontend.rs +++ b/src/frontend.rs @@ -7,7 +7,7 @@ use std::sync::Arc; use crate::component::{context::Context, scope::Scope}; -use crate::{render::Point, state::HookDependency, View}; +use crate::{render::Point, hooks::HookDependency, View}; /// Trait for converting values to RSX pub trait ToRsx { diff --git a/src/state/effect.rs b/src/hooks/effect.rs similarity index 100% rename from src/state/effect.rs rename to src/hooks/effect.rs diff --git a/src/state/mod.rs b/src/hooks/mod.rs similarity index 96% rename from src/state/mod.rs rename to src/hooks/mod.rs index 0dc2c0e..36aea03 100644 --- a/src/state/mod.rs +++ b/src/hooks/mod.rs @@ -1,4 +1,4 @@ -//! # State Management Module +//! # State Management and Hooks Module //! //! Provides React-like hooks for managing component state and side effects. //! This module includes useState, useEffect, useMount, and state synchronization hooks. diff --git a/src/state/mount.rs b/src/hooks/mount.rs similarity index 100% rename from src/state/mount.rs rename to src/hooks/mount.rs diff --git a/src/state/state.rs b/src/hooks/state.rs similarity index 100% rename from src/state/state.rs rename to src/hooks/state.rs diff --git a/src/lib.rs b/src/lib.rs index 2f434e8..f0a73e3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,7 +44,7 @@ pub mod component; pub mod engine; pub mod frontend; pub mod render; -pub mod state; +pub mod hooks; pub mod prelude { //! Prelude module - Re-exports commonly used items for convenience @@ -52,7 +52,7 @@ pub mod prelude { pub use crate::engine::*; pub use crate::frontend::*; pub use crate::render::*; - pub use crate::state::*; + pub use crate::hooks::*; pub use crate::{sleep, Error, Result, View, ViewWrapper}; pub use crossterm; pub use osui_macros::{component, rsx}; From b0eaa763984607b2c1387a1e887f3672b1600e0d Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sun, 1 Feb 2026 16:34:27 +0100 Subject: [PATCH 08/16] View plugins --- examples/hello_world.rs | 7 +++++++ macros/src/emit.rs | 27 ++++++++++++++++++++++++-- macros/src/parse.rs | 42 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 73 insertions(+), 3 deletions(-) diff --git a/examples/hello_world.rs b/examples/hello_world.rs index f3616dd..f81be34 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -8,6 +8,7 @@ pub fn main() { #[component] fn App(cx: &Arc) -> View { rsx! { + impl my_center(11, 1) Card { content: "Hello World".to_string() } } .view(&cx) @@ -22,3 +23,9 @@ fn Card(cx: &Arc, content: String) -> View { } .view(&cx) } + +fn my_center(ctx: &mut DrawContext, view: View, w: u16, h: u16) { + let (x, y) = ((ctx.area.width - w) / 2, (ctx.area.height - h) / 2); + let area = ctx.allocate(x, y, w, h); + ctx.draw_view(area, view); +} diff --git a/macros/src/emit.rs b/macros/src/emit.rs index 09a7f97..e861b20 100644 --- a/macros/src/emit.rs +++ b/macros/src/emit.rs @@ -51,6 +51,20 @@ fn emit_deps_vec(deps: &[Dep]) -> TokenStream { } } +/// Emits a Vec of plugins +fn emit_plugins(deps: &[ViewPlugin]) -> TokenStream { + deps.iter() + .map(|d| { + let path = &d.path; + if let Some(args) = &d.args { + quote!( #path(ctx, view #(,#args)*); ) + } else { + quote!( #path(ctx, view); ) + } + }) + .collect() +} + /// Emits code for a single node within a scope fn emit_node_scope(node: &RsxNode) -> TokenStream { match node { @@ -144,6 +158,7 @@ fn emit_node(node: &RsxNode) -> TokenStream { } RsxNode::Component { + plugins, path, props, children, @@ -171,8 +186,16 @@ fn emit_node(node: &RsxNode) -> TokenStream { } }; - quote! { - scope.child(#component_expr, None); + if plugins.len() == 0 { + quote! { + scope.child(#component_expr, None); + } + } else { + let plugins_emit = emit_plugins(plugins); + + quote! { + scope.child(#component_expr, Some(std::sync::Arc::new(|ctx, view| {#plugins_emit}))); + } } } diff --git a/macros/src/parse.rs b/macros/src/parse.rs index 2cfc37c..2da9553 100644 --- a/macros/src/parse.rs +++ b/macros/src/parse.rs @@ -5,13 +5,14 @@ use std::collections::HashSet; use proc_macro2::Span; -use syn::braced; use syn::parse::discouraged::Speculative; +use syn::punctuated::Punctuated; use syn::{ Expr, Ident, LitStr, Pat, Path, Result, Token, parse::{Parse, ParseStream}, token::Brace, }; +use syn::{braced, parenthesized}; /// Root of an RSX expression pub struct RsxRoot { @@ -45,6 +46,8 @@ pub enum RsxNode { Expr(Expr), /// Component instantiation: `Component { prop: value, ... }` Component { + /// View plugins + plugins: Vec, /// Component path (e.g., `my_module::MyComponent`) path: Path, /// Component properties @@ -82,6 +85,11 @@ pub struct Dep { pub is_dep: bool, } +pub struct ViewPlugin { + pub path: Path, + pub args: Option>, +} + fn parse_deps(input: ParseStream) -> Result> { let mut deps = Vec::new(); @@ -122,6 +130,35 @@ fn parse_deps(input: ParseStream) -> Result> { Ok(deps) } +fn parse_plugins(input: ParseStream) -> Result> { + let mut plugins = Vec::new(); + + if input.peek(Token![impl]) { + input.parse::()?; + loop { + let path: Path = input.parse()?; + + let args = if input.peek(syn::token::Paren) { + let content; + parenthesized!(content in input); + let exprs = Punctuated::::parse_terminated(&content)?; + Some(exprs.into_iter().collect()) + } else { + None + }; + + plugins.push(ViewPlugin { path, args }); + + if !input.peek(Token![,]) { + break; + } + input.parse::()?; + } + } + + Ok(plugins) +} + impl Parse for RsxNode { fn parse(input: ParseStream) -> Result { // @{ $expr } @@ -201,10 +238,12 @@ impl Parse for RsxNode { /// Parses Path or Path { ... } into RsxNode::Component. fn parse_component_invocation(input: ParseStream) -> Result { + let plugins = parse_plugins(input)?; let path: Path = input.parse()?; if !input.peek(Brace) { return Ok(RsxNode::Component { + plugins, path, props: Vec::new(), children: Vec::new(), @@ -217,6 +256,7 @@ fn parse_component_invocation(input: ParseStream) -> Result { let (props, children) = parse_props_and_children(&content)?; Ok(RsxNode::Component { + plugins, path, props, children, From 2ca419bfd413f74b66dbbf6ae47baf1d70604095 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sun, 1 Feb 2026 16:37:07 +0100 Subject: [PATCH 09/16] Fixed newline drawing in engine --- src/engine/console.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/engine/console.rs b/src/engine/console.rs index 236ee41..b3ad971 100644 --- a/src/engine/console.rs +++ b/src/engine/console.rs @@ -26,7 +26,7 @@ pub struct ConsoleExecutor { } /// Console-based rendering engine -/// +/// /// Renders components to the terminal using crossterm for cross-platform support. pub struct Console { /// Thread functions to execute @@ -64,8 +64,10 @@ impl Engine for Console { match inst { crate::render::DrawInstruction::Text(point, text) => { let (x, y) = (ctx.area.x + point.x, ctx.area.y + point.y); - execute!(stdout(), MoveTo(x, y),).unwrap(); - print!("{text}"); + for (i, line) in text.lines().enumerate() { + execute!(stdout(), MoveTo(x, y + (i as u16))).unwrap(); + print!("{line}"); + } stdout().flush().unwrap(); } crate::render::DrawInstruction::Child(_point, child) => self.draw_context(child), From c4addd7f01818d0ac225064cace9ed320b1e1b50 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sun, 1 Feb 2026 17:18:59 +0100 Subject: [PATCH 10/16] View plugins --- examples/hello_world.rs | 8 +------- macros/src/emit.rs | 10 +++++++--- src/lib.rs | 6 ++++-- src/render.rs | 23 ++++++++++++++--------- src/view_plugins.rs | 41 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 67 insertions(+), 21 deletions(-) create mode 100644 src/view_plugins.rs diff --git a/examples/hello_world.rs b/examples/hello_world.rs index f81be34..a3e1aea 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -8,7 +8,7 @@ pub fn main() { #[component] fn App(cx: &Arc) -> View { rsx! { - impl my_center(11, 1) + impl size_auto, center, redraw Card { content: "Hello World".to_string() } } .view(&cx) @@ -23,9 +23,3 @@ fn Card(cx: &Arc, content: String) -> View { } .view(&cx) } - -fn my_center(ctx: &mut DrawContext, view: View, w: u16, h: u16) { - let (x, y) = ((ctx.area.width - w) / 2, (ctx.area.height - h) / 2); - let area = ctx.allocate(x, y, w, h); - ctx.draw_view(area, view); -} diff --git a/macros/src/emit.rs b/macros/src/emit.rs index e861b20..e61c26e 100644 --- a/macros/src/emit.rs +++ b/macros/src/emit.rs @@ -57,9 +57,9 @@ fn emit_plugins(deps: &[ViewPlugin]) -> TokenStream { .map(|d| { let path = &d.path; if let Some(args) = &d.args { - quote!( #path(ctx, view #(,#args)*); ) + quote!( #path(ctx, &view #(,#args)*); ) } else { - quote!( #path(ctx, view); ) + quote!( #path(ctx, &view); ) } }) .collect() @@ -194,7 +194,11 @@ fn emit_node(node: &RsxNode) -> TokenStream { let plugins_emit = emit_plugins(plugins); quote! { - scope.child(#component_expr, Some(std::sync::Arc::new(|ctx, view| {#plugins_emit}))); + scope.child(#component_expr, Some(std::sync::Arc::new(|ctx, view| { + let area = ctx.allocate(ctx.area.x, ctx.area.y, ctx.area.width, ctx.area.height); + ctx.draw_view(area, view.clone()); + #plugins_emit + }))); } } } diff --git a/src/lib.rs b/src/lib.rs index f0a73e3..3b76b8b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,16 +43,18 @@ use crate::render::DrawContext; pub mod component; pub mod engine; pub mod frontend; -pub mod render; pub mod hooks; +pub mod render; +pub mod view_plugins; pub mod prelude { //! Prelude module - Re-exports commonly used items for convenience pub use crate::component::{context::*, scope::*, *}; pub use crate::engine::*; pub use crate::frontend::*; - pub use crate::render::*; pub use crate::hooks::*; + pub use crate::render::*; + pub use crate::view_plugins::*; pub use crate::{sleep, Error, Result, View, ViewWrapper}; pub use crossterm; pub use osui_macros::{component, rsx}; diff --git a/src/render.rs b/src/render.rs index 0c75837..91d3a65 100644 --- a/src/render.rs +++ b/src/render.rs @@ -18,7 +18,7 @@ pub enum DrawInstruction { } /// Represents the dimensions of a drawable area -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct Size { /// Width in terminal columns pub width: u16, @@ -27,7 +27,7 @@ pub struct Size { } /// Represents a position in 2D space -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct Point { /// X coordinate (column) pub x: u16, @@ -36,7 +36,7 @@ pub struct Point { } /// Represents a rectangular area with position and dimensions -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct Area { /// X coordinate (column) of the top-left corner pub x: u16, @@ -68,8 +68,8 @@ impl DrawContext { Self { area, allocated: Area { - x: u16::MAX, - y: u16::MAX, + x: 0, + y: 0, width: 0, height: 0, }, @@ -80,10 +80,10 @@ impl DrawContext { /// Allocates space within the drawable area and returns the allocated area /// Updates the allocated bounds to include this allocation pub fn allocate(&mut self, x: u16, y: u16, width: u16, height: u16) -> Area { - self.allocated.x = self.allocated.x.min(x); - self.allocated.y = self.allocated.y.min(y); - self.allocated.width = self.allocated.width.max(width); - self.allocated.height = self.allocated.height.max(height); + self.allocated.x = x; + self.allocated.y = y; + self.allocated.width = width; + self.allocated.height = height; Area { x, @@ -108,4 +108,9 @@ impl DrawContext { pub fn draw_view(&mut self, area: Area, view: View) { self.drawing.push(DrawInstruction::View(area, view)); } + + /// Clears drawing + pub fn clear(&mut self) { + self.drawing.clear(); + } } diff --git a/src/view_plugins.rs b/src/view_plugins.rs new file mode 100644 index 0000000..843e3a6 --- /dev/null +++ b/src/view_plugins.rs @@ -0,0 +1,41 @@ +use crate::{ + render::{DrawContext, DrawInstruction}, + View, +}; + +pub fn x_center(ctx: &mut DrawContext, _view: &View) { + ctx.allocated.y = (ctx.area.height - ctx.allocated.height) / 2; +} + +pub fn y_center(ctx: &mut DrawContext, _view: &View) { + ctx.allocated.y = (ctx.area.height - ctx.allocated.height) / 2; +} + +pub fn center(ctx: &mut DrawContext, _view: &View) { + ctx.allocated.x = (ctx.area.width - ctx.allocated.width) / 2; + ctx.allocated.y = (ctx.area.height - ctx.allocated.height) / 2; +} + +pub fn size_auto(ctx: &mut DrawContext, _view: &View) { + for i in &ctx.drawing { + match i { + DrawInstruction::Text(_, text) => { + ctx.allocated.width = text.len() as u16; + ctx.allocated.height = 1; + } + DrawInstruction::View(area, view) => { + let mut c = DrawContext::new(area.clone()); + view(&mut c); + size_auto(&mut c, view); + ctx.allocated.width = c.allocated.width; + ctx.allocated.height = c.allocated.height; + } + DrawInstruction::Child(_, _) => {} + } + } +} + +pub fn redraw(ctx: &mut DrawContext, view: &View) { + ctx.clear(); + ctx.draw_view(ctx.allocated.clone(), view.clone()); +} From e9541c0d21911c559088f28515f3d9bc7cd1e76e Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sun, 1 Feb 2026 17:25:49 +0100 Subject: [PATCH 11/16] proper auto sizing --- src/view_plugins.rs | 52 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/src/view_plugins.rs b/src/view_plugins.rs index 843e3a6..ee2f6c3 100644 --- a/src/view_plugins.rs +++ b/src/view_plugins.rs @@ -20,8 +20,14 @@ pub fn size_auto(ctx: &mut DrawContext, _view: &View) { for i in &ctx.drawing { match i { DrawInstruction::Text(_, text) => { - ctx.allocated.width = text.len() as u16; - ctx.allocated.height = 1; + let mut height = 0; + + for line in text.lines() { + ctx.allocated.width = ctx.allocated.width.max(line.len() as u16); + height += 1; + } + + ctx.allocated.height = height; } DrawInstruction::View(area, view) => { let mut c = DrawContext::new(area.clone()); @@ -35,6 +41,48 @@ pub fn size_auto(ctx: &mut DrawContext, _view: &View) { } } +pub fn width_auto(ctx: &mut DrawContext, _view: &View) { + for i in &ctx.drawing { + match i { + DrawInstruction::Text(_, text) => { + for line in text.lines() { + ctx.allocated.width = ctx.allocated.width.max(line.len() as u16); + } + } + DrawInstruction::View(area, view) => { + let mut c = DrawContext::new(area.clone()); + view(&mut c); + size_auto(&mut c, view); + ctx.allocated.width = c.allocated.width; + } + DrawInstruction::Child(_, _) => {} + } + } +} + +pub fn height_auto(ctx: &mut DrawContext, _view: &View) { + for i in &ctx.drawing { + match i { + DrawInstruction::Text(_, text) => { + let mut height = 0; + + for _ in text.lines() { + height += 1; + } + + ctx.allocated.height = height; + } + DrawInstruction::View(area, view) => { + let mut c = DrawContext::new(area.clone()); + view(&mut c); + size_auto(&mut c, view); + ctx.allocated.height = c.allocated.height; + } + DrawInstruction::Child(_, _) => {} + } + } +} + pub fn redraw(ctx: &mut DrawContext, view: &View) { ctx.clear(); ctx.draw_view(ctx.allocated.clone(), view.clone()); From 44a48376470e620128b9ab0a6b58dcb1af28d205 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sun, 1 Feb 2026 17:36:48 +0100 Subject: [PATCH 12/16] view plugins Documentation --- examples/hello_world.rs | 1 + src/view_plugins.rs | 65 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/examples/hello_world.rs b/examples/hello_world.rs index a3e1aea..3cca2e7 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -8,6 +8,7 @@ pub fn main() { #[component] fn App(cx: &Arc) -> View { rsx! { + // redraw is important because the effects won't be applied impl size_auto, center, redraw Card { content: "Hello World".to_string() } } diff --git a/src/view_plugins.rs b/src/view_plugins.rs index ee2f6c3..368705a 100644 --- a/src/view_plugins.rs +++ b/src/view_plugins.rs @@ -1,21 +1,68 @@ +//! # `View` Plugins Module +//! +//! Provides the essential plugins for modifying a `View` + use crate::{ render::{DrawContext, DrawInstruction}, View, }; +/// Horizontally centers the allocated area within the available draw area. +/// +/// # Behavior +/// - Modifies `ctx.allocated.x` +/// - Does **not** modify height or width +/// - Assumes `ctx.allocated.width <= ctx.area.width` +/// +/// # Notes +/// This function only affects positioning, not sizing. pub fn x_center(ctx: &mut DrawContext, _view: &View) { ctx.allocated.y = (ctx.area.height - ctx.allocated.height) / 2; } +/// Vertically centers the allocated area within the available draw area. +/// +/// # Behavior +/// - Modifies `ctx.allocated.y` +/// - Does **not** modify width or height +/// - Assumes `ctx.allocated.height <= ctx.area.height` +/// +/// # Typical usage +/// Called after size has been resolved (e.g. after `size_auto`, +/// `height_auto`, or a fixed height has been set). pub fn y_center(ctx: &mut DrawContext, _view: &View) { ctx.allocated.y = (ctx.area.height - ctx.allocated.height) / 2; } +/// Centers the allocated area both horizontally and vertically. +/// +/// # Behavior +/// - Modifies `ctx.allocated.x` and `ctx.allocated.y` +/// - Does **not** modify width or height +/// +/// # Order +/// Should generally be called **after** size resolution +/// (e.g. `size_auto`, `width_auto`, `height_auto`). pub fn center(ctx: &mut DrawContext, _view: &View) { ctx.allocated.x = (ctx.area.width - ctx.allocated.width) / 2; ctx.allocated.y = (ctx.area.height - ctx.allocated.height) / 2; } +/// Automatically computes both width and height based on drawn content. +/// +/// # Sizing rules +/// - `Text`: +/// - Width = longest line length +/// - Height = number of lines +/// - `View`: +/// - Recursively executes the view in a fresh `DrawContext` +/// - Uses the child view's allocated size +/// - `Child`: ignored +/// +/// # Notes +/// - This function performs a layout pass only. +/// - It does not draw anything. +/// - Later instructions may overwrite the computed size. pub fn size_auto(ctx: &mut DrawContext, _view: &View) { for i in &ctx.drawing { match i { @@ -41,6 +88,15 @@ pub fn size_auto(ctx: &mut DrawContext, _view: &View) { } } +/// Automatically computes width based on drawn content. +/// +/// # Sizing rules +/// - `Text`: width is the longest line +/// - `View`: width is taken from the child view's auto-sized result +/// - Height is left unchanged +/// +/// # When to use +/// Useful when height is fixed or externally constrained. pub fn width_auto(ctx: &mut DrawContext, _view: &View) { for i in &ctx.drawing { match i { @@ -60,6 +116,15 @@ pub fn width_auto(ctx: &mut DrawContext, _view: &View) { } } +/// Automatically computes height based on drawn content. +/// +/// # Sizing rules +/// - `Text`: height is the number of lines +/// - `View`: height is taken from the child view's auto-sized result +/// - Width is left unchanged +/// +/// # Notes +/// This pass ignores line width and wrapping concerns. pub fn height_auto(ctx: &mut DrawContext, _view: &View) { for i in &ctx.drawing { match i { From 76984187a0420623a084b763447a937ad60210eb Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sun, 1 Feb 2026 17:46:40 +0100 Subject: [PATCH 13/16] View plugins for literals --- macros/src/emit.rs | 31 +++++++++++++++++++++++++------ macros/src/parse.rs | 19 ++++++++++++++----- src/component/scope.rs | 13 +++++++++++-- 3 files changed, 50 insertions(+), 13 deletions(-) diff --git a/macros/src/emit.rs b/macros/src/emit.rs index e61c26e..b943e1c 100644 --- a/macros/src/emit.rs +++ b/macros/src/emit.rs @@ -146,14 +146,33 @@ fn emit_node_scope(node: &RsxNode) -> TokenStream { fn emit_node(node: &RsxNode) -> TokenStream { match node { - RsxNode::Text { text, deps } => { + RsxNode::Text { + text, + deps, + plugins, + } => { let deps_emit = emit_deps(deps); - quote! { - scope.view(Arc::new({ - #deps_emit - move |ctx| ctx.draw_text(Point { x: 0, y: 0 }, &format!(#text)) - })); + if plugins.len() == 0 { + quote! { + scope.view(Arc::new({ + #deps_emit + move |ctx| ctx.draw_text(Point { x: 0, y: 0 }, &format!(#text)) + })); + } + } else { + let plugins_emit = emit_plugins(plugins); + + quote! { + scope.view_wrapped(Arc::new({ + #deps_emit + move |ctx| ctx.draw_text(Point { x: 0, y: 0 }, &format!(#text)) + }), std::sync::Arc::new(|ctx, view| { + let area = ctx.allocate(ctx.area.x, ctx.area.y, ctx.area.width, ctx.area.height); + ctx.draw_view(area, view.clone()); + #plugins_emit + })); + } } } diff --git a/macros/src/parse.rs b/macros/src/parse.rs index 2da9553..cc1c3ed 100644 --- a/macros/src/parse.rs +++ b/macros/src/parse.rs @@ -41,7 +41,11 @@ pub struct RsxProp { /// AST node representing different RSX constructs pub enum RsxNode { /// String literal: `"text"` - Text { text: LitStr, deps: Vec }, + Text { + plugins: Vec, + text: LitStr, + deps: Vec, + }, /// Expression node: `{expr}` Expr(Expr), /// Component instantiation: `Component { prop: value, ... }` @@ -212,6 +216,8 @@ impl Parse for RsxNode { }); } + let plugins = parse_plugins(input)?; + // "%$dep for $pat in $expr { $rsx }" if input.peek(LitStr) { let text = input.parse()?; @@ -229,16 +235,19 @@ impl Parse for RsxNode { .collect(), ); - return Ok(RsxNode::Text { text, deps }); + return Ok(RsxNode::Text { + text, + deps, + plugins, + }); } - parse_component_invocation(input) + parse_component_invocation(input, plugins) } } /// Parses Path or Path { ... } into RsxNode::Component. -fn parse_component_invocation(input: ParseStream) -> Result { - let plugins = parse_plugins(input)?; +fn parse_component_invocation(input: ParseStream, plugins: Vec) -> Result { let path: Path = input.parse()?; if !input.peek(Brace) { diff --git a/src/component/scope.rs b/src/component/scope.rs index 6ac0926..bfe5ec7 100644 --- a/src/component/scope.rs +++ b/src/component/scope.rs @@ -10,7 +10,7 @@ use crate::{engine::CommandExecutor, View, ViewWrapper}; use super::{context::Context, ComponentImpl}; /// A scope groups child components and manages their rendering -/// +/// /// Scopes form the hierarchical structure of a component tree. /// Each scope contains references to its child components and their /// optional view wrappers (for layout/styling). @@ -31,7 +31,7 @@ impl Scope { } /// Adds a child component to this scope - /// + /// /// The view_wrapper is optional and can be used for layout or styling. pub fn child( self: &Arc, @@ -53,4 +53,13 @@ impl Scope { self.children.lock().unwrap().push((ctx, None)); } + + /// Adds a view directly to this scope + pub fn view_wrapped(self: &Arc, view: View, view_wrapper: ViewWrapper) { + let ctx = Context::new(view, self.executor.clone()); + + ctx.refresh(); + + self.children.lock().unwrap().push((ctx, Some(view_wrapper))); + } } From 25bebcbb81d065f7eadeb24b10ff5c70980cdbdb Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sun, 1 Feb 2026 18:20:20 +0100 Subject: [PATCH 14/16] Flex components --- examples/hello_world.rs | 7 ++++- src/component/components/flex.rs | 51 ++++++++++++++++++++++++++++++++ src/component/components/mod.rs | 3 ++ src/component/context.rs | 26 +++++++++++----- src/component/mod.rs | 1 + src/lib.rs | 6 ++++ 6 files changed, 85 insertions(+), 9 deletions(-) create mode 100644 src/component/components/flex.rs create mode 100644 src/component/components/mod.rs diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 3cca2e7..0f0bec7 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -10,7 +10,12 @@ fn App(cx: &Arc) -> View { rsx! { // redraw is important because the effects won't be applied impl size_auto, center, redraw - Card { content: "Hello World".to_string() } + FlexRow { + impl size_auto, redraw + Card { content: "One".to_string() } + impl size_auto, redraw + Card { content: "Two".to_string() } + } } .view(&cx) } diff --git a/src/component/components/flex.rs b/src/component/components/flex.rs new file mode 100644 index 0000000..897dc64 --- /dev/null +++ b/src/component/components/flex.rs @@ -0,0 +1,51 @@ +use crate::component_prelude::*; + +#[component] +pub fn FlexRow(cx: &Arc, children: Rsx) -> View { + children.generate_children(&cx); + + Arc::new({ + let cx = cx.clone(); + move |ctx| { + for (child, view_wrapper) in cx.get_children() { + let view = child.get_view(); + + if let Some(view_wrapper) = view_wrapper { + let mut ctx2 = ctx.clone(); + + view_wrapper(&mut ctx2, view); + + ctx.drawing.append(&mut ctx2.drawing); + ctx.area.y += ctx2.allocated.height; + } else { + ctx.draw_view(ctx.area.clone(), view); + } + } + } + }) +} + +#[component] +pub fn FlexColumn(cx: &Arc, children: Rsx) -> View { + children.generate_children(&cx); + + Arc::new({ + let cx = cx.clone(); + move |ctx| { + for (child, view_wrapper) in cx.get_children() { + let view = child.get_view(); + + if let Some(view_wrapper) = view_wrapper { + let mut ctx2 = ctx.clone(); + + view_wrapper(&mut ctx2, view); + + ctx.drawing.append(&mut ctx2.drawing); + ctx.area.x += ctx2.allocated.width; + } else { + ctx.draw_view(ctx.area.clone(), view); + } + } + } + }) +} diff --git a/src/component/components/mod.rs b/src/component/components/mod.rs new file mode 100644 index 0000000..aaf7e40 --- /dev/null +++ b/src/component/components/mod.rs @@ -0,0 +1,3 @@ +mod flex; + +pub use flex::*; diff --git a/src/component/context.rs b/src/component/context.rs index 92fec14..7f00388 100644 --- a/src/component/context.rs +++ b/src/component/context.rs @@ -15,15 +15,15 @@ use access_cell::AccessCell; use crate::{ component::EventHandler, engine::{Command, CommandExecutor}, - render::DrawContext, hooks::{use_effect, HookDependency}, - View, + render::DrawContext, + View, ViewWrapper, }; use super::{scope::Scope, Component, ComponentImpl}; /// Context represents the runtime state and behavior of a component -/// +/// /// Each component instance has a Context that holds: /// - The component implementation /// - The current view (render result) @@ -58,7 +58,7 @@ impl Context { } /// Refreshes the component by re-rendering it - /// + /// /// Clears event handlers and calls the component to produce a new view. pub fn refresh(self: &Arc) { self.event_handlers @@ -76,7 +76,7 @@ impl Context { } /// Synchronously refreshes the component - /// + /// /// Blocks until the component has finished rendering. pub fn refresh_sync(self: &Arc) { let (tx, rx) = std::sync::mpsc::channel::<()>(); @@ -110,7 +110,7 @@ impl Context { } /// Registers an event handler for events of type T - /// + /// /// When an event of type T is emitted, the handler is called with /// the context and a reference to the event. pub fn on_event, &T) + Send + Sync + 'static>( @@ -132,7 +132,7 @@ impl Context { } /// Emits an event to this component and all descendants - /// + /// /// Calls all registered handlers for this event type, /// then propagates the event to child components. pub fn emit_event(self: &Arc, event: E) { @@ -153,7 +153,7 @@ impl Context { } /// Emits an event to this component in a spawned thread - /// + /// /// Similar to emit_event but handlers are called in spawned threads /// for concurrent execution. pub fn emit_event_threaded( @@ -216,6 +216,16 @@ impl Context { self.scopes.lock().unwrap().push(scope); } + /// Gets the children + pub fn get_children(self: &Arc) -> Vec<(Arc, Option)> { + self.scopes + .lock() + .unwrap() + .iter() + .flat_map(|s| s.children.lock().unwrap().clone()) + .collect::>() + } + /// Renders all child components to the draw context pub fn draw_children(self: &Arc, ctx: &mut DrawContext) { for scope in self.scopes.lock().unwrap().iter() { diff --git a/src/component/mod.rs b/src/component/mod.rs index e48b642..4bb0022 100644 --- a/src/component/mod.rs +++ b/src/component/mod.rs @@ -4,6 +4,7 @@ //! Components are reusable units of UI that can manage their own state //! and respond to events. +pub mod components; pub mod context; pub mod scope; diff --git a/src/lib.rs b/src/lib.rs index 3b76b8b..beb5fa5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,6 +48,12 @@ pub mod render; pub mod view_plugins; pub mod prelude { + //! Prelude module - Re-exports commonly used items for convenience + pub use crate::component::components::*; + pub use crate::component_prelude::*; +} + +pub mod component_prelude { //! Prelude module - Re-exports commonly used items for convenience pub use crate::component::{context::*, scope::*, *}; pub use crate::engine::*; From 2606e9b4e1b36a0bfe91172d347bc135d672ac8a Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sun, 1 Feb 2026 19:03:12 +0100 Subject: [PATCH 15/16] Area merging --- src/render.rs | 18 ++++++++++++++++++ src/view_plugins.rs | 31 +++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/src/render.rs b/src/render.rs index 91d3a65..cf0c830 100644 --- a/src/render.rs +++ b/src/render.rs @@ -114,3 +114,21 @@ impl DrawContext { self.drawing.clear(); } } + +impl Area { + fn right(&self) -> u16 { + self.x + self.width + } + + fn bottom(&self) -> u16 { + self.y + self.height + } + + pub fn merge(&mut self, other: &Self) { + self.x = self.x.min(other.x); + self.y = self.y.min(other.y); + + self.width = self.right().max(other.right()) - self.x; + self.height = self.bottom().max(other.bottom()) - self.y; + } +} diff --git a/src/view_plugins.rs b/src/view_plugins.rs index 368705a..cda57d0 100644 --- a/src/view_plugins.rs +++ b/src/view_plugins.rs @@ -88,6 +88,37 @@ pub fn size_auto(ctx: &mut DrawContext, _view: &View) { } } +/// Automatically computes both width and height based on drawn content. +/// Merges the allocation for multiple drawings. +pub fn size_auto_merge(ctx: &mut DrawContext, _view: &View) { + ctx.allocated.x = u16::MAX; + ctx.allocated.y = u16::MAX; + ctx.allocated.width = 0; + ctx.allocated.height = 0; + + for i in &ctx.drawing { + match i { + DrawInstruction::Text(_, text) => { + let mut height = 0; + + for line in text.lines() { + ctx.allocated.width = ctx.allocated.width.max(line.len() as u16); + height += 1; + } + + ctx.allocated.height = height; + } + DrawInstruction::View(area, view) => { + let mut c = DrawContext::new(area.clone()); + view(&mut c); + size_auto(&mut c, view); + ctx.allocated.merge(&c.allocated); + } + DrawInstruction::Child(_, _) => {} + } + } +} + /// Automatically computes width based on drawn content. /// /// # Sizing rules From 6ac9f33a6211914912e2fc666bace9c37f6c312f Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sun, 1 Feb 2026 19:13:15 +0100 Subject: [PATCH 16/16] Benchmark --- benchmark.csv | 448 +++++++++++++++++++++++++------------------------- 1 file changed, 224 insertions(+), 224 deletions(-) diff --git a/benchmark.csv b/benchmark.csv index 50f08bb..f7bdfb7 100644 --- a/benchmark.csv +++ b/benchmark.csv @@ -1,227 +1,227 @@ Iterx72,Nestingx72,Time µs -14,14,11964 -0,1,1078 -5,8,1818 -10,13,2632 -14,2,3392 -0,9,520 -9,3,2360 -10,5,2551 -10,11,2548 -6,8,1738 -12,12,2922 -0,8,601 -12,4,2919 -0,11,521 -6,4,1942 -11,7,2741 -9,4,2496 -5,9,1764 -13,9,3069 -2,5,1053 -5,11,1790 -14,1,3581 -5,0,962 -0,3,845 -1,3,868 -7,8,2013 -12,3,2987 -9,8,2328 -10,7,2533 -11,1,2766 -7,1,1938 -0,5,687 -6,9,1830 -10,14,2628 -6,12,1786 -4,0,835 -4,14,2464 -10,1,2633 -12,7,2948 -2,9,1066 -4,6,2646 -7,11,1999 -5,13,1748 -13,4,3110 -13,13,3094 -3,5,1166 -11,12,2745 -1,2,814 -9,5,2419 -2,11,1117 -1,13,835 -11,11,2759 -1,12,882 -9,1,2457 -0,6,680 -5,12,1794 -9,11,2326 -10,9,2546 -10,10,2604 -13,2,3250 -13,7,3113 -7,10,1959 -1,14,833 -6,6,1886 -14,7,3339 -6,7,1824 -11,6,2756 -12,6,3007 -14,6,3315 -4,3,11464 -2,14,924 -7,12,1966 -11,14,2817 -9,10,2367 -1,11,827 -8,14,2184 -14,0,643 -11,3,2781 -2,4,1123 -12,8,2950 -13,6,3160 -3,13,1289 -7,7,1989 -3,9,1142 -3,6,1168 -4,8,2788 -14,12,3346 -1,1,805 -5,10,1781 -7,13,1988 -5,5,2015 +14,14,7668 +0,4,714 +5,1,1550 +3,0,617 +9,6,2399 +0,3,882 +11,10,2691 +4,10,1343 +5,5,1562 +6,6,1749 +11,3,2719 +9,14,2315 +1,13,770 +14,8,3290 +6,14,1767 +6,11,1857 +10,11,2530 +7,11,1936 +13,5,3130 +3,13,1161 +0,2,928 +6,3,1751 +11,11,2727 +11,6,2860 +3,12,1170 +13,14,3090 +3,2,1140 +12,4,2952 +9,11,4884 +10,2,2726 +1,3,756 +3,6,1151 +6,5,1731 +9,1,2348 +11,4,2688 +2,13,944 +13,3,3125 +12,0,643 +1,4,721 +6,1,1757 +1,14,730 +10,7,2505 +7,9,2002 +13,6,3132 +4,6,1354 +4,7,1311 0,14,520 -11,8,2890 -3,10,1140 -13,10,3235 -3,14,1185 -8,10,2183 -12,10,2896 -2,8,1072 -9,6,2358 -13,1,3066 -3,8,1133 -11,10,2758 -4,13,2550 -1,0,643 -4,2,5482 -3,7,1155 -1,4,726 -3,3,1192 -4,11,2576 -8,0,619 -9,2,2444 -10,3,2581 -4,5,5400 -5,3,2444 -1,10,804 -10,2,2563 -0,7,592 -0,2,802 -1,5,741 -2,7,984 -10,0,755 -13,14,3112 -5,14,1811 -8,12,2180 -6,11,1723 -4,7,2897 -10,8,2536 -1,7,813 -6,0,699 -8,2,2187 -5,7,1811 -14,8,3304 -2,0,661 -8,13,2185 -1,9,786 -3,0,623 -12,5,3012 -8,1,2203 -8,11,2285 -14,10,3413 -4,10,2613 +4,8,1372 +14,7,3304 +7,0,620 +0,0,605 +11,9,2748 +4,13,1389 +12,10,2989 +8,1,2211 +9,9,2323 +4,11,1404 +5,9,1569 +3,11,1129 +5,7,1567 +10,14,2543 +5,0,612 +10,0,636 +6,4,1745 +5,4,1606 +1,2,707 +11,12,2727 +13,0,605 +8,2,2127 +10,4,2512 +4,4,1340 +6,8,1973 +7,8,2185 +3,10,1175 +9,2,2300 +1,6,743 +10,1,2547 +1,5,715 +3,1,1209 +7,4,2217 +4,5,1346 +11,1,2719 +7,10,1972 +9,3,2351 +14,13,3301 +14,3,3315 +2,12,939 +13,8,3098 +9,13,2339 +11,7,2749 +6,7,1742 +9,8,2348 +8,11,2171 +7,6,2370 +12,8,2988 +4,2,1349 +2,8,955 +8,4,2165 +2,5,938 +1,0,765 +14,1,3275 +14,11,3349 +2,11,1024 +14,2,3423 +12,12,2844 +8,12,2219 +14,5,3308 +3,14,1136 +1,9,710 +6,13,1831 +8,13,2296 11,5,2729 -6,1,2035 -12,1,2891 -5,4,2036 -2,3,1083 -14,3,3312 -3,2,1278 -5,2,2392 -8,7,2151 -5,6,2072 -3,12,1230 -2,10,954 -2,13,932 -13,8,3128 -0,12,521 -11,4,2769 -7,2,1947 -4,1,1802 -8,8,2163 -7,3,1966 -13,3,3207 -1,8,746 -12,9,2955 -10,12,2570 -9,9,2388 -4,12,2566 -7,0,619 -7,9,2004 -13,0,607 -14,9,3355 -3,4,1154 -11,2,2753 -11,13,2739 -6,13,1735 -7,6,1983 -8,3,2166 -7,14,1958 -6,2,2101 -13,5,3221 -8,4,2120 -10,4,2716 -14,5,3264 -14,13,3299 -11,9,2725 -13,11,3132 -1,6,809 -2,2,1155 -4,4,2242 -8,6,2218 -12,0,645 -7,4,1925 -2,6,1054 -9,13,2340 -13,12,3112 -14,4,3334 -3,11,1139 -12,14,3004 -4,9,2649 -0,13,527 -12,11,2976 -0,0,1559 -12,13,2952 -6,10,1749 -14,11,3365 -11,0,647 -2,12,1024 -10,6,2562 -8,5,2227 -9,12,2363 -2,1,1023 -0,10,528 -6,3,2005 -8,9,2195 -7,5,1966 -6,14,1755 -9,0,608 -14,14,3389 -9,7,2391 -0,4,688 -5,1,2574 -9,14,2353 -12,2,2975 -3,1,1276 -6,5,1804 \ No newline at end of file +4,1,1364 +14,10,3324 +12,5,2925 +8,5,2121 +10,3,2519 +1,7,698 +2,4,959 +6,9,1973 +7,5,2240 +2,7,936 +0,6,753 +5,10,1557 +2,3,948 +2,14,931 +8,6,2132 +2,10,936 +4,0,618 +13,4,3136 +5,6,1530 +5,8,1546 +7,14,1920 +8,10,2110 +12,9,3021 +1,11,738 +13,11,3121 +1,1,730 +0,1,981 +3,8,1271 +10,8,2554 +14,14,3267 +0,9,600 +4,9,1328 +9,12,3707 +6,12,1800 +0,5,684 +7,2,2180 +9,10,7168 +8,14,2163 +7,7,3732 +8,0,633 +5,14,1645 +8,8,2129 +12,7,2898 +0,10,573 +7,3,2255 +14,9,3272 +2,9,924 +9,4,2331 +10,5,2513 +10,13,2514 +11,8,2733 +14,12,3326 +0,8,618 +6,0,609 +8,9,2157 +12,1,3013 +12,11,2916 +12,3,2959 +9,0,623 +12,13,3064 +0,13,536 +5,13,1558 +6,10,1835 +11,13,2784 +13,9,3071 +0,7,599 +2,0,617 +9,5,2303 +8,3,2152 +12,2,2994 +0,12,542 +7,13,1923 +4,3,1350 +13,7,3177 +10,10,2533 +6,2,1748 +3,7,1209 +13,2,3097 +11,2,2731 +5,2,1562 +14,4,3273 +7,1,2088 +9,7,2333 +5,12,1556 +3,5,1177 +12,14,2888 +3,4,1153 +1,10,710 +11,0,614 +10,12,2505 +11,14,2827 +1,8,710 +14,6,3282 +7,12,1938 +2,2,943 +10,9,2578 +0,11,537 +2,1,926 +4,14,1367 +14,0,614 +1,12,716 +2,6,935 +13,10,3105 +10,6,2524 +4,12,1393 +8,7,2156 +12,6,2897 +5,11,1528 +13,13,3129 +5,3,1536 +3,3,1179 +3,9,1173 +13,12,3094 +13,1,3097 \ No newline at end of file