diff --git a/pulse-ui/src/layout.rs b/pulse-ui/src/layout.rs index f30f424..4e94d14 100644 --- a/pulse-ui/src/layout.rs +++ b/pulse-ui/src/layout.rs @@ -4,20 +4,22 @@ 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), } #[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); + full_alloc.frame.push(*alloc); let is_row = matches!(self, Self::Rows { .. }); @@ -59,12 +61,26 @@ impl LayoutItem { curr += unit_alloc; - item.allocate(&item_alloc, frame, widgets); + item.allocate(&item_alloc, full_alloc); } } + Self::Frame { padding, item } => { + full_alloc.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, full_alloc); + } + Self::Widget(_) => { - widgets.push(*alloc); + full_alloc.widgets.push(*alloc); } } } @@ -74,26 +90,26 @@ impl LayoutItem { Self::Rows { unit, .. } => unit, Self::Columns { unit, .. } => unit, Self::Widget(unit) => unit, + Self::Frame { item, .. } => item.get_size(), } } } 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 } } pub fn layout(rows: Vec) -> LayoutItem { - LayoutItem::Rows { + LayoutItem::Columns { unit: Size::Percent(100), items: rows, } diff --git a/pulse-ui/src/lib.rs b/pulse-ui/src/lib.rs index fa13f10..5336b83 100644 --- a/pulse-ui/src/lib.rs +++ b/pulse-ui/src/lib.rs @@ -11,48 +11,59 @@ 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(); self.render(get_screen().allocate(&self.layout().await)) .await; + + crossterm::execute!(std::io::stdout()).unwrap(); } } 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 { + loop { + 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 +76,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/render.rs b/pulse-ui/src/render.rs index 8e458be..bf8940d 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 { @@ -62,7 +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[index]); widget.render(&mut scope); scope.draw(); } @@ -81,7 +82,6 @@ impl RenderScope { ); } } - Instr::DrawOutline(rect) => {} } } } @@ -89,5 +89,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/state.rs b/pulse-ui/src/state.rs index 279386c..7ff8e6b 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(), @@ -39,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; @@ -58,7 +65,15 @@ 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() }); } } } + +impl Context { + pub async fn close(&self) { + self.tx.send(Box::new(Close)).await.unwrap() + } +} diff --git a/pulse-ui/src/widget/mod.rs b/pulse-ui/src/widget/mod.rs index 8d37a34..819165e 100644 --- a/pulse-ui/src/widget/mod.rs +++ b/pulse-ui/src/widget/mod.rs @@ -1,11 +1,39 @@ +pub mod outline; +pub mod spaced; + use crate::render::RenderScope; pub trait Widget { fn render(&self, scope: &mut RenderScope); } -impl Widget for &str { +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); + +impl Widget for &dyn Widget { fn render(&self, scope: &mut RenderScope) { - scope.draw_text(0, self); + (*self).render(scope); } } diff --git a/pulse-ui/src/widget/outline.rs b/pulse-ui/src/widget/outline.rs new file mode 100644 index 0000000..8ff1e8c --- /dev/null +++ b/pulse-ui/src/widget/outline.rs @@ -0,0 +1,25 @@ +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, 1), + "│\n".repeat(scope.rect.height as usize - 2), + ); + + scope.draw_text( + (0, scope.rect.height - 1), + "─".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/pulse-ui/src/widget/spaced.rs b/pulse-ui/src/widget/spaced.rs new file mode 100644 index 0000000..23cec09 --- /dev/null +++ b/pulse-ui/src/widget/spaced.rs @@ -0,0 +1,50 @@ +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 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 = &alloc.widgets[i]; + + scope.draw_text((0, rect.y - 2), "─".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 74f9a7f..2af3330 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,11 @@ +use std::any::Any; + use pulse_ui::{ App, layout::{LayoutItem, layout}, - state::State, + state::{Refresh, State}, unit::Size, + widget::{outline::Outline, spaced::SpacedRows}, }; pub struct PulseTradeApp { @@ -10,19 +13,35 @@ 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) { + if let Some(Refresh) = event.downcast_ref() { + return; + } + + *self.count.lock().await += 1; + } async fn layout(&self) -> pulse_ui::layout::LayoutItem { - layout(vec![ - LayoutItem::Widget(Size::Flex(1)), - 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(1, "Yo"); + layout.draw_frame(0, 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")), + (LayoutItem::Widget(Size::Flex(1)), Box::new(self.count.display().await)), + ]), + ); } }