feat(tui): rewrite UI with line numbers, soft wrap, and scrolling

This commit is contained in:
dailz
2026-04-10 23:38:06 +08:00
parent 555ffc0836
commit 6820246997
2 changed files with 125 additions and 12 deletions

View File

@@ -29,7 +29,7 @@ fn main() -> anyhow::Result<()> {
} }
while !app.should_quit { 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))? { if crossterm::event::poll(std::time::Duration::from_millis(100))? {
match crossterm::event::read()? { match crossterm::event::read()? {
crossterm::event::Event::Key(key) => app.handle_key(key), crossterm::event::Event::Key(key) => app.handle_key(key),

View File

@@ -1,26 +1,139 @@
use crate::app::App; 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::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::Length(1),
Constraint::Min(1), Constraint::Min(1),
Constraint::Length(1), Constraint::Length(1),
]) ])
.split(frame.area()); .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( frame.render_widget(
Paragraph::new(" Log Viewer").style(ratatui::style::Style::default().bold()), Paragraph::new(title_text).style(Style::default().bold()),
chunks[0], outer[0],
); );
// Main area — 使用 Block::new()ratatui 0.30 推荐风格) // ── Content area ───────────────────────────────────────────────
frame.render_widget( if !app.is_loaded() {
Block::new().borders(Borders::ALL).title("No file loaded"), frame.render_widget(Paragraph::new(" No file loaded"), outer[1]);
chunks[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<Line> = 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);
} }