diff --git a/Cargo.toml b/Cargo.toml index d9080c4..a40db29 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,11 +6,10 @@ license = "Apache-2.0" description = "A TUI library for advanced uis" repository = "https://github.com/osui-rs/osui" homepage = "https://osui.netlify.app" -documentation = "https://osui.netlify.app/docs/" +documentation = "https://osui.netlify.app/docs/" [features] -no_rsx = [] -no_elem = [] +rsx = [] [lib] name = "osui" diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 87702ef..09c9dd3 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -1,124 +1,68 @@ use std::sync::Arc; -use osui::prelude::*; +use osui::{prelude::*, rsx}; -#[derive(Debug, Clone)] -pub struct KeyPress; - -impl Event for KeyPress { - fn as_any(&self) -> &dyn std::any::Any { - self - } -} +pub struct Count(pub State); fn main() { let console = Console::new(); - - console.thread(move |ctx| loop { - crossterm::event::read().unwrap(); - ctx.emit_event_threaded(&KeyPress); - }); - console.run(app); } fn app(cx: &Arc) -> View { let count = use_state(0); + let mount = use_mount_manual(); - cx.on_event({ - let count = count.clone(); - move |_, _: &KeyPress| { - let mut count = count.get(); + // Sync state with children + use_sync_effect(cx, &count, |v| Count(v.clone()), &[&mount, &count]); - if *count > 5 { - *count = 0; - - return; + use_effect( + { + let count = count.clone(); + move || loop { + std::thread::sleep(std::time::Duration::from_millis(500)); + let mut count = count.get(); + if *count > 5 { + *count = 0; + continue; + } + *count += 1; } + }, + &[&mount], + ); - *count += 1; + rsx! { + // Static scope + my_component (ctx, view) { + let area = ctx.allocate(5, 0, 10, 10); + ctx.draw_view(area, view); } - }); - { - let scope = cx.scope(); - - scope.child( - my_component, - Some(Arc::new(|ctx, view| { - let area = ctx.allocate(5, 0, 0, 0); - ctx.draw_view(area, view) - })), - ); - } - - { - cx.dyn_scope( - { - let count = count.clone(); - move |scope| { - if *count.get() != 0 { - if scope.children.lock().unwrap().len() == 0 { - scope.child( - my_component, - Some(Arc::new(|ctx, view| { - let area = ctx.allocate(5, 2, 0, 0); - ctx.draw_view(area, view) - })), - ); - } - } else { - scope.children.lock().unwrap().clear(); - } - } - }, - &[&count], - ); - } - - { - cx.dyn_scope( - { - let count = count.clone(); - move |scope| { - scope.children.lock().unwrap().clear(); - - for i in 0..count.get_dl() { - scope.view(Arc::new(move |ctx| { - ctx.draw_text(Point { x: 0, y: i }, &format!("{i}")); - })); - } - } - }, - &[&count], - ); - } - - Arc::new({ - let cx = cx.clone(); - - move |ctx| { - cx.draw_children(ctx); + // Dynamic scope + if %count (*count.get() > 5) { + my_component (ctx, view) { + let area = ctx.allocate(18, 0, 10, 10); + ctx.draw_view(area, view); + } } - }) + + // Dynamic scope + for %count (i in 0..count.get_dl()) { + "{i}" @Point { x: 0, y: i }; + } + + !mount mount + } + .view(cx.clone()) } fn my_component(cx: &Arc) -> View { - let count = use_state(1); + // Sync state with parent + let count = use_sync_state(cx, 0, |Count(v)| v.get_dl()); - cx.on_event({ - let count = count.clone(); - move |_cx, _event: &KeyPress| { - let mut count = count.get(); - if *count > 5 { - _cx.refresh(); - return; - } - *count += 1; - } - }); - - Arc::new(move |ctx| { - ctx.draw_text(Point { x: 0, y: 0 }, &format!("Count: {count}")); - }) + rsx! { + "Count: {count}" + } + .view(cx.clone()) } diff --git a/src/component.rs b/src/component.rs index a65bde0..40e63c4 100644 --- a/src/component.rs +++ b/src/component.rs @@ -10,18 +10,8 @@ use crate::{ View, ViewWrapper, }; -pub trait Event { - fn as_any(&self) -> &dyn Any; -} - -impl<'a> dyn Event + 'a { - pub fn get(&self) -> Option<&T> { - self.as_any().downcast_ref() - } -} - pub type Component = Arc) -> View + Send + Sync>; -pub type EventHandler = Arc, &dyn Event) + Send + Sync>>; +pub type EventHandler = Arc, &dyn Any) + Send + Sync>>; pub struct Scope { pub children: Mutex, Option)>>, @@ -31,7 +21,7 @@ pub struct Context { component: Mutex, event_handlers: Mutex>>, view: Mutex, - scopes: Mutex>>, + pub(crate) scopes: Mutex>>, } impl Context { @@ -77,7 +67,7 @@ impl Context { self.view.lock().unwrap().clone() } - pub fn on_event, &T) + Send + Sync + 'static>( + pub fn on_event, &T) + Send + Sync + 'static>( self: &Arc, handler: F, ) { @@ -87,15 +77,15 @@ impl Context { .entry(TypeId::of::()) .or_insert_with(|| Vec::new()) .push(Arc::new(Mutex::new( - move |ctx: &Arc, event: &dyn Event| { - if let Some(e) = event.get::() { + move |ctx: &Arc, event: &dyn Any| { + if let Some(e) = event.downcast_ref::() { (handler)(ctx, e); } }, ))); } - pub fn emit_event(self: &Arc, event: &E) { + pub fn emit_event(self: &Arc, event: &E) { if let Some(v) = self.event_handlers.lock().unwrap().get(&TypeId::of::()) { for i in v { (i.lock().unwrap())(self, event); @@ -109,7 +99,7 @@ impl Context { } } - pub fn emit_event_threaded( + pub fn emit_event_threaded( self: &Arc, event: &E, ) { @@ -150,6 +140,8 @@ impl Context { }); self.scopes.lock().unwrap().push(scope.clone()); + drawer(&scope); + use_effect( { let scope = scope.clone(); @@ -179,6 +171,12 @@ impl Context { } impl Scope { + pub fn new() -> Arc { + Arc::new(Self { + children: Mutex::new(Vec::new()), + }) + } + pub fn child) -> View + Send + Sync + 'static>( self: &Arc, child: F, diff --git a/src/frontend.rs b/src/frontend.rs new file mode 100644 index 0000000..e90ea99 --- /dev/null +++ b/src/frontend.rs @@ -0,0 +1,58 @@ +use std::sync::Arc; + +use crate::{ + component::{Context, Scope}, + state::HookDependency, + View, +}; + +pub enum RsxScope { + Static(Arc), + Dynamic( + Arc) + Send + Sync>, + Vec>, + ), +} + +pub struct Rsx(Vec); + +impl Rsx { + pub fn new() -> Self { + Self(Vec::new()) + } + + pub fn static_scope(&mut self, scope: Arc) { + self.0.push(RsxScope::Static(scope)); + } + + pub fn dynamic_scope) + Send + Sync + 'static>( + &mut self, + drawer: F, + dependencies: Vec>, + ) { + self.0 + .push(RsxScope::Dynamic(Arc::new(drawer), dependencies)); + } + + pub fn view(&self, context: Arc) -> View { + for scope in &self.0 { + match scope { + RsxScope::Static(scope) => context.scopes.lock().unwrap().push(scope.clone()), + RsxScope::Dynamic(drawer, dependencies) => { + let drawer = drawer.clone(); + context.dyn_scope( + move |c| drawer(c), + &dependencies.iter().map(|d| d.as_ref()).collect::>(), + ); + } + } + } + + let context = context.clone(); + Arc::new({ + move |ctx| { + context.draw_children(ctx); + } + }) + } +} diff --git a/src/lib.rs b/src/lib.rs index 16af55e..1462c02 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,8 @@ use crate::render::DrawContext; pub mod component; pub mod engine; +pub mod frontend; +pub mod macros; pub mod render; pub mod state; diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000..23c3e7a --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,165 @@ +#[macro_export] +macro_rules! rsx { + () => { + $crate::frontend::Rsx::new() + }; + + ($($rsx:tt)+) => {{ + let mut r = $crate::frontend::Rsx::new(); + { + $crate::rsx_scope!(r, $($rsx)+); + } + r + }}; +} + +#[macro_export] +macro_rules! rsx_scope { + ($rsx:expr, for $(%$($dep:ident $(as $dp:pat,)?),+)? ($p:pat in $v:expr) {$($inner:tt)*} $($rest:tt)*) => { + { + $($($crate::rsx_dep!($dep);)+)? + + $rsx.dynamic_scope({ + $($($crate::rsx_dep!($dep $(as $dp)?);)+)? + move |scope| { + scope.children.lock().unwrap().clear(); + for $p in $v { + $crate::rsx_child!(scope, $($inner)*); + } + } + }, vec![$($(Box::new($dep)),+)?]); + } + + $crate::rsx_scope!($rsx, $($rest)*); + }; + + ($rsx:expr, if $(%$($dep:ident $(as $dp:pat,)?),+)? ($st:expr) {$($inner:tt)*} $($rest:tt)*) => { + { + $($($crate::rsx_dep!($dep);)+)? + + $rsx.dynamic_scope({ + $($($crate::rsx_dep!($dep $(as $dp)?);)+)? + move |scope| { + if $st { + if scope.children.lock().unwrap().len() == 0 { + $crate::rsx_child!(scope, $($inner)*); + } + } else { + scope.children.lock().unwrap().clear(); + } + } + }, vec![$($(Box::new($dep)),+)?]); + } + + $crate::rsx_scope!($rsx, $($rest)*); + }; + + ($rsx:expr, $(%$($dep:ident $(as $dp:pat,)?),+)? $text:literal @$d:expr; $($rest:tt)*) => { + { + let scope = $crate::component::Scope::new(); + + $($($crate::rsx_dep!($dep $(as $dp)?);)+)? + + $crate::rsx_child!(scope, $text @$d;); + + $rsx.static_scope(scope); + } + + $crate::rsx_scope!($rsx, $($rest)*); + }; + + ($rsx:expr, $(%$($dep:ident $(as $dp:pat,)?),+)? $text:literal $($rest:tt)*) => { + { + let scope = $crate::component::Scope::new(); + + $($($crate::rsx_dep!($dep $(as $dp)?);)+)? + + $crate::rsx_child!(scope, $text); + + $rsx.static_scope(scope); + } + + $crate::rsx_scope!($rsx, $($rest)*); + }; + + ($rsx:expr, $component:ident ($ctx:ident, $view:ident) $body:block $($rest:tt)*) => { + { + let scope = $crate::component::Scope::new(); + + $crate::rsx_child!(scope, $component ($ctx, $view) $body); + + $rsx.static_scope(scope); + } + + $crate::rsx_scope!($rsx, $($rest)*); + }; + + ($rsx:expr, $component:ident $($rest:tt)*) => { + { + let scope = $crate::component::Scope::new(); + + $crate::rsx_child!(scope, $component); + + $rsx.static_scope(scope); + } + + $crate::rsx_scope!($rsx, $($rest)*); + }; + + ($rsx:expr, !mount $mount:ident $($rest:tt)*) => { + { + $mount.mount(); + } + + $crate::rsx_scope!($rsx, $($rest)*); + }; + + ($rsx:expr,) => {}; +} + +#[macro_export] +macro_rules! rsx_child { + ($scope:expr, $text:literal @$d:expr; $($rest:tt)*) => { + { + $scope.view(Arc::new(move |ctx| ctx.draw_text($d, &format!($text)))); + } + + $crate::rsx_child!($scope, $($rest)*); + }; + + ($scope:expr, $text:literal $($rest:tt)*) => { + { + $scope.view(Arc::new(move |ctx| ctx.draw_text(Point { x: 0, y: 0 }, &format!($text)))); + } + + $crate::rsx_child!($scope, $($rest)*); + }; + + ($scope:expr, $component:ident ($ctx:ident, $view:ident) $body:block $($rest:tt)*) => { + { + $scope.child($component, Some(Arc::new(|$ctx, $view| $body))); + } + + $crate::rsx_child!($scope, $($rest)*); + }; + + ($scope:expr, $component:ident $($rest:tt)*) => { + { + $scope.child($component, None); + } + $crate::rsx_child!($scope, $($rest)*); + }; + + ($rsx:expr,) => {}; +} + +#[macro_export] +macro_rules! rsx_dep { + ($dep:ident as $dp:pat) => { + let $dp = $dep.clone(); + }; + + ($dep:ident) => { + let $dep = $dep.clone(); + }; +} diff --git a/src/state.rs b/src/state.rs index 669ecca..502e56c 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,13 +1,16 @@ use std::{ + any::Any, fmt::{Debug, Display, Formatter, Result as FmtResult}, ops::{Deref, DerefMut}, sync::{Arc, Mutex, MutexGuard}, }; +use crate::prelude::Context; + #[derive(Clone)] pub struct HookEffect(Arc>); -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct State { value: Arc>, dependents: Arc>>, @@ -19,6 +22,9 @@ pub struct Inner<'a, T> { updated: bool, } +#[derive(Debug, Clone)] +pub struct Mount(Arc>, Arc>>); + pub fn use_state(v: T) -> State { State { value: Arc::new(Mutex::new(v)), @@ -54,6 +60,13 @@ impl State { d.call(); } } + + pub fn clone(&self) -> Self { + Self { + dependents: self.dependents.clone(), + value: self.value.clone(), + } + } } impl Display for State { @@ -112,6 +125,26 @@ impl HookDependency for State { } } +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 { + 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(); + } +} + pub fn use_effect(f: F, dependencies: &[&dyn HookDependency]) { let f = Arc::new(Mutex::new(f)); let hook = HookEffect(Arc::new(Mutex::new({ @@ -126,3 +159,55 @@ pub fn use_effect(f: F, dependencies: &[&dyn d.on_update(hook.clone()); } } + +pub fn use_mount() -> Mount { + Mount(Arc::new(Mutex::new(true)), Arc::new(Mutex::new(Vec::new()))) +} + +pub fn use_mount_manual() -> Mount { + Mount( + Arc::new(Mutex::new(false)), + Arc::new(Mutex::new(Vec::new())), + ) +} + +pub fn use_sync_state< + T: Send + Sync + 'static, + E: Any + 'static, + D: Fn(&E) -> T + Send + Sync + 'static, +>( + cx: &Arc, + v: T, + decoder: D, +) -> State { + let state = use_state(v); + + cx.on_event({ + let state = state.clone(); + move |_, v: &E| state.set(decoder(v)) + }); + + state +} + +pub fn use_sync_effect< + T: Send + Sync + 'static, + Ev: '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 || { + cx.emit_event(&encoder(&state)); + } + }, + deps, + ); +}