use super::super::theme::Palette;
use super::super::{helpers, theme};
use super::scrollbar::render_preview_scrollbar;
use crate::app::{App, FrameState};
use ratatui::{
Frame,
buffer::Buffer,
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Modifier, Style},
text::{Line, Span},
widgets::{Block, BorderType, Borders, Paragraph, Widget},
};
pub(super) fn render_preview(
frame: &mut Frame<'_>,
area: Rect,
app: &App,
state: &mut FrameState,
palette: Palette,
) {
state.preview_panel = Some(area);
let title_line = if let Some(entry) = app.selected_entry() {
Line::from(vec![
Span::styled(
format!(" {} ", theme::entry_symbol(entry)),
Style::default()
.fg(theme::entry_color(entry, palette))
.add_modifier(Modifier::BOLD),
),
Span::styled(
helpers::clamp_label(&entry.name, area.width.saturating_sub(10) as usize),
Style::default()
.fg(palette.accent_text)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
])
} else {
Line::from(vec![
Span::styled(
" Preview ",
Style::default()
.fg(palette.accent_text)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
])
};
let block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.style(Style::default().bg(palette.panel).fg(palette.text))
.border_style(Style::default().fg(palette.border));
frame.render_widget(block, area);
helpers::render_panel_title(frame, area, title_line);
let inner = helpers::inner_with_padding(area);
helpers::fill_area(frame, inner, palette.panel, palette.text);
if app.selected_entry().is_none() {
helpers::render_empty_state(frame, inner, "Nothing selected", palette);
return;
}
if inner.height > 0 {
render_preview_body(frame, inner, app, state, palette);
}
}
fn render_preview_body(
frame: &mut Frame<'_>,
area: Rect,
app: &App,
state: &mut FrameState,
palette: Palette,
) {
let sections = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1), Constraint::Min(0)])
.split(area);
helpers::fill_area(frame, sections[0], palette.panel, palette.text);
if sections[1].height > 0 {
helpers::fill_area(frame, sections[1], palette.panel, palette.text);
}
let body = if sections[1].width >= 6 {
Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Min(0), Constraint::Length(1)])
.split(sections[1])
} else {
Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Min(0)])
.split(sections[1])
};
let body_area = body[0];
let scrollbar_area = body.get(1).copied();
state.preview_body_area = Some(sections[1]);
let (media_area, text_area) = if let Some(media_rows) = app.preview_visual_rows(body_area) {
let split = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(media_rows), Constraint::Min(0)])
.split(body_area);
(Some(split[0]), split[1])
} else {
(None, body_area)
};
state.preview_media_area = media_area;
state.preview_content_area = Some(text_area);
if let Some(media_area) = media_area {
helpers::fill_area(frame, media_area, palette.panel, palette.text);
}
helpers::fill_area(frame, text_area, palette.panel, palette.text);
if let Some(scrollbar_area) = scrollbar_area {
helpers::fill_area(frame, scrollbar_area, palette.panel, palette.border);
}
let visible_rows = text_area.height as usize;
state.preview_rows_visible = visible_rows;
state.preview_cols_visible = text_area.width as usize;
let section_label = app.preview_section_label();
let header_detail_width = sections[0]
.width
.saturating_sub(section_label.len() as u16 + 2) as usize;
let header_detail = app
.preview_header_detail_for_width(visible_rows, header_detail_width)
.as_deref()
.map(|detail| {
if header_detail_width == 0 {
String::new()
} else {
helpers::clamp_label(detail, header_detail_width)
}
})
.unwrap_or_default();
frame.render_widget(
Paragraph::new(Line::from(vec![
Span::styled(
section_label.to_string(),
Style::default()
.fg(palette.accent)
.add_modifier(Modifier::BOLD),
),
Span::styled(" ", Style::default().fg(palette.muted)),
Span::styled(header_detail, Style::default().fg(palette.muted)),
]))
.style(Style::default().bg(palette.panel).fg(palette.text)),
sections[0],
);
if app.browser_wheel_burst_active() {
frame.render_widget(
Paragraph::new(Line::from(Span::styled(
"Scrolling...",
Style::default().fg(palette.muted),
)))
.style(Style::default().bg(palette.panel).fg(palette.text))
.alignment(Alignment::Center),
text_area,
);
return;
}
if app.preview_prefers_image_surface() {
if let Some(message) = app.preview_overlay_placeholder_message() {
frame.render_widget(
Paragraph::new(Line::from(Span::styled(
message,
Style::default().fg(palette.muted),
)))
.style(Style::default().bg(palette.panel).fg(palette.text))
.alignment(Alignment::Center),
text_area,
);
}
return;
}
if app.preview_uses_image_overlay() {
return;
}
if app.preview_wraps() {
let wrapped_lines = app.preview_wrapped_lines(text_area.width as usize);
frame.render_widget(
PreviewLinesWidget::new(
wrapped_lines.as_ref(),
app.preview_scroll_offset(),
app.preview_horizontal_scroll_offset(),
Style::default().bg(palette.panel).fg(palette.text),
),
text_area,
);
} else {
let paragraph = Paragraph::new(app.preview_lines())
.style(Style::default().bg(palette.panel).fg(palette.text))
.scroll((
app.preview_scroll_offset().min(u16::MAX as usize) as u16,
app.preview_horizontal_scroll_offset()
.min(u16::MAX as usize) as u16,
));
frame.render_widget(paragraph, text_area);
}
if let Some(scrollbar_area) = scrollbar_area {
render_preview_scrollbar(
frame,
scrollbar_area,
app,
visible_rows,
text_area.width as usize,
palette,
);
}
}
struct PreviewLinesWidget<'a> {
lines: &'a [Line<'static>],
scroll: usize,
h_scroll: usize,
style: Style,
}
impl<'a> PreviewLinesWidget<'a> {
fn new(lines: &'a [Line<'static>], scroll: usize, h_scroll: usize, style: Style) -> Self {
Self {
lines,
scroll,
h_scroll,
style,
}
}
}
impl Widget for PreviewLinesWidget<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let area = area.intersection(buf.area);
if area.is_empty() {
return;
}
buf.set_style(area, self.style);
for (line, row) in self.lines.iter().skip(self.scroll).zip(area.rows()) {
let clipped;
let render_line: &Line = if self.h_scroll > 0 {
clipped = skip_line_chars(line, self.h_scroll);
&clipped
} else {
line
};
let line_width = render_line.width();
let offset = match render_line.alignment.unwrap_or(Alignment::Left) {
Alignment::Center => row.width.saturating_sub(line_width as u16) / 2,
Alignment::Right => row.width.saturating_sub(line_width as u16),
Alignment::Left => 0,
};
if offset >= row.width {
continue;
}
let x = row.x.saturating_add(offset);
let max_width = row.width.saturating_sub(offset);
buf.set_line(x, row.y, render_line, max_width);
}
}
}
fn skip_line_chars(line: &Line<'static>, skip: usize) -> Line<'static> {
let mut remaining = skip;
let mut result = Vec::new();
for span in &line.spans {
if remaining == 0 {
result.push(span.clone());
continue;
}
let char_count = span.content.chars().count();
if char_count <= remaining {
remaining -= char_count;
} else {
let content: String = span.content.chars().skip(remaining).collect();
if !content.is_empty() {
result.push(Span::styled(content, span.style));
}
remaining = 0;
}
}
let mut new_line = Line::from(result);
new_line.alignment = line.alignment;
new_line
}