use ratatui::Frame;
use ratatui::layout::Rect;
use ratatui::style::Style;
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
use travelagent_core::syntax::SyntaxHighlighter;
use crate::app::{App, FocusedPanel, ViewerRender};
use crate::ui::app_layout::diff_stat_title;
use crate::ui::styles;
pub(super) fn render_viewer_pane(frame: &mut Frame, app: &mut App, area: Rect) {
let focused = app.nav.focused_panel == FocusedPanel::Diff;
let path_label = app
.current_file_path()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "Overview".to_string());
let mode_label = match app.viewer.render_mode() {
ViewerRender::Raw => "Raw",
ViewerRender::Rendered => "Rendered",
};
let title = format!(" Viewer ({mode_label}) \u{2014} {path_label} ");
let block = Block::default()
.title(title)
.title_top(diff_stat_title(app).right_aligned())
.borders(Borders::ALL)
.style(styles::panel_style(&app.theme))
.border_style(styles::border_style(&app.theme, focused));
let inner = block.inner(area);
frame.render_widget(block, area);
app.viewer
.set_viewport(inner.height as usize, inner.width as usize);
app.viewer.ensure_cursor_visible();
match app.viewer.render_mode() {
ViewerRender::Raw => render_raw(frame, app, inner),
ViewerRender::Rendered => render_rendered(frame, app, inner),
}
}
fn render_raw(frame: &mut Frame, app: &App, inner: Rect) {
let Some(content) = app.viewer.content() else {
let msg = Paragraph::new("(no content)").style(Style::default().fg(app.theme.fg_dim));
frame.render_widget(msg, inner);
return;
};
let theme = &app.theme;
let total = content.lines.len();
let scroll = app.viewer.scroll_offset();
let cursor = app.viewer.cursor();
let height = inner.height as usize;
let highlighter = SyntaxHighlighter::default();
let highlighted = highlighter.highlight_file_lines(&content.path, &content.lines);
let gutter_w = total.max(1).to_string().len();
let scroll_x = app.viewer.scroll_x();
let wrap = app.viewer.wrap_lines();
let mut lines: Vec<Line> = Vec::with_capacity(height.min(total.saturating_sub(scroll)));
for idx in scroll..(scroll + height).min(total) {
let lineno = idx + 1;
let is_cursor = idx == cursor;
let gutter_style = if is_cursor {
Style::default().fg(theme.cursor_color)
} else {
Style::default().fg(theme.fg_dim)
};
let mut spans: Vec<Span> = vec![Span::styled(
format!("{lineno:>gutter_w$} \u{2502} "),
gutter_style,
)];
match highlighted.as_ref().and_then(|h| h.get(idx)) {
Some(Some(hl)) => {
for (hint, text) in hl {
spans.push(Span::styled(
text.clone(),
styles::style_hint_to_ratatui(*hint),
));
}
}
_ => {
let raw = content.lines.get(idx).cloned().unwrap_or_default();
spans.push(Span::styled(raw, Style::default().fg(theme.fg_primary)));
}
}
let mut line = Line::from(spans);
if is_cursor {
line = line.style(Style::default().bg(theme.bg_highlight));
}
lines.push(line);
}
let mut para = Paragraph::new(lines).style(Style::default().fg(theme.fg_primary));
if wrap {
para = para.wrap(Wrap { trim: false });
} else if scroll_x > 0 {
para = para.scroll((0, scroll_x as u16));
}
frame.render_widget(para, inner);
}
fn render_rendered(frame: &mut Frame, app: &App, inner: Rect) {
let Some(content) = app.viewer.content() else {
let msg = Paragraph::new("(no content)").style(Style::default().fg(app.theme.fg_dim));
frame.render_widget(msg, inner);
return;
};
let body = content.lines.join("\n");
let rendered = crate::ui::markdown::render_markdown(&body, &app.theme, inner.width as usize);
let scroll = app
.viewer
.scroll_offset()
.min(rendered.len().saturating_sub(1)) as u16;
let para = Paragraph::new(rendered)
.style(Style::default().fg(app.theme.fg_primary))
.wrap(Wrap { trim: false })
.scroll((scroll, 0));
frame.render_widget(para, inner);
}