From 68202469972e40f256dab5b69c7995d4d0861131 Mon Sep 17 00:00:00 2001 From: dailz Date: Fri, 10 Apr 2026 23:38:06 +0800 Subject: [PATCH] feat(tui): rewrite UI with line numbers, soft wrap, and scrolling --- crates/tui/src/main.rs | 2 +- crates/tui/src/ui.rs | 135 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 125 insertions(+), 12 deletions(-) diff --git a/crates/tui/src/main.rs b/crates/tui/src/main.rs index 67c09c4..00ba847 100644 --- a/crates/tui/src/main.rs +++ b/crates/tui/src/main.rs @@ -29,7 +29,7 @@ fn main() -> anyhow::Result<()> { } while !app.should_quit { - terminal.draw(|frame| ui::render(frame, &app))?; + terminal.draw(|frame| ui::render(frame, &mut app))?; if crossterm::event::poll(std::time::Duration::from_millis(100))? { match crossterm::event::read()? { crossterm::event::Event::Key(key) => app.handle_key(key), diff --git a/crates/tui/src/ui.rs b/crates/tui/src/ui.rs index e44685c..a6fecf0 100644 --- a/crates/tui/src/ui.rs +++ b/crates/tui/src/ui.rs @@ -1,26 +1,139 @@ use crate::app::App; -pub fn render(frame: &mut ratatui::Frame, _app: &App) { +pub fn render(frame: &mut ratatui::Frame, app: &mut App) { use ratatui::layout::{Constraint, Layout}; - use ratatui::widgets::{Block, Borders, Paragraph}; + use ratatui::style::Style; + use ratatui::widgets::Paragraph; - let chunks = Layout::vertical([ + let outer = Layout::vertical([ Constraint::Length(1), Constraint::Min(1), Constraint::Length(1), ]) .split(frame.area()); + // ── Title bar ────────────────────────────────────────────────── + let title_text = if app.is_loaded() { + let name = app.file_name().unwrap_or("unknown"); + format!(" {} [{}/{}]", name, app.cursor_line + 1, app.total_lines()) + } else { + " Log Viewer".to_string() + }; frame.render_widget( - Paragraph::new(" Log Viewer").style(ratatui::style::Style::default().bold()), - chunks[0], + Paragraph::new(title_text).style(Style::default().bold()), + outer[0], ); - // Main area — 使用 Block::new()(ratatui 0.30 推荐风格) - frame.render_widget( - Block::new().borders(Borders::ALL).title("No file loaded"), - chunks[1], - ); + // ── Content area ─────────────────────────────────────────────── + if !app.is_loaded() { + frame.render_widget(Paragraph::new(" No file loaded"), outer[1]); + } else { + render_content(frame, app, outer[1]); + } - frame.render_widget(Paragraph::new(" Press '?' for help | q to quit"), chunks[2]); + // ── Status bar ───────────────────────────────────────────────── + frame.render_widget( + Paragraph::new(" j/k:scroll d/u:half-page f/b:page G/gg:jump q:quit"), + outer[2], + ); +} + +fn render_content(frame: &mut ratatui::Frame, app: &mut App, area: ratatui::layout::Rect) { + use ratatui::style::{Color, Style}; + use ratatui::text::Line; + use ratatui::widgets::Paragraph; + + let content_width = area.width as usize; + let content_height = area.height as usize; + + app.content_height = area.height; + app.content_width = area.width; + + let total_lines = app.total_lines(); + let line_num_width = if total_lines > 0 { + total_lines.to_string().len() + } else { + 0 + }; + let gutter_width = if total_lines > 0 { + line_num_width + 1 + 1 + } else { + 0 + }; + + let actual_content_width = content_width.saturating_sub(gutter_width); + + if content_height == 0 || actual_content_width == 0 { + return; + } + + app.recompute_wrap_cache(actual_content_width); + let mut visual_acc: usize = 0; + let mut start_logical: usize = 0; + let mut offset_in_line: usize = 0; + let v_offset = app.v_offset; + + for (i, &h) in app.visual_heights.iter().enumerate() { + if visual_acc.saturating_add(h) > v_offset { + start_logical = i; + offset_in_line = v_offset.saturating_sub(visual_acc); + break; + } + visual_acc += h; + if i == app.visual_heights.len() - 1 { + start_logical = i; + offset_in_line = 0; + } + } + + let mut lines: Vec = Vec::new(); + let mut current_visual_offset: usize = 0; + let available_rows = content_height; + + for logical_line in start_logical..total_lines { + let wrapped = &app.wrap_cache[logical_line]; + let start_row = if logical_line == start_logical { + offset_in_line + } else { + 0 + }; + + for (visual_row, text) in wrapped.iter().enumerate().skip(start_row) { + if current_visual_offset >= available_rows { + break; + } + + let is_cursor = logical_line == app.cursor_line; + let style = if is_cursor { + Style::default().bg(Color::DarkGray) + } else { + Style::default() + }; + + let gutter_text = if visual_row == 0 { + format!( + "{:>width$} \u{2502}", + logical_line + 1, + width = line_num_width + ) + } else { + format!("{:width$} \u{2502}", "", width = line_num_width) + }; + + let full_line = format!("{}{}", gutter_text, text); + lines.push(Line::styled(full_line, style)); + + current_visual_offset += 1; + } + + if current_visual_offset >= available_rows { + break; + } + } + + while lines.len() < available_rows { + lines.push(Line::styled(String::new(), Style::default())); + } + + frame.render_widget(Paragraph::new(lines), area); }