From 3428beef8ed5ab50c2b6c1a8ac80a5644bdad7cd Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Tue, 14 Jul 2026 13:29:16 -0400 Subject: [PATCH] Working UI engine --- Cargo.lock | 335 ++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 3 + pulse-ui/Cargo.toml | 4 +- pulse-ui/src/lib.rs | 102 ++++--------- pulse-ui/src/state.rs | 64 ++++++++ src/main.rs | 46 ++++-- 6 files changed, 461 insertions(+), 93 deletions(-) create mode 100644 pulse-ui/src/state.rs diff --git a/Cargo.lock b/Cargo.lock index 93fa691..609c1a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,26 +2,294 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + [[package]] name = "pin-project-lite" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + [[package]] name = "pulse-trader" version = "0.1.0-alpha.0" dependencies = [ "pulse-ui", + "tokio", ] [[package]] name = "pulse-ui" version = "0.1.0" dependencies = [ + "crossterm", "tokio", ] +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "tokio" version = "1.52.3" @@ -29,4 +297,71 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "pin-project-lite", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", ] diff --git a/Cargo.toml b/Cargo.toml index 6de5a68..772db70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,9 +5,12 @@ edition = "2024" [dependencies] pulse-ui = { workspace = true } +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } [workspace] members = ["pulse-ui"] [workspace.dependencies] pulse-ui = { path = "pulse-ui", version = "0.1.0-alpha.0" } +tokio = "1.52.3" +crossterm = "0.29.0" diff --git a/pulse-ui/Cargo.toml b/pulse-ui/Cargo.toml index 9d098b2..5755de8 100644 --- a/pulse-ui/Cargo.toml +++ b/pulse-ui/Cargo.toml @@ -4,4 +4,6 @@ version = "0.1.0" edition = "2024" [dependencies] -tokio = { version = "1.52.3", features = ["sync"] } +tokio = { workspace = true, features = ["sync"] } +crossterm = { workspace = true } + diff --git a/pulse-ui/src/lib.rs b/pulse-ui/src/lib.rs index b448b45..833911d 100644 --- a/pulse-ui/src/lib.rs +++ b/pulse-ui/src/lib.rs @@ -1,100 +1,48 @@ -use std::{any::Any, sync::Arc}; +use std::any::Any; -use tokio::sync::{Mutex, MutexGuard, mpsc}; +use tokio::sync::mpsc; pub mod layout; pub mod render; +pub mod state; pub mod unit; pub mod widget; -#[derive(Debug, Clone)] -pub struct Context { - tx: mpsc::Sender>, -} - -pub struct Refresh; - +#[allow(async_fn_in_trait)] pub trait App { // State - #[allow(async_fn_in_trait)] - async fn init(&mut self, ctx: Context); - #[allow(async_fn_in_trait)] + async fn init(&mut self, ctx: state::Context); async fn update(&mut self, event: Box); // Rendering - fn layout(&self) -> layout::Allocation; - fn render(&mut self, layout: layout::Allocation); + async fn layout(&self) -> layout::LayoutItem; + async fn render(&mut self, allocation: layout::Allocation); } -pub struct AppHandler { - pub app: A, -} +pub async fn run(create_app: impl FnOnce(&state::Context) -> A) { + let (tx, mut rx) = mpsc::channel(100); -impl AppHandler { - pub async fn run(&mut self) { - let (tx, mut rx) = mpsc::channel(100); + let ctx = state::Context { tx }; - self.app.init(Context { tx }).await; + let mut app = create_app(&ctx); - self.app.render(self.app.layout()); + app.init(ctx).await; - while let Some(event) = rx.recv().await { - self.app.update(event).await; - self.app.render(self.app.layout()); - } + app.render(get_screen().allocate(&app.layout().await)).await; + + while let Some(event) = rx.recv().await { + app.update(event).await; + app.render(get_screen().allocate(&app.layout().await)).await; } } -impl Context { - pub fn use_state(&self, v: T) -> State { - State { - value: Arc::new(Mutex::new(v)), - tx: self.tx.clone(), - } - } -} - -pub struct State { - pub value: Arc>, - tx: mpsc::Sender>, -} - -pub struct StateGuard<'a, T> { - is_mutated: bool, - value: MutexGuard<'a, T>, - tx: &'a mpsc::Sender>, -} - -impl State { - pub async fn lock<'a>(&'a self) -> StateGuard<'a, T> { - StateGuard { - value: self.value.lock().await, - is_mutated: false, - tx: &self.tx, - } - } -} - -impl<'a, T> std::ops::Deref for StateGuard<'a, T> { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.value - } -} - -impl<'a, T> std::ops::DerefMut for StateGuard<'a, T> { - fn deref_mut(&mut self) -> &mut Self::Target { - self.is_mutated = true; - - &mut self.value - } -} - -impl<'a, T> Drop for StateGuard<'a, T> { - fn drop(&mut self) { - if self.is_mutated { - self.tx.blocking_send(Box::new(Refresh)).unwrap() - } +pub fn get_screen() -> unit::Rect { + let (width, height) = crossterm::terminal::size().unwrap(); + + unit::Rect { + x: 0, + y: 0, + width, + height, } } diff --git a/pulse-ui/src/state.rs b/pulse-ui/src/state.rs new file mode 100644 index 0000000..279386c --- /dev/null +++ b/pulse-ui/src/state.rs @@ -0,0 +1,64 @@ +use std::{any::Any, sync::Arc}; + +use tokio::sync::{Mutex, MutexGuard, mpsc::Sender}; + +pub struct Refresh; + +#[derive(Debug, Clone)] +pub struct Context { + pub(crate) tx: Sender>, +} + +pub struct State { + pub value: Arc>, + tx: Sender>, +} + +pub struct StateGuard<'a, T> { + is_mutated: bool, + value: MutexGuard<'a, T>, + tx: &'a Sender>, +} + +impl Context { + pub fn use_state(&self, v: T) -> State { + State { + value: Arc::new(Mutex::new(v)), + tx: self.tx.clone(), + } + } +} + +impl State { + pub async fn lock<'a>(&'a self) -> StateGuard<'a, T> { + StateGuard { + value: self.value.lock().await, + is_mutated: false, + tx: &self.tx, + } + } +} + +impl<'a, T> std::ops::Deref for StateGuard<'a, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.value + } +} + +impl<'a, T> std::ops::DerefMut for StateGuard<'a, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.is_mutated = true; + + &mut self.value + } +} + +impl<'a, T> Drop for StateGuard<'a, T> { + fn drop(&mut self) { + if self.is_mutated { + self.tx.blocking_send(Box::new(Refresh)).unwrap() + } + } +} diff --git a/src/main.rs b/src/main.rs index 925c9dd..8fc34c1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,20 +1,36 @@ use pulse_ui::{ + App, layout::{LayoutItem, layout}, - unit::{Rect, Size}, + state::State, + unit::Size, }; -fn main() { - let layout = layout(vec![ - LayoutItem::Widget(Size::Flex(16)), - LayoutItem::Widget(Size::Flex(9)), - ]); - - let screen = Rect { - x: 0, - y: 0, - width: 3000, - height: 3000, - }; - - println!("{:?}", screen.allocate(&layout)); +pub struct PulseTradeApp { + count: State, +} + +impl App for PulseTradeApp { + async fn init(&mut self, ctx: pulse_ui::state::Context) {} + + async fn update(&mut self, event: Box) {} + + async fn layout(&self) -> pulse_ui::layout::LayoutItem { + layout(vec![ + LayoutItem::Widget(Size::Flex(16)), + LayoutItem::Widget(Size::Flex(9)), + ]) + } + + async fn render(&mut self, layout: pulse_ui::layout::Allocation) { + println!("{:?}", layout); + println!("{:?}", *self.count.lock().await); + } +} + +#[tokio::main] +async fn main() { + pulse_ui::run(|ctx| PulseTradeApp { + count: ctx.use_state(0), + }) + .await; }