diff --git a/src/user/context.rs b/src/user/context.rs index a0203b2..96af080 100644 --- a/src/user/context.rs +++ b/src/user/context.rs @@ -7,12 +7,11 @@ use ratatui::{ layout::Rect, style::{Color, Style}, text::Line, - widgets::{Paragraph, Wrap}, Frame, }; use super::{App, ScreenView, screen_legend}; -use super::widgets::{SectionTree, pane_block}; +use super::widgets::{SectionTree, pane_block, render_scrollable}; pub(crate) struct ConsciousScreen { agent: std::sync::Arc>, @@ -41,7 +40,7 @@ impl ScreenView for ConsciousScreen { if let ratatui::crossterm::event::Event::Key(key) = event { if key.kind != ratatui::crossterm::event::KeyEventKind::Press { continue; } let context_state = self.read_context_state(); - self.tree.handle_nav(key.code, &context_state); + self.tree.handle_nav(key.code, &context_state, area.height); } } @@ -97,11 +96,6 @@ impl ScreenView for ConsciousScreen { let block = pane_block("context") .title_top(Line::from(screen_legend()).left_aligned()); - let para = Paragraph::new(lines) - .block(block) - .wrap(Wrap { trim: false }) - .scroll((self.tree.scroll, 0)); - - frame.render_widget(para, area); + render_scrollable(frame, area, lines, block, self.tree.scroll); } } diff --git a/src/user/subconscious.rs b/src/user/subconscious.rs index dc4039a..8734589 100644 --- a/src/user/subconscious.rs +++ b/src/user/subconscious.rs @@ -9,13 +9,13 @@ use ratatui::{ layout::{Constraint, Layout, Rect}, style::{Color, Modifier, Style}, text::{Line, Span}, - widgets::{List, ListItem, ListState, Paragraph, Wrap}, + widgets::{List, ListItem, ListState}, Frame, crossterm::event::KeyCode, }; use super::{App, ScreenView, screen_legend}; -use super::widgets::{SectionTree, pane_block_focused, format_age, format_ts_age}; +use super::widgets::{SectionTree, pane_block_focused, render_scrollable, format_age, format_ts_age}; use crate::agent::context::ContextSection; #[derive(Clone, Copy, PartialEq)] @@ -82,7 +82,7 @@ impl ScreenView for SubconsciousScreen { } _ => {} } - Pane::Outputs => self.output_tree.handle_nav(code, &output_sections), + Pane::Outputs => self.output_tree.handle_nav(code, &output_sections, area.height), Pane::History => match code { KeyCode::Up => self.history_scroll = self.history_scroll.saturating_sub(3), KeyCode::Down => self.history_scroll += 3, @@ -90,7 +90,7 @@ impl ScreenView for SubconsciousScreen { KeyCode::PageDown => self.history_scroll += 20, _ => {} } - Pane::Context => self.context_tree.handle_nav(code, &context_sections), + Pane::Context => self.context_tree.handle_nav(code, &context_sections, area.height), } } } @@ -207,11 +207,9 @@ impl SubconsciousScreen { self.output_tree.render_sections(§ions, &mut lines); } - let para = Paragraph::new(lines) - .block(pane_block_focused("state", self.focus == Pane::Outputs)) - .wrap(Wrap { trim: false }) - .scroll((self.output_tree.scroll, 0)); - frame.render_widget(para, area); + render_scrollable(frame, area, lines, + pane_block_focused("state", self.focus == Pane::Outputs), + self.output_tree.scroll); } fn draw_history(&self, frame: &mut Frame, area: Rect, app: &App) { @@ -248,11 +246,9 @@ impl SubconsciousScreen { } } - let para = Paragraph::new(lines) - .block(pane_block_focused(&title, self.focus == Pane::History)) - .wrap(Wrap { trim: false }) - .scroll((self.history_scroll, 0)); - frame.render_widget(para, area); + render_scrollable(frame, area, lines, + pane_block_focused(&title, self.focus == Pane::History), + self.history_scroll); } fn draw_context( @@ -277,10 +273,8 @@ impl SubconsciousScreen { .map(|s| s.name.as_str()) .unwrap_or("—"); - let para = Paragraph::new(lines) - .block(pane_block_focused(title, self.focus == Pane::Context)) - .wrap(Wrap { trim: false }) - .scroll((self.context_tree.scroll, 0)); - frame.render_widget(para, area); + render_scrollable(frame, area, lines, + pane_block_focused(title, self.focus == Pane::Context), + self.context_tree.scroll); } } diff --git a/src/user/widgets.rs b/src/user/widgets.rs index 4b52918..7e10d90 100644 --- a/src/user/widgets.rs +++ b/src/user/widgets.rs @@ -1,9 +1,11 @@ // widgets.rs — Shared TUI helpers and reusable components use ratatui::{ + layout::{Margin, Rect}, style::{Color, Modifier, Style}, text::Line, - widgets::{Block, Borders}, + widgets::{Block, Borders, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, Wrap}, + Frame, crossterm::event::KeyCode, }; use crate::agent::context::ContextSection; @@ -46,6 +48,33 @@ pub fn format_ts_age(ts: i64) -> String { format_age((now - ts).max(0) as f64) } +/// Render a paragraph with a vertical scrollbar. +pub fn render_scrollable( + frame: &mut Frame, + area: Rect, + lines: Vec>, + block: Block<'_>, + scroll: u16, +) { + let content_len = lines.len(); + let para = Paragraph::new(lines) + .block(block) + .wrap(Wrap { trim: false }) + .scroll((scroll, 0)); + frame.render_widget(para, area); + + let visible = area.height.saturating_sub(2) as usize; + if content_len > visible { + let mut sb_state = ScrollbarState::new(content_len) + .position(scroll as usize); + frame.render_stateful_widget( + Scrollbar::new(ScrollbarOrientation::VerticalRight), + area.inner(Margin { vertical: 1, horizontal: 0 }), + &mut sb_state, + ); + } +} + // --------------------------------------------------------------------------- // SectionTree — expand/collapse tree renderer for ContextSection // --------------------------------------------------------------------------- @@ -77,17 +106,35 @@ impl SectionTree { sections.iter().map(|s| count(s, &self.expanded, &mut idx)).sum() } - pub fn handle_nav(&mut self, code: KeyCode, sections: &[ContextSection]) { + pub fn handle_nav(&mut self, code: KeyCode, sections: &[ContextSection], height: u16) { let item_count = self.item_count(sections); + let page = height.saturating_sub(2) as usize; // account for border match code { KeyCode::Up => { self.selected = Some(self.selected.unwrap_or(0).saturating_sub(1)); - self.scroll_to_selected(); } KeyCode::Down => { let max = item_count.saturating_sub(1); self.selected = Some(self.selected.map_or(0, |s| (s + 1).min(max))); - self.scroll_to_selected(); + } + KeyCode::PageUp => { + let sel = self.selected.unwrap_or(0); + self.selected = Some(sel.saturating_sub(page)); + self.scroll = self.scroll.saturating_sub(page as u16); + return; // skip scroll_to_selected — we moved both together + } + KeyCode::PageDown => { + let max = item_count.saturating_sub(1); + let sel = self.selected.map_or(0, |s| (s + page).min(max)); + self.selected = Some(sel); + self.scroll += page as u16; + return; + } + KeyCode::Home => { + self.selected = Some(0); + } + KeyCode::End => { + self.selected = Some(item_count.saturating_sub(1)); } KeyCode::Right | KeyCode::Enter => { if let Some(sel) = self.selected { @@ -99,19 +146,19 @@ impl SectionTree { self.expanded.remove(&sel); } } - KeyCode::PageUp => { self.scroll = self.scroll.saturating_sub(20); } - KeyCode::PageDown => { self.scroll += 20; } _ => {} } + self.scroll_to_selected(height); } - fn scroll_to_selected(&mut self) { + fn scroll_to_selected(&mut self, height: u16) { if let Some(sel) = self.selected { let sel_line = sel as u16; - if sel_line < self.scroll + 2 { - self.scroll = sel_line.saturating_sub(2); - } else if sel_line > self.scroll + 30 { - self.scroll = sel_line.saturating_sub(15); + let visible = height.saturating_sub(2); // border + if sel_line < self.scroll { + self.scroll = sel_line; + } else if sel_line >= self.scroll + visible { + self.scroll = sel_line.saturating_sub(visible.saturating_sub(1)); } } }