Fix scroll: PgUp/PgDn move cursor in place, add Scrollbar widget
SectionTree.handle_nav() now takes viewport height: - PgUp/PgDn move both cursor and viewport by one page, keeping the cursor at the same screen position - Home/End jump to first/last item - scroll_to_selected() uses actual viewport height instead of hardcoded 30 Added render_scrollable() in widgets.rs: renders a Paragraph with a vertical Scrollbar when content exceeds the viewport. Used by the conscious and subconscious screens. Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
parent
818cdcc4e5
commit
19bb6d02e3
3 changed files with 74 additions and 39 deletions
|
|
@ -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<tokio::sync::Mutex<crate::agent::Agent>>,
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Line<'_>>,
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue