From 3c47dd2c4802cfe959a628ea55c17a61832a57b1 Mon Sep 17 00:00:00 2001 From: lancebord Date: Sat, 7 Mar 2026 21:50:12 -0500 Subject: added chat scrolling --- src/main.rs | 4 +++- src/tui/app.rs | 13 +++++++++++++ src/tui/ui.rs | 22 ++++++++++++++-------- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/src/main.rs b/src/main.rs index 9c3d631..23a44f8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -70,7 +70,7 @@ async fn run( // neither blocks the other. loop { // Draw - terminal.draw(|f| ui::draw(f, &app))?; + terminal.draw(|f| ui::draw(f, &mut app))?; // Poll crossterm for keyboard input (non-blocking, 20ms timeout) if event::poll(Duration::from_millis(20))? { @@ -96,6 +96,8 @@ async fn run( (_, KeyCode::Backspace) => app.input_backspace(), (_, KeyCode::Left) => app.cursor_left(), (_, KeyCode::Right) => app.cursor_right(), + (_, KeyCode::Up) => app.scroll_up(), + (_, KeyCode::Down) => app.scroll_down(), (_, KeyCode::Home) => app.cursor = 0, (_, KeyCode::End) => app.cursor = app.input.len(), _ => {} diff --git a/src/tui/app.rs b/src/tui/app.rs index 82342c0..7786858 100644 --- a/src/tui/app.rs +++ b/src/tui/app.rs @@ -23,6 +23,8 @@ pub struct AppState { pub input: String, /// Cursor position within `input` (byte index) pub cursor: usize, + /// Line scroll offset (byte index) + pub scroll_offset: usize, /// Status line text (connection state, errors, etc.) pub status: String, /// Whether we've fully registered @@ -38,6 +40,7 @@ impl AppState { members: Vec::new(), input: String::new(), cursor: 0, + scroll_offset: 0, status: "Set nick with /nick to connect.".into(), connected: false, } @@ -101,6 +104,16 @@ impl AppState { } } + /// Scroll up one line + pub fn scroll_up(&mut self) { + self.scroll_offset += 1; + } + + /// Scroll down one line + pub fn scroll_down(&mut self) { + self.scroll_offset = self.scroll_offset.saturating_sub(1); + } + /// Take the current input, clear the box, return the text pub fn take_input(&mut self) -> String { self.cursor = 0; diff --git a/src/tui/ui.rs b/src/tui/ui.rs index 0f552f2..ecdc477 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -12,7 +12,7 @@ use unicode_width::UnicodeWidthStr; // Dark terminal aesthetic: near-black background, cool grey chrome, // amber accent for our own nick, cyan for others, muted green for system. -pub fn draw(f: &mut Frame, state: &AppState) { +pub fn draw(f: &mut Frame, state: &mut AppState) { let area = f.area(); // Fill background @@ -54,7 +54,7 @@ fn draw_titlebar(f: &mut Frame, area: Rect, state: &AppState) { f.render_widget(Paragraph::new(title).style(Style::default()), area); } -fn draw_body(f: &mut Frame, area: Rect, state: &AppState) { +fn draw_body(f: &mut Frame, area: Rect, state: &mut AppState) { // Body: [chat (fill)] | [members (18)] let cols = Layout::default() .direction(Direction::Horizontal) @@ -68,7 +68,7 @@ fn draw_body(f: &mut Frame, area: Rect, state: &AppState) { draw_members_panel(f, cols[1], state); } -fn draw_center(f: &mut Frame, area: Rect, state: &AppState) { +fn draw_center(f: &mut Frame, area: Rect, state: &mut AppState) { // Centre column: chat log on top, input box on bottom let rows = Layout::default() .direction(Direction::Vertical) @@ -82,38 +82,44 @@ fn draw_center(f: &mut Frame, area: Rect, state: &AppState) { draw_input(f, rows[1], state); } -fn draw_chat_log(f: &mut Frame, area: Rect, state: &AppState) { +fn draw_chat_log(f: &mut Frame, area: Rect, state: &mut AppState) { let lines: Vec = state .messages .iter() .map(|msg| render_chat_line(msg)) .collect(); + let block = Block::default() .borders(Borders::ALL) .border_type(BorderType::Plain) .border_style(Style::default().fg(Color::Green)) .style(Style::default()); + let inner_width = area.width.saturating_sub(2) as usize; let inner_height = area.height.saturating_sub(2) as usize; let total_wrapped = count_wrapped_lines(&lines, inner_width); - let (padded_lines, scroll) = if total_wrapped < inner_height { - // Pad the top with empty lines to push content to the bottom + let (padded_lines, base_scroll) = if total_wrapped < inner_height { let padding = inner_height - total_wrapped; let mut padded = vec![Line::raw(""); padding]; padded.extend(lines); (padded, 0u16) } else { - // Content overflows — scroll to keep the latest lines visible let scroll = total_wrapped.saturating_sub(inner_height); (lines, scroll as u16) }; + // Max scrollable lines upward from the natural bottom position + let max_offset = base_scroll as usize; + + // Clamp the offset and write it back so app.rs stays in sync + state.scroll_offset = state.scroll_offset.clamp(0, max_offset); + let final_scroll = (base_scroll as i32 - state.scroll_offset as i32) as u16; f.render_widget( Paragraph::new(Text::from(padded_lines)) .block(block) .wrap(Wrap { trim: false }) - .scroll((scroll, 0)), + .scroll((final_scroll, 0)), area, ); } -- cgit v1.2.3-59-g8ed1b