use super::*;
pub(super) fn render_viewer(f: &mut ratatui::Frame<'_>, area: Rect, app_state: &AppState) -> (usize, usize, Rect, Vec<ScrollBar>) {
let mut bars = Vec::new();
let bar_style = app_state.options.scroll_bar;
if let Some(viewer_state) = app_state.viewer() {
let filename = shown_name(&viewer_state.file_path);
let prefix = if viewer_state.from_edit { "Edit" } else { "View" };
let title = format!(" {}: {} ", prefix, filename);
let border_block = Block::default().title(Line::from(Span::styled(title, style_title())).centered()).borders(Borders::ALL).border_style(style_border());
let inner_area = border_block.inner(area);
f.render_widget(border_block, area);
let no_gutter = viewer_state.mode != ViewMode::Text || !app_state.options.line_numbers;
let line_num_width = if no_gutter { 0 } else { (viewer_state.line_count().to_string().len() as u16).max(3) + 2 };
let chunks = Layout::default().direction(Direction::Horizontal).constraints([Constraint::Length(line_num_width), Constraint::Min(0)]).split(inner_area);
let viewport_height = inner_area.height as usize;
let start = viewer_state.scroll_offset;
let end = (start + viewport_height).min(viewer_state.total_lines);
if !no_gutter {
let num_width = (line_num_width as usize).saturating_sub(1);
let line_numbers: Vec<Line> = (start..end)
.map(|index| {
let (line, from, _) = viewer_state.row(index);
let number = if from == 0 { (line + 1).to_string() } else { String::new() };
Line::from(Span::styled(format!("{number:>num_width$} "), style_columns()))
})
.collect();
let line_number_para = Paragraph::new(line_numbers).style(Style::default().bg(crate::display::gutter()));
f.render_widget(line_number_para, chunks[0]);
}
let content_area = if viewer_state.mode == ViewMode::Image {
centered(chunks[1], viewer_state.image_columns, viewer_state.total_lines)
} else {
chunks[1]
};
if viewer_state.from_edit {
let binary_msg = Paragraph::new("Binary file detected. Press Esc to return.").alignment(Alignment::Center).style(style_title());
f.render_widget(binary_msg, chunks[1]);
} else {
let selection = viewer_state.selected_range();
let marking = app_state.find_shown && viewer_state.mode != ViewMode::Image;
let needle = marking.then(|| crate::find::Needle::new(&app_state.find_term)).flatten();
let mut held: Option<LaidOut> = None;
let content_lines: Vec<Line> = (start..end)
.map(|index| {
let (line, row_from, row_to) = viewer_state.row(index);
if held.as_ref().is_none_or(|(held_line, _, _)| *held_line != line) {
let whole = viewer_state.line_text(line);
let matches = needle.as_ref().map(|needle| needle.find_all(&whole)).unwrap_or_default();
held = Some((line, whole.chars().collect(), matches));
}
let (_, characters, matches) = held.as_ref().expect("held just above");
let width = characters.len();
let (row_from, row_to) = (row_from.min(width), row_to.min(width));
let on_row = |from: usize, to: usize| {
let (from, to) = (from.clamp(row_from, row_to), to.clamp(row_from, row_to));
(from < to).then(|| (from - row_from, to - row_from))
};
let text: String = characters[row_from..row_to].iter().collect();
let mut spans = match viewer_state.image_colors.get(line) {
Some(colors) if viewer_state.mode == ViewMode::Image => colored_spans(&text, colors, viewer_state.image_backgrounds.get(line).map(Vec::as_slice)),
_ => vec![Span::styled(text, style_file())],
};
let first = matches.partition_point(|&(_, to)| to <= row_from);
for (from, to) in matches[first..].iter().take_while(|&&(from, _)| from < row_to).filter_map(|&(from, to)| on_row(from, to)) {
spans = overlay_range(spans, from, to, style_match());
}
if let Some(((first_line, first_col), (last_line, last_col))) = selection
&& (first_line..=last_line).contains(&line)
{
let to = if line == last_line { last_col.min(width) } else { width };
let from = if line == first_line { first_col.min(to) } else { 0 };
if let Some((from, to)) = on_row(from, to) {
spans = overlay_range(spans, from, to, style_selection());
}
}
Line::from(skip_columns(spans, viewer_state.horizontal_offset))
})
.collect();
let content_para = Paragraph::new(content_lines).style(style_file());
f.render_widget(content_para, content_area);
let rows = Extent {
content: viewer_state.total_lines,
view: viewport_height,
offset: viewer_state.scroll_offset,
};
bars.extend(vertical_bar(f, area, rows, bar_style));
if !viewer_state.wrapping() {
let columns = Extent {
content: viewer_state.content_width(),
view: chunks[1].width as usize,
offset: viewer_state.horizontal_offset,
};
bars.extend(horizontal_bar(f, area, chunks[1], columns, bar_style));
}
}
(viewport_height, chunks[1].width as usize, content_area, bars)
} else {
(0, 0, Rect::default(), bars)
}
}
pub(super) fn render_diff(f: &mut ratatui::Frame<'_>, area: Rect, app_state: &mut AppState) -> (usize, usize) {
let block = Block::default().title(Line::from(Span::styled(" Compare ", style_title())).centered()).borders(Borders::ALL).border_style(style_border());
let inner = block.inner(area);
if let Screen::Diff(state) = &mut app_state.screen {
let rows = (inner.height as usize).saturating_sub(1);
state.scroll_offset = state.scroll_offset.min(state.rows.len().saturating_sub(rows));
}
let app_state = &*app_state;
let Screen::Diff(state) = &app_state.screen else {
return (0, 0);
};
f.render_widget(block, area);
let [left_area, marker_area, right_area] = Layout::horizontal([Constraint::Fill(1), Constraint::Length(3), Constraint::Fill(1)]).areas(inner);
let [_, marker_area] = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]).areas(marker_area);
let height = inner.height.saturating_sub(1) as usize;
let shown = &state.rows[state.scroll_offset.min(state.rows.len())..(state.scroll_offset + height).min(state.rows.len())];
let markers: Vec<Line> = shown.iter().map(|row| Line::from(Span::styled(format!(" {} ", row.marker()), style_columns().add_modifier(Modifier::BOLD)))).collect();
f.render_widget(Paragraph::new(markers), marker_area);
let gutter_width = if app_state.options.line_numbers { (state.left.len().max(state.right.len()).to_string().len() as u16).max(3) + 1 } else { 0 };
let mut text_width = 0;
for (area, path, lines, is_left) in [(left_area, &state.left_path, &state.left, true), (right_area, &state.right_path, &state.right, false)] {
let [header_area, body_area] = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]).areas(area);
let width = header_area.width as usize;
let header = match crate::utils::limit_path_string(path, width) {
whole if display_width(&whole) <= width => whole,
_ => crate::utils::limit_path_string(path, width.saturating_sub(3)),
};
f.render_widget(Paragraph::new(Line::from(Span::styled(header, style_title()))), header_area);
let [gutter_area, text_area] = Layout::horizontal([Constraint::Length(gutter_width), Constraint::Min(0)]).areas(body_area);
text_width = text_area.width as usize;
let mut numbers = Vec::with_capacity(shown.len());
let mut texts = Vec::with_capacity(shown.len());
for row in shown {
let line = if is_left { row.left } else { row.right };
let style = if row.same || line.is_none() { style_file() } else { style_file().patch(style_selection()) };
let number = line.map(|index| (index + 1).to_string()).unwrap_or_default();
numbers.push(Line::from(Span::styled(format!("{number:>width$} ", width = (gutter_width as usize).saturating_sub(1)), style_columns())));
let text = line.and_then(|index| lines.get(index)).map(|line| crate::utils::printable_line(line)).unwrap_or_default();
texts.push(Line::from(skip_columns(vec![Span::raw(text)], state.horizontal_offset)).style(style));
}
if gutter_width > 0 {
f.render_widget(Paragraph::new(numbers).style(Style::default().bg(crate::display::gutter())), gutter_area);
}
f.render_widget(Paragraph::new(texts), text_area);
}
(height, text_width)
}
pub(super) fn render_editor(f: &mut ratatui::Frame<'_>, area: Rect, app_state: &mut AppState) -> usize {
let line_numbers = app_state.options.line_numbers;
let mut bars = Vec::new();
let bar_style = app_state.options.scroll_bar;
let (viewport_height, content_area) = if let Screen::Editor(editor_state) = &mut app_state.screen {
let filename = shown_name(&editor_state.file_path);
let modified = if editor_state.modified { " [Modified]" } else { "" };
let title = format!(" Edit: {}{} ", filename, modified);
let border_block = Block::default().title(Line::from(Span::styled(title, style_title())).centered()).borders(Borders::ALL).border_style(style_border());
let inner_area = border_block.inner(area);
f.render_widget(border_block, area);
let total_lines = editor_state.lines.len();
let line_num_width = if line_numbers { (total_lines.to_string().len() as u16).max(3) + 2 } else { 0 };
let chunks = Layout::default().direction(Direction::Horizontal).constraints([Constraint::Length(line_num_width), Constraint::Min(0)]).split(inner_area);
let viewport_height = inner_area.height as usize;
editor_state.keep_in_view(viewport_height);
let start = editor_state.scroll_offset;
let end = (start + viewport_height).min(total_lines);
let num_width = (line_num_width as usize).saturating_sub(1);
let style_current_line = style_title().add_modifier(Modifier::BOLD);
let numbers: Vec<Line> = (start..end)
.map(|line_num| {
let style = if line_num == editor_state.cursor_line { style_current_line } else { style_columns() };
Line::from(Span::styled(format!("{:>width$} ", line_num + 1, width = num_width), style))
})
.collect();
if line_numbers {
let line_number_para = Paragraph::new(numbers).style(Style::default().bg(crate::display::gutter()));
f.render_widget(line_number_para, chunks[0]);
}
if editor_state.auto_scroll {
let visual_cursor_col = crate::utils::display_column(&editor_state.lines[editor_state.cursor_line], editor_state.cursor_col);
let viewport_width = chunks[1].width as usize;
if visual_cursor_col < editor_state.horizontal_offset {
editor_state.horizontal_offset = visual_cursor_col;
}
if visual_cursor_col >= editor_state.horizontal_offset + viewport_width {
editor_state.horizontal_offset = visual_cursor_col - viewport_width + 1;
}
}
let h_offset = editor_state.horizontal_offset;
let has_highlighting = !editor_state.highlighted_lines.is_empty();
let cursor_style = Style::new().add_modifier(EDITOR_CURSOR);
let selection = editor_state.selection();
let needle = app_state.find_shown.then(|| crate::find::Needle::new(&app_state.find_term)).flatten();
let mut content_lines: Vec<Line> = Vec::with_capacity(end - start);
for (idx, line) in editor_state.lines[start..end].iter().enumerate() {
let actual_line_idx = start + idx;
let mut spans: Vec<Span<'static>> = if has_highlighting && actual_line_idx < editor_state.highlighted_lines.len() {
editor_state.highlighted_lines[actual_line_idx].iter().map(|span| Span::styled(printable_line(&span.content), span.style)).collect()
} else {
vec![Span::styled(printable_line(line), style_file())]
};
for (from, to) in needle.as_ref().map(|needle| needle.find_all(line)).unwrap_or_default() {
spans = overlay_range(spans, visual_column(line, from), visual_column(line, to), style_match());
}
if let Some(((first_line, first_col), (last_line, last_col))) = selection
&& (first_line..=last_line).contains(&actual_line_idx)
{
let from = if actual_line_idx == first_line { visual_column(line, first_col) } else { 0 };
let to = if actual_line_idx == last_line { visual_column(line, last_col) } else { visual_column(line, line.chars().count()) };
spans = overlay_range(spans, from, to, style_selection());
}
if actual_line_idx == editor_state.cursor_line {
spans = place_cursor(spans, visual_column(line, editor_state.cursor_col), cursor_style);
}
content_lines.push(Line::from(skip_columns(spans, h_offset)));
}
let content_para = Paragraph::new(content_lines);
f.render_widget(content_para, chunks[1]);
let rows = Extent {
content: editor_state.lines.len(),
view: viewport_height,
offset: editor_state.scroll_offset,
};
bars.extend(vertical_bar(f, area, rows, bar_style));
let view = chunks[1].width as usize;
let columns = Extent {
content: (editor_state.widest_line() + 1).max(h_offset + view),
view,
offset: h_offset,
};
bars.extend(horizontal_bar(f, area, chunks[1], columns, bar_style));
(viewport_height, chunks[1])
} else {
(0, Rect::default())
};
app_state.editor_content_area = content_area;
app_state.scroll_bars = bars;
viewport_height
}