feat(tui): rewrite UI with line numbers, soft wrap, and scrolling
This commit is contained in:
@@ -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),
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user