From 809e5d1529d41f07bad4426f50f46614e9c3e185 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Tue, 14 Jul 2026 21:42:29 +0200 Subject: [PATCH 01/11] Stateful closing and io input --- pulse-ui/src/lib.rs | 55 ++++++++++++++++++++++++++++++------------- pulse-ui/src/state.rs | 15 ++++++++---- src/main.rs | 8 +++++-- 3 files changed, 56 insertions(+), 22 deletions(-) diff --git a/pulse-ui/src/lib.rs b/pulse-ui/src/lib.rs index fa13f10..f60bd7b 100644 --- a/pulse-ui/src/lib.rs +++ b/pulse-ui/src/lib.rs @@ -11,25 +11,15 @@ pub mod widget; #[allow(async_fn_in_trait)] pub trait App { // State - async fn init(&mut self, ctx: state::Context); - async fn update(&mut self, event: Box); + async fn init(&mut self, ctx: &state::Context); + async fn update(&mut self, ctx: &state::Context, event: Box); // Rendering async fn layout(&self) -> layout::LayoutItem; async fn render(&mut self, allocation: layout::Allocation); async fn refresh(&mut self) { - crossterm::execute!( - std::io::stdout(), - crossterm::terminal::Clear(crossterm::terminal::ClearType::All) - ) - .unwrap(); - - crossterm::execute!( - std::io::stdout(), - crossterm::terminal::Clear(crossterm::terminal::ClearType::Purge) - ) - .unwrap(); + clear(); crossterm::execute!(std::io::stdout(), crossterm::cursor::Hide).unwrap(); @@ -41,18 +31,35 @@ pub trait App { pub async fn run(create_app: impl FnOnce(&state::Context) -> A) { let (tx, mut rx) = mpsc::channel(100); - let ctx = state::Context { tx }; + let ctx = state::Context { tx: tx.clone() }; + + crossterm::terminal::enable_raw_mode().unwrap(); + + tokio::spawn(async move { + tx.send(Box::new(crossterm::event::read().unwrap())) + .await + .unwrap(); + }); let mut app = create_app(&ctx); - app.init(ctx).await; + app.init(&ctx).await; app.refresh().await; while let Some(event) = rx.recv().await { - app.update(event).await; + if let Some(state::Close) = event.downcast_ref() { + break; + } + + app.update(&ctx, event).await; app.refresh().await; } + + crossterm::terminal::disable_raw_mode().unwrap(); + crossterm::execute!(std::io::stdout(), crossterm::cursor::Show).unwrap(); + + clear(); } pub fn get_screen() -> unit::Rect { @@ -65,3 +72,19 @@ pub fn get_screen() -> unit::Rect { height, } } + +pub fn clear() { + crossterm::execute!( + std::io::stdout(), + crossterm::terminal::Clear(crossterm::terminal::ClearType::All) + ) + .unwrap(); + + crossterm::execute!( + std::io::stdout(), + crossterm::terminal::Clear(crossterm::terminal::ClearType::Purge) + ) + .unwrap(); + + crossterm::execute!(std::io::stdout(), crossterm::cursor::MoveTo(0, 0)).unwrap(); +} diff --git a/pulse-ui/src/state.rs b/pulse-ui/src/state.rs index 279386c..28b7141 100644 --- a/pulse-ui/src/state.rs +++ b/pulse-ui/src/state.rs @@ -3,25 +3,26 @@ use std::{any::Any, sync::Arc}; use tokio::sync::{Mutex, MutexGuard, mpsc::Sender}; pub struct Refresh; +pub struct Close; #[derive(Debug, Clone)] pub struct Context { - pub(crate) tx: Sender>, + pub(crate) tx: Sender>, } pub struct State { pub value: Arc>, - tx: Sender>, + tx: Sender>, } pub struct StateGuard<'a, T> { is_mutated: bool, value: MutexGuard<'a, T>, - tx: &'a Sender>, + tx: &'a Sender>, } impl Context { - pub fn use_state(&self, v: T) -> State { + pub fn use_state(&self, v: T) -> State { State { value: Arc::new(Mutex::new(v)), tx: self.tx.clone(), @@ -62,3 +63,9 @@ impl<'a, T> Drop for StateGuard<'a, T> { } } } + +impl Context { + pub async fn close(&self) { + self.tx.send(Box::new(Close)).await.unwrap() + } +} diff --git a/src/main.rs b/src/main.rs index 74f9a7f..5187e18 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ +use std::any::Any; + use pulse_ui::{ App, layout::{LayoutItem, layout}, @@ -10,9 +12,11 @@ pub struct PulseTradeApp { } impl App for PulseTradeApp { - async fn init(&mut self, ctx: pulse_ui::state::Context) {} + async fn init(&mut self, ctx: &pulse_ui::state::Context) {} - async fn update(&mut self, event: Box) {} + async fn update(&mut self, ctx: &pulse_ui::state::Context, event: Box) { + ctx.close().await + } async fn layout(&self) -> pulse_ui::layout::LayoutItem { layout(vec![ From c758f55fc6cc861e38838a4696a62bebdde8779c Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Tue, 14 Jul 2026 22:08:26 +0200 Subject: [PATCH 02/11] Outline --- pulse-ui/src/lib.rs | 2 ++ pulse-ui/src/render.rs | 8 +++++++- pulse-ui/src/widget/mod.rs | 2 ++ pulse-ui/src/widget/outline.rs | 20 ++++++++++++++++++++ src/main.rs | 6 ++---- 5 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 pulse-ui/src/widget/outline.rs diff --git a/pulse-ui/src/lib.rs b/pulse-ui/src/lib.rs index f60bd7b..eafb645 100644 --- a/pulse-ui/src/lib.rs +++ b/pulse-ui/src/lib.rs @@ -25,6 +25,8 @@ pub trait App { self.render(get_screen().allocate(&self.layout().await)) .await; + + crossterm::execute!(std::io::stdout()).unwrap(); } } diff --git a/pulse-ui/src/render.rs b/pulse-ui/src/render.rs index 8e458be..bde8aaf 100644 --- a/pulse-ui/src/render.rs +++ b/pulse-ui/src/render.rs @@ -66,6 +66,12 @@ impl Allocation { widget.render(&mut scope); scope.draw(); } + + pub fn draw_frame(&self, index: usize, widget: W) { + let mut scope = RenderScope::new(self.frame_alloc[index]); + widget.render(&mut scope); + scope.draw(); + } } impl RenderScope { @@ -89,5 +95,5 @@ impl RenderScope { pub fn print_at(x: u16, y: u16, text: &str) { crossterm::execute!(stdout(), crossterm::cursor::MoveTo(x, y)).unwrap(); - println!("{text}"); + print!("{text}"); } diff --git a/pulse-ui/src/widget/mod.rs b/pulse-ui/src/widget/mod.rs index 8d37a34..f11b45f 100644 --- a/pulse-ui/src/widget/mod.rs +++ b/pulse-ui/src/widget/mod.rs @@ -1,3 +1,5 @@ +pub mod outline; + use crate::render::RenderScope; pub trait Widget { diff --git a/pulse-ui/src/widget/outline.rs b/pulse-ui/src/widget/outline.rs new file mode 100644 index 0000000..b2ab554 --- /dev/null +++ b/pulse-ui/src/widget/outline.rs @@ -0,0 +1,20 @@ +use crate::widget::Widget; + +pub struct Outline; + +impl Widget for Outline { + fn render(&self, scope: &mut crate::render::RenderScope) { + scope.draw_text(0, "-".repeat(scope.rect.width as usize)); + + scope.draw_text((0, 1), "|\n".repeat(scope.rect.height as usize - 2)); + scope.draw_text( + (scope.rect.width, 1), + "|\n".repeat(scope.rect.height as usize - 2), + ); + + scope.draw_text( + (0, scope.rect.height), + "-".repeat(scope.rect.width as usize), + ); + } +} diff --git a/src/main.rs b/src/main.rs index 5187e18..1048831 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,7 @@ use std::any::Any; use pulse_ui::{ - App, - layout::{LayoutItem, layout}, - state::State, - unit::Size, + App, layout::{LayoutItem, layout}, state::State, unit::Size, widget::outline::Outline, }; pub struct PulseTradeApp { @@ -27,6 +24,7 @@ impl App for PulseTradeApp { async fn render(&mut self, layout: pulse_ui::layout::Allocation) { layout.draw(1, "Yo"); + layout.draw_frame(0, Outline); } } From 62511d2101ba9341ab0ce1713e1222f826ddc04a Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Tue, 14 Jul 2026 22:17:41 +0200 Subject: [PATCH 03/11] Added Frame layout, and fix outline --- pulse-ui/src/layout.rs | 16 ++++++++++++++++ pulse-ui/src/widget/outline.rs | 2 +- src/main.rs | 19 +++++++++++++++---- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/pulse-ui/src/layout.rs b/pulse-ui/src/layout.rs index f30f424..eac5504 100644 --- a/pulse-ui/src/layout.rs +++ b/pulse-ui/src/layout.rs @@ -4,6 +4,7 @@ use crate::unit::{Rect, Size}; pub enum LayoutItem { Rows { unit: Size, items: Vec }, Columns { unit: Size, items: Vec }, + Frame { padding: u16, item: Box }, Widget(Size), } @@ -63,6 +64,20 @@ impl LayoutItem { } } + Self::Frame { padding, item } => { + frame.push(*alloc); + + let mut item_alloc = alloc.clone(); + + item_alloc.width -= padding * 2; + item_alloc.height -= padding * 2; + + item_alloc.x += padding; + item_alloc.y += padding; + + item.allocate(&item_alloc, frame, widgets); + } + Self::Widget(_) => { widgets.push(*alloc); } @@ -74,6 +89,7 @@ impl LayoutItem { Self::Rows { unit, .. } => unit, Self::Columns { unit, .. } => unit, Self::Widget(unit) => unit, + Self::Frame { item, .. } => item.get_size(), } } } diff --git a/pulse-ui/src/widget/outline.rs b/pulse-ui/src/widget/outline.rs index b2ab554..62daf82 100644 --- a/pulse-ui/src/widget/outline.rs +++ b/pulse-ui/src/widget/outline.rs @@ -13,7 +13,7 @@ impl Widget for Outline { ); scope.draw_text( - (0, scope.rect.height), + (0, scope.rect.height - 1), "-".repeat(scope.rect.width as usize), ); } diff --git a/src/main.rs b/src/main.rs index 1048831..ecec22c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,11 @@ use std::any::Any; use pulse_ui::{ - App, layout::{LayoutItem, layout}, state::State, unit::Size, widget::outline::Outline, + App, + layout::{LayoutItem, layout}, + state::State, + unit::Size, + widget::outline::Outline, }; pub struct PulseTradeApp { @@ -17,14 +21,21 @@ impl App for PulseTradeApp { async fn layout(&self) -> pulse_ui::layout::LayoutItem { layout(vec![ - LayoutItem::Widget(Size::Flex(1)), - LayoutItem::Widget(Size::Flex(1)), + LayoutItem::Frame { + padding: 1, + item: Box::new(LayoutItem::Widget(Size::Flex(1))), + }, + LayoutItem::Frame { + padding: 1, + item: Box::new(LayoutItem::Widget(Size::Flex(1))), + }, ]) } async fn render(&mut self, layout: pulse_ui::layout::Allocation) { layout.draw(1, "Yo"); - layout.draw_frame(0, Outline); + layout.draw_frame(1, Outline); + layout.draw_frame(2, Outline); } } From 08edc981ee75d05e4d258c097ef4ae79d7b23367 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Tue, 14 Jul 2026 22:19:43 +0200 Subject: [PATCH 04/11] Fix outline --- pulse-ui/src/layout.rs | 2 +- pulse-ui/src/widget/outline.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pulse-ui/src/layout.rs b/pulse-ui/src/layout.rs index eac5504..9125442 100644 --- a/pulse-ui/src/layout.rs +++ b/pulse-ui/src/layout.rs @@ -109,7 +109,7 @@ impl Rect { } pub fn layout(rows: Vec) -> LayoutItem { - LayoutItem::Rows { + LayoutItem::Columns { unit: Size::Percent(100), items: rows, } diff --git a/pulse-ui/src/widget/outline.rs b/pulse-ui/src/widget/outline.rs index 62daf82..2d28b27 100644 --- a/pulse-ui/src/widget/outline.rs +++ b/pulse-ui/src/widget/outline.rs @@ -8,7 +8,7 @@ impl Widget for Outline { scope.draw_text((0, 1), "|\n".repeat(scope.rect.height as usize - 2)); scope.draw_text( - (scope.rect.width, 1), + (scope.rect.width - 1, 1), "|\n".repeat(scope.rect.height as usize - 2), ); From b8162ebe3c02e4286694e106c8cc003fb55b5291 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Tue, 14 Jul 2026 22:29:45 +0200 Subject: [PATCH 05/11] Fix input events and drop spawning tokio --- pulse-ui/src/lib.rs | 8 +++++--- pulse-ui/src/state.rs | 4 +++- src/main.rs | 12 ++++++++---- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/pulse-ui/src/lib.rs b/pulse-ui/src/lib.rs index eafb645..5336b83 100644 --- a/pulse-ui/src/lib.rs +++ b/pulse-ui/src/lib.rs @@ -38,9 +38,11 @@ pub async fn run(create_app: impl FnOnce(&state::Context) -> A) { crossterm::terminal::enable_raw_mode().unwrap(); tokio::spawn(async move { - tx.send(Box::new(crossterm::event::read().unwrap())) - .await - .unwrap(); + loop { + tx.send(Box::new(crossterm::event::read().unwrap())) + .await + .unwrap(); + } }); let mut app = create_app(&ctx); diff --git a/pulse-ui/src/state.rs b/pulse-ui/src/state.rs index 28b7141..36020d1 100644 --- a/pulse-ui/src/state.rs +++ b/pulse-ui/src/state.rs @@ -59,7 +59,9 @@ impl<'a, T> std::ops::DerefMut for StateGuard<'a, T> { impl<'a, T> Drop for StateGuard<'a, T> { fn drop(&mut self) { if self.is_mutated { - self.tx.blocking_send(Box::new(Refresh)).unwrap() + let tx = self.tx.clone(); + + tokio::spawn(async move { tx.send(Box::new(Refresh)).await.unwrap() }); } } } diff --git a/src/main.rs b/src/main.rs index ecec22c..44d26c7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,9 @@ use std::any::Any; use pulse_ui::{ - App, + App, get_screen, layout::{LayoutItem, layout}, - state::State, + state::{Refresh, State}, unit::Size, widget::outline::Outline, }; @@ -16,7 +16,11 @@ impl App for PulseTradeApp { async fn init(&mut self, ctx: &pulse_ui::state::Context) {} async fn update(&mut self, ctx: &pulse_ui::state::Context, event: Box) { - ctx.close().await + if let Some(Refresh) = event.downcast_ref() { + return; + } + + *self.count.lock().await += 1; } async fn layout(&self) -> pulse_ui::layout::LayoutItem { @@ -33,7 +37,7 @@ impl App for PulseTradeApp { } async fn render(&mut self, layout: pulse_ui::layout::Allocation) { - layout.draw(1, "Yo"); + layout.draw(1, format!("{:?}", *self.count.lock().await).as_str()); layout.draw_frame(1, Outline); layout.draw_frame(2, Outline); } From 7be4040267bcc54c4d65e6c642f08fc5c585dfbf Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Tue, 14 Jul 2026 22:53:27 +0200 Subject: [PATCH 06/11] Display stuff --- pulse-ui/src/render.rs | 6 ------ pulse-ui/src/state.rs | 6 ++++++ pulse-ui/src/widget/mod.rs | 27 +++++++++++++++++++++++---- src/main.rs | 4 ++-- 4 files changed, 31 insertions(+), 12 deletions(-) diff --git a/pulse-ui/src/render.rs b/pulse-ui/src/render.rs index bde8aaf..b8ab8a1 100644 --- a/pulse-ui/src/render.rs +++ b/pulse-ui/src/render.rs @@ -8,7 +8,6 @@ use crate::{ pub enum Instr { DrawText(Point, String), - DrawOutline(Rect), } pub struct RenderScope { @@ -21,10 +20,6 @@ impl RenderScope { self.draw_instructions .push(Instr::DrawText(at.into(), text.to_string())); } - - pub fn draw_outline>(&mut self, rect: Rect) { - self.draw_instructions.push(Instr::DrawOutline(rect)); - } } impl RenderScope { @@ -87,7 +82,6 @@ impl RenderScope { ); } } - Instr::DrawOutline(rect) => {} } } } diff --git a/pulse-ui/src/state.rs b/pulse-ui/src/state.rs index 36020d1..7ff8e6b 100644 --- a/pulse-ui/src/state.rs +++ b/pulse-ui/src/state.rs @@ -40,6 +40,12 @@ impl State { } } +impl State { + pub async fn display(&self) -> String { + self.lock().await.to_string() + } +} + impl<'a, T> std::ops::Deref for StateGuard<'a, T> { type Target = T; diff --git a/pulse-ui/src/widget/mod.rs b/pulse-ui/src/widget/mod.rs index f11b45f..aa8e40b 100644 --- a/pulse-ui/src/widget/mod.rs +++ b/pulse-ui/src/widget/mod.rs @@ -6,8 +6,27 @@ pub trait Widget { fn render(&self, scope: &mut RenderScope); } -impl Widget for &str { - fn render(&self, scope: &mut RenderScope) { - scope.draw_text(0, self); - } +macro_rules! display_widget { + ($t:ty) => { + impl Widget for $t { + fn render(&self, scope: &mut RenderScope) { + scope.draw_text(0, self); + } + } + }; } + +display_widget!(&str); +display_widget!(String); +display_widget!(i8); +display_widget!(i16); +display_widget!(i32); +display_widget!(i64); +display_widget!(i128); +display_widget!(u8); +display_widget!(u16); +display_widget!(u32); +display_widget!(u64); +display_widget!(u128); +display_widget!(f32); +display_widget!(f64); diff --git a/src/main.rs b/src/main.rs index 44d26c7..0428109 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ use std::any::Any; use pulse_ui::{ - App, get_screen, + App, layout::{LayoutItem, layout}, state::{Refresh, State}, unit::Size, @@ -37,7 +37,7 @@ impl App for PulseTradeApp { } async fn render(&mut self, layout: pulse_ui::layout::Allocation) { - layout.draw(1, format!("{:?}", *self.count.lock().await).as_str()); + layout.draw(1, format!("{}", self.count.display().await).as_str()); layout.draw_frame(1, Outline); layout.draw_frame(2, Outline); } From b5470a9f05356f3192bbbb97cb43b59f779cc70d Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Wed, 15 Jul 2026 00:02:13 +0200 Subject: [PATCH 07/11] Allocation layout and spaced items --- pulse-ui/src/layout.rs | 42 ++++++++++++++++++++++++------------------ pulse-ui/src/render.rs | 4 ++-- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/pulse-ui/src/layout.rs b/pulse-ui/src/layout.rs index 9125442..cf79b15 100644 --- a/pulse-ui/src/layout.rs +++ b/pulse-ui/src/layout.rs @@ -3,22 +3,29 @@ use crate::unit::{Rect, Size}; #[derive(Debug, Clone)] pub enum LayoutItem { Rows { unit: Size, items: Vec }, + SpacedRows { unit: Size, items: Vec }, + Columns { unit: Size, items: Vec }, + SpacedColumns { unit: Size, items: Vec }, + Frame { padding: u16, item: Box }, Widget(Size), } #[derive(Debug, Clone)] pub struct Allocation { - pub widget_alloc: Vec, - pub frame_alloc: Vec, + pub widgets: Vec, + pub frame: Vec, } impl LayoutItem { - pub fn allocate(&self, alloc: &Rect, frame: &mut Vec, widgets: &mut Vec) { + pub fn allocate(&self, alloc: &Rect, full_alloc: &mut Allocation) { match self { - Self::Rows { items, .. } | Self::Columns { items, .. } => { - frame.push(*alloc); + Self::Rows { items, .. } + | Self::Columns { items, .. } + | Self::SpacedRows { items, .. } + | Self::SpacedColumns { items, .. } => { + full_alloc.frame.push(*alloc); let is_row = matches!(self, Self::Rows { .. }); @@ -60,12 +67,12 @@ impl LayoutItem { curr += unit_alloc; - item.allocate(&item_alloc, frame, widgets); + item.allocate(&item_alloc, full_alloc); } } Self::Frame { padding, item } => { - frame.push(*alloc); + full_alloc.frame.push(*alloc); let mut item_alloc = alloc.clone(); @@ -75,19 +82,19 @@ impl LayoutItem { item_alloc.x += padding; item_alloc.y += padding; - item.allocate(&item_alloc, frame, widgets); + item.allocate(&item_alloc, full_alloc); } Self::Widget(_) => { - widgets.push(*alloc); + full_alloc.widgets.push(*alloc); } } } pub fn get_size<'a>(&'a self) -> &'a Size { match self { - Self::Rows { unit, .. } => unit, - Self::Columns { unit, .. } => unit, + Self::Rows { unit, .. } | Self::SpacedRows { unit, .. } => unit, + Self::Columns { unit, .. } | Self::SpacedColumns { unit, .. } => unit, Self::Widget(unit) => unit, Self::Frame { item, .. } => item.get_size(), } @@ -96,15 +103,14 @@ impl LayoutItem { impl Rect { pub fn allocate(&self, layout: &LayoutItem) -> Allocation { - let mut widget_alloc = Vec::new(); - let mut frame_alloc = Vec::new(); + let mut alloc = Allocation { + widgets: Vec::new(), + frame: Vec::new(), + }; - layout.allocate(self, &mut frame_alloc, &mut widget_alloc); + layout.allocate(self, &mut alloc); - Allocation { - widget_alloc, - frame_alloc, - } + alloc } } diff --git a/pulse-ui/src/render.rs b/pulse-ui/src/render.rs index b8ab8a1..bf8940d 100644 --- a/pulse-ui/src/render.rs +++ b/pulse-ui/src/render.rs @@ -57,13 +57,13 @@ impl From for Point { impl Allocation { pub fn draw(&self, index: usize, widget: W) { - let mut scope = RenderScope::new(self.widget_alloc[index]); + let mut scope = RenderScope::new(self.widgets[index]); widget.render(&mut scope); scope.draw(); } pub fn draw_frame(&self, index: usize, widget: W) { - let mut scope = RenderScope::new(self.frame_alloc[index]); + let mut scope = RenderScope::new(self.frame[index]); widget.render(&mut scope); scope.draw(); } From 5820fce49f9350c2c4153cefd7fd2cf80f021b78 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Wed, 15 Jul 2026 00:39:35 +0200 Subject: [PATCH 08/11] Dynamic spaced rows --- pulse-ui/src/layout.rs | 12 +++------ pulse-ui/src/widget/mod.rs | 7 +++++ pulse-ui/src/widget/spaced.rs | 49 +++++++++++++++++++++++++++++++++++ src/main.rs | 24 +++++++---------- 4 files changed, 69 insertions(+), 23 deletions(-) create mode 100644 pulse-ui/src/widget/spaced.rs diff --git a/pulse-ui/src/layout.rs b/pulse-ui/src/layout.rs index cf79b15..4e94d14 100644 --- a/pulse-ui/src/layout.rs +++ b/pulse-ui/src/layout.rs @@ -3,10 +3,7 @@ use crate::unit::{Rect, Size}; #[derive(Debug, Clone)] pub enum LayoutItem { Rows { unit: Size, items: Vec }, - SpacedRows { unit: Size, items: Vec }, - Columns { unit: Size, items: Vec }, - SpacedColumns { unit: Size, items: Vec }, Frame { padding: u16, item: Box }, Widget(Size), @@ -21,10 +18,7 @@ pub struct Allocation { impl LayoutItem { pub fn allocate(&self, alloc: &Rect, full_alloc: &mut Allocation) { match self { - Self::Rows { items, .. } - | Self::Columns { items, .. } - | Self::SpacedRows { items, .. } - | Self::SpacedColumns { items, .. } => { + Self::Rows { items, .. } | Self::Columns { items, .. } => { full_alloc.frame.push(*alloc); let is_row = matches!(self, Self::Rows { .. }); @@ -93,8 +87,8 @@ impl LayoutItem { pub fn get_size<'a>(&'a self) -> &'a Size { match self { - Self::Rows { unit, .. } | Self::SpacedRows { unit, .. } => unit, - Self::Columns { unit, .. } | Self::SpacedColumns { unit, .. } => unit, + Self::Rows { unit, .. } => unit, + Self::Columns { unit, .. } => unit, Self::Widget(unit) => unit, Self::Frame { item, .. } => item.get_size(), } diff --git a/pulse-ui/src/widget/mod.rs b/pulse-ui/src/widget/mod.rs index aa8e40b..819165e 100644 --- a/pulse-ui/src/widget/mod.rs +++ b/pulse-ui/src/widget/mod.rs @@ -1,4 +1,5 @@ pub mod outline; +pub mod spaced; use crate::render::RenderScope; @@ -30,3 +31,9 @@ display_widget!(u64); display_widget!(u128); display_widget!(f32); display_widget!(f64); + +impl Widget for &dyn Widget { + fn render(&self, scope: &mut RenderScope) { + (*self).render(scope); + } +} diff --git a/pulse-ui/src/widget/spaced.rs b/pulse-ui/src/widget/spaced.rs new file mode 100644 index 0000000..e1b336d --- /dev/null +++ b/pulse-ui/src/widget/spaced.rs @@ -0,0 +1,49 @@ +use crate::{layout::LayoutItem, unit::Size::Percent, widget::Widget}; + +pub struct SpacedRows(pub Vec<(LayoutItem, Box)>); + +impl Widget for SpacedRows { + fn render(&self, scope: &mut crate::render::RenderScope) { + let mut alloc = scope.rect.allocate(&crate::layout::LayoutItem::Rows { + unit: Percent(100), + items: self.0.iter().map(|v| v.0.clone()).collect(), + }); + + for i in 0..alloc.widgets.len() { + let item = &self.0[i]; + + if i > 0 { + let rect = &mut alloc.widgets[i]; + scope.draw_text((0, rect.y - 1), "─".repeat(rect.width as usize)); + } + + alloc.draw(i, &*item.1); + } + } +} + +pub struct SpacedColumns(pub Vec<(LayoutItem, Box)>); + +impl Widget for SpacedColumns { + fn render(&self, scope: &mut crate::render::RenderScope) { + let mut alloc = scope.rect.allocate(&crate::layout::LayoutItem::Columns { + unit: Percent(100), + items: self.0.iter().map(|v| v.0.clone()).collect(), + }); + + for i in 0..alloc.widgets.len() { + let item = &self.0[i]; + + if i > 0 { + let rect = &mut alloc.widgets[i]; + + for i in 0..rect.height { + scope.draw_text((rect.x - 1, i), "│"); + } + rect.x += 1; + } + + alloc.draw(i, &*item.1); + } + } +} diff --git a/src/main.rs b/src/main.rs index 0428109..6e51fcb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,7 @@ use pulse_ui::{ layout::{LayoutItem, layout}, state::{Refresh, State}, unit::Size, - widget::outline::Outline, + widget::spaced::SpacedRows, }; pub struct PulseTradeApp { @@ -24,22 +24,18 @@ impl App for PulseTradeApp { } async fn layout(&self) -> pulse_ui::layout::LayoutItem { - layout(vec![ - LayoutItem::Frame { - padding: 1, - item: Box::new(LayoutItem::Widget(Size::Flex(1))), - }, - LayoutItem::Frame { - padding: 1, - item: Box::new(LayoutItem::Widget(Size::Flex(1))), - }, - ]) + layout(vec![LayoutItem::Widget(Size::Flex(1))]) } async fn render(&mut self, layout: pulse_ui::layout::Allocation) { - layout.draw(1, format!("{}", self.count.display().await).as_str()); - layout.draw_frame(1, Outline); - layout.draw_frame(2, Outline); + layout.draw( + 0, + SpacedRows(vec![ + (LayoutItem::Widget(Size::Flex(1)), Box::new("one")), + (LayoutItem::Widget(Size::Flex(1)), Box::new("two")), + (LayoutItem::Widget(Size::Flex(1)), Box::new("three")), + ]), + ); } } From 96656140d102a97f8406f39a9f81382798d028b9 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Wed, 15 Jul 2026 01:00:33 +0200 Subject: [PATCH 09/11] Fixed outline --- pulse-ui/src/widget/outline.rs | 13 +++++++++---- src/main.rs | 9 +++++++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/pulse-ui/src/widget/outline.rs b/pulse-ui/src/widget/outline.rs index 2d28b27..8ff1e8c 100644 --- a/pulse-ui/src/widget/outline.rs +++ b/pulse-ui/src/widget/outline.rs @@ -4,17 +4,22 @@ pub struct Outline; impl Widget for Outline { fn render(&self, scope: &mut crate::render::RenderScope) { - scope.draw_text(0, "-".repeat(scope.rect.width as usize)); + scope.draw_text(0, "─".repeat(scope.rect.width as usize)); - scope.draw_text((0, 1), "|\n".repeat(scope.rect.height as usize - 2)); + scope.draw_text((0, 1), "│\n".repeat(scope.rect.height as usize - 2)); scope.draw_text( (scope.rect.width - 1, 1), - "|\n".repeat(scope.rect.height as usize - 2), + "│\n".repeat(scope.rect.height as usize - 2), ); scope.draw_text( (0, scope.rect.height - 1), - "-".repeat(scope.rect.width as usize), + "─".repeat(scope.rect.width as usize), ); + + scope.draw_text(0, "┌"); + scope.draw_text((scope.rect.width - 1, 0), "┐"); + scope.draw_text((0, scope.rect.height - 1), "└"); + scope.draw_text((scope.rect.width - 1, scope.rect.height - 1), "┘"); } } diff --git a/src/main.rs b/src/main.rs index 6e51fcb..4574386 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,7 @@ use pulse_ui::{ layout::{LayoutItem, layout}, state::{Refresh, State}, unit::Size, - widget::spaced::SpacedRows, + widget::{outline::Outline, spaced::SpacedRows}, }; pub struct PulseTradeApp { @@ -24,10 +24,15 @@ impl App for PulseTradeApp { } async fn layout(&self) -> pulse_ui::layout::LayoutItem { - layout(vec![LayoutItem::Widget(Size::Flex(1))]) + layout(vec![LayoutItem::Frame { + padding: 1, + item: Box::new(LayoutItem::Widget(Size::Flex(1))), + }]) } async fn render(&mut self, layout: pulse_ui::layout::Allocation) { + // layout.draw_frame(0, Outline); + layout.draw( 0, SpacedRows(vec![ From 5b43f089054b5deb7b774cd60a1e0af00011be28 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Wed, 15 Jul 2026 01:27:51 +0200 Subject: [PATCH 10/11] Fix disappearing items in rows --- pulse-ui/src/widget/spaced.rs | 7 ++++--- src/main.rs | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pulse-ui/src/widget/spaced.rs b/pulse-ui/src/widget/spaced.rs index e1b336d..23cec09 100644 --- a/pulse-ui/src/widget/spaced.rs +++ b/pulse-ui/src/widget/spaced.rs @@ -4,7 +4,7 @@ pub struct SpacedRows(pub Vec<(LayoutItem, Box)>); impl Widget for SpacedRows { fn render(&self, scope: &mut crate::render::RenderScope) { - let mut alloc = scope.rect.allocate(&crate::layout::LayoutItem::Rows { + let alloc = scope.rect.allocate(&crate::layout::LayoutItem::Rows { unit: Percent(100), items: self.0.iter().map(|v| v.0.clone()).collect(), }); @@ -13,8 +13,9 @@ impl Widget for SpacedRows { let item = &self.0[i]; if i > 0 { - let rect = &mut alloc.widgets[i]; - scope.draw_text((0, rect.y - 1), "─".repeat(rect.width as usize)); + let rect = &alloc.widgets[i]; + + scope.draw_text((0, rect.y - 2), "─".repeat(rect.width as usize)); } alloc.draw(i, &*item.1); diff --git a/src/main.rs b/src/main.rs index 4574386..493fbb6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,7 +31,7 @@ impl App for PulseTradeApp { } async fn render(&mut self, layout: pulse_ui::layout::Allocation) { - // layout.draw_frame(0, Outline); + layout.draw_frame(0, Outline); layout.draw( 0, From 98665f713cb3095ce9c09f780cf5733f447db9bc Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Wed, 15 Jul 2026 01:42:24 +0200 Subject: [PATCH 11/11] Added count --- src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.rs b/src/main.rs index 493fbb6..2af3330 100644 --- a/src/main.rs +++ b/src/main.rs @@ -39,6 +39,7 @@ impl App for PulseTradeApp { (LayoutItem::Widget(Size::Flex(1)), Box::new("one")), (LayoutItem::Widget(Size::Flex(1)), Box::new("two")), (LayoutItem::Widget(Size::Flex(1)), Box::new("three")), + (LayoutItem::Widget(Size::Flex(1)), Box::new(self.count.display().await)), ]), ); }