elio 1.3.0

Snappy, batteries-included terminal file manager with rich previews, inline images, bulk actions, and trash support.
Documentation
use super::super::theme::Palette;
use super::super::{helpers, theme};
use super::entries::{
    browser_directory_secondary, browser_entry_detail, browser_entry_modified,
    render_compact_list_row,
};
use super::scrollbar::{render_browser_scrollbar, split_scrollbar_area};
use crate::app::{App, ClipOp, EntryHit, FrameState, ViewMetrics};
use ratatui::{
    Frame,
    layout::{Alignment, Constraint, Direction, Layout, Rect},
    style::{Modifier, Style},
    text::{Line, Span},
    widgets::Paragraph,
};

pub(super) fn render_list(
    frame: &mut Frame<'_>,
    area: Rect,
    app: &App,
    state: &mut FrameState,
    palette: Palette,
) {
    let (content_area, scrollbar_area) = split_scrollbar_area(area);

    helpers::fill_area(frame, content_area, palette.panel_alt, palette.text);
    if let Some(sb) = scrollbar_area {
        helpers::fill_area(frame, sb, palette.panel_alt, palette.border);
    }

    let row_height = helpers::list_row_height();
    state.metrics = ViewMetrics {
        cols: 1,
        rows_visible: (content_area.height / row_height.max(1)).max(1) as usize,
    };

    if app.navigation.entries.is_empty() {
        helpers::render_empty_state(frame, content_area, "This folder is empty", palette);
        return;
    }

    for (visible_index, entry_index) in (app.navigation.scroll_row..app.navigation.entries.len())
        .take(state.metrics.rows_visible)
        .enumerate()
    {
        let entry = &app.navigation.entries[entry_index];
        let row = Rect {
            x: content_area.x,
            y: content_area.y + visible_index as u16 * row_height,
            width: content_area.width,
            height: row_height,
        };
        let selected = entry_index == app.navigation.selected;
        let multi_selected = app.is_selected(&entry.path);
        let clip_op = app.clipboard_op_for(&entry.path);
        let appearance = theme::resolve_browser_entry(entry);
        let icon_color = appearance.color;
        let bg = if selected {
            palette.selected_bg
        } else {
            palette.panel_alt
        };
        if row_height == 1 {
            frame.render_widget(
                Paragraph::new(render_compact_list_row(
                    app, entry, selected, row.width, palette,
                ))
                .style(Style::default().bg(bg).fg(palette.text)),
                row,
            );
        } else {
            // All mark states take priority over the cursor colour for the bar —
            // the cursor position is already communicated by the row background.
            let bar_color = if clip_op == Some(ClipOp::Yank) {
                palette.yank_bar
            } else if clip_op == Some(ClipOp::Cut) {
                palette.cut_bar
            } else if multi_selected {
                palette.selection_bar
            } else if selected {
                palette.selected_border
            } else {
                bg
            };
            let columns = Layout::default()
                .direction(Direction::Horizontal)
                .constraints([Constraint::Length(1), Constraint::Min(1)])
                .split(row);
            frame.render_widget(
                Paragraph::new(if selected || multi_selected || clip_op.is_some() {
                    ""
                } else {
                    " "
                })
                .alignment(Alignment::Left)
                .style(Style::default().bg(bg).fg(bar_color)),
                columns[0],
            );
            let secondary = if entry.is_dir() {
                browser_directory_secondary(app, entry)
            } else if row_height >= 3 {
                format!(
                    "{}{}",
                    browser_entry_detail(app, entry).unwrap_or_default(),
                    browser_entry_modified(entry)
                )
            } else {
                browser_entry_detail(app, entry).unwrap_or_default()
            };
            frame.render_widget(
                Paragraph::new(vec![
                    Line::from(vec![
                        Span::styled(
                            appearance.icon,
                            Style::default().fg(icon_color).add_modifier(Modifier::BOLD),
                        ),
                        Span::raw(" "),
                        Span::styled(
                            helpers::clamp_label(&entry.name, row.width.saturating_sub(8) as usize),
                            Style::default()
                                .fg(palette.text)
                                .add_modifier(Modifier::BOLD),
                        ),
                    ]),
                    Line::from(vec![
                        Span::raw("  "),
                        Span::styled(secondary, Style::default().fg(palette.muted)),
                    ]),
                ])
                .style(Style::default().bg(bg).fg(palette.text)),
                columns[1],
            );
        }
        state.entry_hits.push(EntryHit {
            rect: row,
            index: entry_index,
        });
    }

    if let Some(sb) = scrollbar_area {
        render_browser_scrollbar(
            frame,
            sb,
            app.navigation.entries.len(),
            state.metrics.rows_visible,
            app.navigation.scroll_row,
            palette,
        );
    }
}