7 Commits
Author SHA1 Message Date
selimaj-dev d40a032bc1 Improved state structure 2026-02-01 13:40:22 +01:00
selimaj-dev 8c4576396b Improved state structure 2026-02-01 13:39:37 +01:00
selimaj-dev 039e751dd1 Removed dupes of deps in {} 2026-02-01 11:01:20 +01:00
selimaj-dev c3519fa6ed Improved Card by dynamicifying the bars 2026-02-01 10:31:37 +01:00
selimaj-dev 5e4b78f353 Removed excess clone in Card component 2026-02-01 10:23:34 +01:00
selimaj-dev b141e134d4 Direct text dependencies 2026-02-01 10:21:28 +01:00
selimaj-dev b23c8c68dc Text dependencies 2026-02-01 10:11:41 +01:00
10 changed files with 266 additions and 146 deletions
+11 -1
View File
@@ -8,7 +8,17 @@ pub fn main() {
#[component] #[component]
fn App(cx: &Arc<Context>) -> View { fn App(cx: &Arc<Context>) -> View {
rsx! { rsx! {
"Hello World" Card { content: "Hello World".to_string() }
}
.view(&cx)
}
#[component]
fn Card(cx: &Arc<Context>, content: String) -> View {
let bar = "-".repeat(content.len());
rsx! {
"{bar}\n{content}\n{bar}"
} }
.view(&cx) .view(&cx)
} }
+15 -7
View File
@@ -38,7 +38,7 @@ fn emit_deps(deps: &[Dep]) -> TokenStream {
/// Emits a Vec of dependencies as HookDependency trait objects /// Emits a Vec of dependencies as HookDependency trait objects
fn emit_deps_vec(deps: &[Dep]) -> TokenStream { 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; let ident = &d.ident;
quote! { quote! {
@@ -54,9 +54,12 @@ fn emit_deps_vec(deps: &[Dep]) -> TokenStream {
/// Emits code for a single node within a scope /// Emits code for a single node within a scope
fn emit_node_scope(node: &RsxNode) -> TokenStream { fn emit_node_scope(node: &RsxNode) -> TokenStream {
match node { match node {
RsxNode::Text(_) => { RsxNode::Text { deps, .. } => {
let emit = emit_node(node); let emit = emit_node(node);
let deps_emit = emit_deps(deps);
quote! { quote! {
#deps_emit
r.static_scope(move |scope| {#emit}); r.static_scope(move |scope| {#emit});
} }
} }
@@ -129,11 +132,16 @@ fn emit_node_scope(node: &RsxNode) -> TokenStream {
fn emit_node(node: &RsxNode) -> TokenStream { fn emit_node(node: &RsxNode) -> TokenStream {
match node { match node {
RsxNode::Text(text) => quote! { RsxNode::Text { text, deps } => {
scope.view(Arc::new(move |ctx| { let deps_emit = emit_deps(deps);
ctx.draw_text(Point { x: 0, y: 0 }, &format!(#text))
})); quote! {
}, scope.view(Arc::new({
#deps_emit
move |ctx| ctx.draw_text(Point { x: 0, y: 0 }, &format!(#text))
}));
}
}
RsxNode::Component { RsxNode::Component {
path, path,
+68 -3
View File
@@ -2,6 +2,9 @@
//! //!
//! Parses RSX syntax into an AST that can be emitted as Rust code. //! 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::braced;
use syn::parse::discouraged::Speculative; use syn::parse::discouraged::Speculative;
use syn::{ use syn::{
@@ -37,7 +40,7 @@ pub struct RsxProp {
/// AST node representing different RSX constructs /// AST node representing different RSX constructs
pub enum RsxNode { pub enum RsxNode {
/// String literal: `"text"` /// String literal: `"text"`
Text(LitStr), Text { text: LitStr, deps: Vec<Dep> },
/// Expression node: `{expr}` /// Expression node: `{expr}`
Expr(Expr), Expr(Expr),
/// Component instantiation: `Component { prop: value, ... }` /// Component instantiation: `Component { prop: value, ... }`
@@ -76,6 +79,7 @@ pub enum RsxNode {
pub struct Dep { pub struct Dep {
pub ident: Ident, pub ident: Ident,
pub pat: Option<Pat>, pub pat: Option<Pat>,
pub is_dep: bool,
} }
fn parse_deps(input: ParseStream) -> Result<Vec<Dep>> { fn parse_deps(input: ParseStream) -> Result<Vec<Dep>> {
@@ -84,7 +88,15 @@ fn parse_deps(input: ParseStream) -> Result<Vec<Dep>> {
if input.peek(Token![%]) { if input.peek(Token![%]) {
input.parse::<Token![%]>()?; input.parse::<Token![%]>()?;
loop { loop {
let is_ref = if input.peek(Token![ref]) {
input.parse::<Token![ref]>()?;
true
} else {
false
};
let ident: Ident = input.parse()?; let ident: Ident = input.parse()?;
let pat = if input.peek(Token![as]) { let pat = if input.peek(Token![as]) {
input.parse::<Token![as]>()?; input.parse::<Token![as]>()?;
// Patterns in syn 2.0 are parsed via `Pat::parse_multi` // Patterns in syn 2.0 are parsed via `Pat::parse_multi`
@@ -94,7 +106,11 @@ fn parse_deps(input: ParseStream) -> Result<Vec<Dep>> {
None None
}; };
deps.push(Dep { ident, pat }); deps.push(Dep {
ident,
pat,
is_dep: !is_ref,
});
if !input.peek(Token![,]) { if !input.peek(Token![,]) {
break; break;
@@ -161,7 +177,22 @@ impl Parse for RsxNode {
// "%$dep for $pat in $expr { $rsx }" // "%$dep for $pat in $expr { $rsx }"
if input.peek(LitStr) { if input.peek(LitStr) {
return Ok(RsxNode::Text(input.parse()?)); 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) parse_component_invocation(input)
@@ -239,3 +270,37 @@ fn parse_children(input: ParseStream) -> Result<Vec<RsxNode>> {
} }
Ok(nodes) Ok(nodes)
} }
fn extract_vars_from_lit(lit: &LitStr) -> HashSet<Ident> {
let s = lit.value();
let mut vars = HashSet::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.insert(Ident::new(
&name.split_once(':').unwrap_or((&name, &name)).0,
Span::call_site(),
));
}
}
}
vars
}
+1 -1
View File
@@ -16,7 +16,7 @@ use crate::{
component::EventHandler, component::EventHandler,
engine::{Command, CommandExecutor}, engine::{Command, CommandExecutor},
render::DrawContext, render::DrawContext,
state::{use_effect, HookDependency}, hooks::{use_effect, HookDependency},
View, View,
}; };
+1 -1
View File
@@ -7,7 +7,7 @@
use std::sync::Arc; use std::sync::Arc;
use crate::component::{context::Context, scope::Scope}; 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 /// Trait for converting values to RSX
pub trait ToRsx { pub trait ToRsx {
+55
View File
@@ -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: FnMut() + Send + Sync + 'static>(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<T>) -> Ev + Send + Sync + 'static,
>(
cx: &Arc<Context>,
state: &State<T>,
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,
);
}
+44
View File
@@ -0,0 +1,44 @@
//! # 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.
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<Mutex<dyn FnMut() + Send + Sync>>);
/// 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: Fn() + Send + Sync + 'static>(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")
}
}
+58
View File
@@ -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<Mutex<bool>>, Arc<Mutex<Vec<HookEffect>>>);
/// 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();
}
}
+11 -131
View File
@@ -1,7 +1,7 @@
//! # State Management Module //! # State hooks module
//! //!
//! Provides React-like hooks for managing component state and side effects. //! Provides React-like use_state hooks for managing states.
//! This module includes useState, useEffect, useMount, and state synchronization hooks. //! This module includes use_state and use_sync_state.
use std::{ use std::{
any::Any, any::Any,
@@ -10,12 +10,9 @@ use std::{
sync::{Arc, Mutex, MutexGuard}, sync::{Arc, Mutex, MutexGuard},
}; };
use super::{HookDependency, HookEffect};
use crate::component::context::Context; use crate::component::context::Context;
/// Effect callback that can be triggered by state changes
#[derive(Clone)]
pub struct HookEffect(Arc<Mutex<dyn FnMut() + Send + Sync>>);
/// State holder for reactive values /// State holder for reactive values
/// ///
/// Similar to React's useState hook. Holds a value and tracks dependents /// Similar to React's useState hook. Holds a value and tracks dependents
@@ -38,23 +35,6 @@ pub struct Inner<'a, T> {
updated: bool, 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<Mutex<bool>>, Arc<Mutex<Vec<HookEffect>>>);
/// Creates a new state value
///
/// Returns a State that can be read and written from multiple threads.
pub fn use_state<T>(v: T) -> State<T> {
State {
value: Arc::new(Mutex::new(v)),
dependents: Arc::new(Mutex::new(Vec::new())),
}
}
impl<T: Clone> State<T> { impl<T: Clone> State<T> {
/// Gets a cloned copy of the state value /// Gets a cloned copy of the state value
/// ///
@@ -106,24 +86,6 @@ impl<T: Display> Display for State<T> {
} }
} }
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: Fn() + Send + Sync + 'static>(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 /// Inner implements Deref for read access and DerefMut for write access
/// On drop after mutation, automatically triggers dependent effects /// On drop after mutation, automatically triggers dependent effects
impl<T> Drop for Inner<'_, T> { impl<T> Drop for Inner<'_, T> {
@@ -150,77 +112,12 @@ impl<T> 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<T: Send + Sync> HookDependency for State<T> { impl<T: Send + Sync> HookDependency for State<T> {
fn on_update(&self, hook: HookEffect) { fn on_update(&self, hook: HookEffect) {
self.dependents.lock().unwrap().push(hook); 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: FnMut() + Send + Sync + 'static>(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 /// Synchronizes state with events from the context
/// ///
/// Creates state that is automatically updated when events are emitted /// Creates state that is automatically updated when events are emitted
@@ -244,29 +141,12 @@ pub fn use_sync_state<
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. /// Returns a State that can be read and written from multiple threads.
/// The encoder function converts state values to events. pub fn use_state<T>(v: T) -> State<T> {
pub fn use_sync_effect< State {
T: Send + Sync + 'static, value: Arc::new(Mutex::new(v)),
Ev: Send + Sync + 'static, dependents: Arc::new(Mutex::new(Vec::new())),
E: Fn(&State<T>) -> Ev + Send + Sync + 'static, }
>(
cx: &Arc<Context>,
state: &State<T>,
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,
);
} }
+2 -2
View File
@@ -44,7 +44,7 @@ pub mod component;
pub mod engine; pub mod engine;
pub mod frontend; pub mod frontend;
pub mod render; pub mod render;
pub mod state; pub mod hooks;
pub mod prelude { pub mod prelude {
//! Prelude module - Re-exports commonly used items for convenience //! Prelude module - Re-exports commonly used items for convenience
@@ -52,7 +52,7 @@ pub mod prelude {
pub use crate::engine::*; pub use crate::engine::*;
pub use crate::frontend::*; pub use crate::frontend::*;
pub use crate::render::*; pub use crate::render::*;
pub use crate::state::*; pub use crate::hooks::*;
pub use crate::{sleep, Error, Result, View, ViewWrapper}; pub use crate::{sleep, Error, Result, View, ViewWrapper};
pub use crossterm; pub use crossterm;
pub use osui_macros::{component, rsx}; pub use osui_macros::{component, rsx};