From e4773db9d83037f3feb6d1b34db05b927ec363ac Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Thu, 23 Jul 2026 00:22:20 +0200 Subject: [PATCH] Scrolling --- pulse-ui/src/widget/mod.rs | 5 +-- pulse-ui/src/widget/scroll.rs | 67 +++++++++++++++++++++++++++++++++++ src/terminal/main.rs | 60 ++++++++++++++++++------------- 3 files changed, 105 insertions(+), 27 deletions(-) create mode 100644 pulse-ui/src/widget/scroll.rs diff --git a/pulse-ui/src/widget/mod.rs b/pulse-ui/src/widget/mod.rs index dc75b5f..b5cc038 100644 --- a/pulse-ui/src/widget/mod.rs +++ b/pulse-ui/src/widget/mod.rs @@ -1,8 +1,9 @@ pub mod align; -pub mod outline; -pub mod spaced; pub mod dynamic; pub mod input; +pub mod outline; +pub mod scroll; +pub mod spaced; use crate::render::RenderScope; diff --git a/pulse-ui/src/widget/scroll.rs b/pulse-ui/src/widget/scroll.rs new file mode 100644 index 0000000..ef93d34 --- /dev/null +++ b/pulse-ui/src/widget/scroll.rs @@ -0,0 +1,67 @@ +use crate::widget::Widget; + +pub struct ScrollState(pub usize, pub [usize; N]); + +pub struct ScrollText { + pub scroll: usize, + pub title: String, + pub text: String, +} + +impl Widget for ScrollText { + fn render(&self, scope: &mut crate::render::RenderScope) { + scope.draw_text(0, &self.title); + + let title_lines = self.title.lines().count(); + + for (y, line) in self + .text + .lines() + .skip(self.scroll) + .take(scope.rect.height as usize - title_lines) + .enumerate() + { + scope.draw_text((0, (y + title_lines) as u16), line); + } + } +} + +impl ScrollState { + pub fn get_selected(&self, index: usize) -> &'static str { + if index == self.0 { "\x1b[32m" } else { "" } + } + + pub fn scroll(&self, index: usize, title: String, text: String) -> ScrollText { + ScrollText { + scroll: self.1[index], + title, + text, + } + } + + pub fn up(&mut self) { + if self.1[self.0] > 0 { + self.1[self.0] -= 1; + } + } + + pub fn down(&mut self) { + self.1[self.0] += 1; + } + + pub fn back_tab(&mut self) { + if self.0 > 0 { + self.0 -= 1; + } else { + self.0 = N - 1; + } + } + + pub fn tab(&mut self) { + if self.0 + 1 < N { + self.0 += 1; + } else { + self.0 = 0; + } + } +} diff --git a/src/terminal/main.rs b/src/terminal/main.rs index 135bff1..848bf9f 100644 --- a/src/terminal/main.rs +++ b/src/terminal/main.rs @@ -14,6 +14,7 @@ use pulse_ui::{ align::End, input::{Input, InputState}, outline::{Outline, VLine}, + scroll::{ScrollState, ScrollText}, spaced::SpacedColumns, }, }; @@ -26,6 +27,7 @@ use pulse_wire::terminal::{ pub struct PulseTradeApp { command: State, + scroll: State>, watch_list: State>, active_positions: State>, logs: State>, @@ -149,6 +151,14 @@ impl App for PulseTradeApp { drop(command); self.execute_command(ctx, command_text.trim()).await; + } else if key.code.is_up() { + self.scroll.value.lock().await.up(); + } else if key.code.is_down() { + self.scroll.value.lock().await.down(); + } else if key.code.is_back_tab() { + self.scroll.value.lock().await.back_tab(); + } else if key.code.is_tab() { + self.scroll.value.lock().await.tab(); } } } @@ -207,18 +217,14 @@ impl App for PulseTradeApp { SpacedColumns(vec![ ( LayoutItem::Widget(Size::Flex(1)), - Box::new(format!( - " WATCHLIST\n{}", - apply_padding(self.watch_list.lock().await.get_formatted()).join("\n") - )), + Box::new(advanced_draw(&self.scroll, 0, "WATCH LIST", &self.watch_list).await), ), ( LayoutItem::Widget(Size::Flex(1)), - Box::new(format!( - " ACTIVE POSITIONS\n{}", - apply_padding(self.active_positions.lock().await.get_formatted()) - .join("\n") - )), + Box::new( + advanced_draw(&self.scroll, 1, "ACTIVE POSITIONS", &self.active_positions) + .await, + ), ), (LayoutItem::Widget(Size::Flex(1)), { let mo = self.market_overview.lock().await; @@ -236,34 +242,22 @@ impl App for PulseTradeApp { SpacedColumns(vec![ ( LayoutItem::Widget(Size::Flex(1)), - Box::new(format!( - " SIGNALS\n{}", - apply_padding(self.signals.lock().await.get_formatted()).join("\n") - )), + Box::new(advanced_draw(&self.scroll, 3, "SIGNALS", &self.signals).await), ), ( LayoutItem::Widget(Size::Flex(1)), - Box::new(format!( - " INSPECTOR\n{}", - apply_padding(self.inspect.lock().await.get_formatted()).join("\n") - )), + Box::new(advanced_draw(&self.scroll, 4, "INSPECTOR", &self.inspect).await), ), ( LayoutItem::Widget(Size::Flex(1)), - Box::new(format!( - " STATUS\n{}", - apply_padding(self.status.lock().await.get_formatted()).join("\n") - )), + Box::new(advanced_draw(&self.scroll, 5, "STATUS", &self.status).await), ), ]), ); layout.draw( 5, - format!( - " EVENT LOGS\n{}", - apply_padding(self.logs.lock().await.get_formatted()).join("\n") - ), + advanced_draw(&self.scroll, 6, "EVENT LOGS", &self.logs).await, ); layout.draw(6, Input(" > ", &*self.command.lock().await)); @@ -283,6 +277,7 @@ async fn main() -> tokio::io::Result<()> { pulse_ui::run(|ctx| { client.use_app(PulseTradeApp { command: ctx.use_state(InputState::new()), + scroll: ctx.use_state(ScrollState(0, [0; 7])), watch_list: ctx.use_state(Vec::new()), active_positions: ctx.use_state(Vec::new()), signals: ctx.use_state(Vec::new()), @@ -306,3 +301,18 @@ async fn main() -> tokio::io::Result<()> { Ok(()) } + +pub async fn advanced_draw( + scroll: &State>, + index: usize, + title: &'static str, + state: &State, +) -> ScrollText { + let scroll = scroll.lock().await; + + scroll.scroll( + index, + format!("{}{title}\x1b[0m", scroll.get_selected(index)), + apply_padding(state.lock().await.get_formatted()).join("\n"), + ) +}