use crate::app::App; pub fn render(frame: &mut ratatui::Frame, app: &mut App) { use ratatui::layout::{Constraint, Layout}; use ratatui::style::Style; use ratatui::widgets::Paragraph; 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"); let cursor_display = if app.total_lines() == 0 { 0 } else { app.cursor_line + 1 }; format!(" {} [{}/{}]", name, cursor_display, app.total_lines()) } else { " Log Viewer".to_string() }; frame.render_widget( Paragraph::new(title_text).style(Style::default().bold()), outer[0], ); // ── Content area ─────────────────────────────────────────────── if !app.is_loaded() { frame.render_widget(Paragraph::new(" No file loaded"), outer[1]); } else { render_content(frame, app, outer[1]); } // ── Status bar ───────────────────────────────────────────────── frame.render_widget( Paragraph::new(" j/k:scroll d/u:half-page f/b:page G/gg:jump Tab:format 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); }