ratatui-markdown 0.3.3

Markdown rendering, syntax highlighting, collapsible trees, and rich scroll widgets for ratatui
Documentation
use ratatui::{
    layout::Rect,
    text::{Line, Span},
    widgets::Paragraph,
    Frame,
};

use super::{CursorLineMode, SpanTree};
use crate::{scroll::render_arrow_scrollbar, theme::RichTextTheme};

pub(super) fn render(
    tree: &mut SpanTree,
    f: &mut Frame,
    inner_area: Rect,
    outer_area: Rect,
    theme: &impl RichTextTheme,
) {
    let visible_height = inner_area.height as usize;
    tree.viewport_height = visible_height.max(1);

    tree.clamp_scroll_offset();

    if tree.entries.is_empty() {
        return;
    }

    let highlight_bg = theme.get_popup_selected_background();
    let selected_id = tree.selected_id.as_deref();

    let mut visible_lines: Vec<Line<'static>> = Vec::new();
    let mut global_line = 0usize;
    let start = tree.scroll_offset;
    let end = start + visible_height;

    for entry in &tree.entries {
        let is_selected = selected_id == Some(entry.id.as_str());

        for (line_idx, entry_spans) in entry.lines.iter().enumerate() {
            if global_line >= end {
                break;
            }
            if global_line >= start {
                let mut spans: Vec<Span<'static>> = entry_spans.clone();
                let should_render_cursor = tree.cursor_column < spans.len()
                    && match tree.cursor_line_mode {
                        CursorLineMode::HeaderOnly => line_idx == 0,
                        CursorLineMode::AllLines => true,
                    };

                if is_selected {
                    if should_render_cursor {
                        spans[tree.cursor_column] = tree.cursor_span.clone();
                    }
                    for span in &mut spans {
                        span.style = span.style.bg(highlight_bg);
                    }
                } else if should_render_cursor {
                    spans[tree.cursor_column] = tree.blank_cursor_span.clone();
                }

                visible_lines.push(Line::from(spans));
            }
            global_line += 1;
        }

        if entry.lines.is_empty() {
            if global_line >= start && global_line < end {
                let mut spans: Vec<Span<'static>> = Vec::new();
                if is_selected {
                    spans.push(tree.cursor_span.clone());
                    for span in &mut spans {
                        span.style = span.style.bg(highlight_bg);
                    }
                } else {
                    spans.push(tree.blank_cursor_span.clone());
                }
                visible_lines.push(Line::from(spans));
            }
            global_line += 1;
        }

        if global_line >= end {
            break;
        }
    }

    let paragraph = Paragraph::new(visible_lines);
    f.render_widget(paragraph, inner_area);

    let total = tree.total_lines();
    if total > visible_height {
        render_arrow_scrollbar(
            f,
            outer_area,
            total,
            visible_height,
            tree.scroll_offset,
            theme,
        );
    }
}