ratatui-kit 0.10.2

A framework for building interactive terminal user interfaces with ratatui
Documentation
use ratatui::{buffer::Buffer, layout::Rect, style::Style, text::Line};
use unicode_width::UnicodeWidthChar;

use super::types::TableBorderMode;

#[derive(Debug)]
pub(super) struct RenderedCell {
    pub(super) lines: Vec<Line<'static>>,
    /// Extra style patched on top of the row style for this cell only
    /// (column / cell highlight). Empty for cells that are not highlighted.
    pub(super) extra_style: Style,
}

#[derive(Debug)]
pub(super) struct RenderedRow {
    pub(super) cells: Vec<RenderedCell>,
    pub(super) style: Style,
    pub(super) selected: bool,
}

impl RenderedRow {
    pub(super) fn separator(style: Style) -> Self {
        Self {
            cells: Vec::new(),
            style,
            selected: false,
        }
    }

    pub(super) fn render_height(&self, border_mode: TableBorderMode) -> u16 {
        if self.is_separator() {
            return u16::from(matches!(border_mode, TableBorderMode::Grid));
        }

        self.cells
            .iter()
            .map(|cell| cell.lines.len() as u16)
            .max()
            .unwrap_or(1)
            .max(1)
    }

    fn is_separator(&self) -> bool {
        self.cells.is_empty()
    }
}

pub(super) fn rendered_rows_height(rows: &[RenderedRow], border_mode: TableBorderMode) -> u16 {
    let border_height = u16::from(matches!(
        border_mode,
        TableBorderMode::Outer | TableBorderMode::Grid
    ))
    .saturating_mul(2);
    rows.iter().fold(border_height, |height, row| {
        height.saturating_add(row.render_height(border_mode))
    })
}

pub(super) struct RenderTable<'a> {
    pub(super) area: Rect,
    pub(super) buf: &'a mut Buffer,
    pub(super) rows: &'a [RenderedRow],
    pub(super) widths: &'a [u16],
    pub(super) border_mode: TableBorderMode,
    pub(super) border_style: Style,
    pub(super) cell_padding: u16,
    pub(super) column_spacing: u16,
    pub(super) highlight_symbol: Option<&'static str>,
    /// Reserved leading width for the selection symbol. `0` means no gutter.
    pub(super) gutter: u16,
}

pub(super) fn render_table(mut table: RenderTable<'_>) {
    let mut y = table.area.y;

    if matches!(
        table.border_mode,
        TableBorderMode::Outer | TableBorderMode::Grid
    ) {
        let style = table.border_style;
        render_border_line(&mut table, y, '', '', '', style);
        y += 1;
    }

    for row in table.rows {
        if y >= table.area.bottom() {
            break;
        }

        if row.is_separator() {
            if matches!(table.border_mode, TableBorderMode::Grid) {
                render_border_line(&mut table, y, '', '', '', row.style);
                y += 1;
            }
            continue;
        }

        let height = row.render_height(table.border_mode);

        for line_index in 0..height {
            if y >= table.area.bottom() {
                break;
            }
            render_row_line(&mut table, row, line_index as usize, y);
            y += 1;
        }
    }

    if y < table.area.bottom()
        && matches!(
            table.border_mode,
            TableBorderMode::Outer | TableBorderMode::Grid
        )
    {
        let style = table.border_style;
        render_border_line(&mut table, y, '', '', '', style);
    }
}

fn render_border_line(
    table: &mut RenderTable<'_>,
    y: u16,
    left: char,
    mid: char,
    right: char,
    style: Style,
) {
    let mut x = table.area.x;
    put(table.buf, x, y, left, style);
    x += 1;
    // 选中符号的 gutter 是首列之前的一段横线,没有列间分隔符。
    for _ in 0..table.gutter {
        put(table.buf, x, y, '', style);
        x += 1;
    }
    for (index, width) in table.widths.iter().copied().enumerate() {
        for _ in 0..width.saturating_add(table.cell_padding.saturating_mul(2)) {
            put(table.buf, x, y, '', style);
            x += 1;
        }
        if index + 1 < table.widths.len() {
            let separator = if matches!(table.border_mode, TableBorderMode::Grid) {
                mid
            } else {
                ''
            };
            put(table.buf, x, y, separator, style);
            x += 1;
        }
    }
    put(table.buf, x, y, right, style);
}

fn render_row_line(table: &mut RenderTable<'_>, row: &RenderedRow, line_index: usize, y: u16) {
    let row_style = row.style;
    let mut x = table.area.x;

    match table.border_mode {
        TableBorderMode::None => {}
        TableBorderMode::Outer | TableBorderMode::Grid => {
            put(table.buf, x, y, '', table.border_style);
            x += 1;
        }
    }

    // 前置 gutter:选中行首行画符号(左对齐、按视宽裁剪),其余填空格,列宽已为它预留。
    if table.gutter > 0 {
        for offset in 0..table.gutter {
            put(table.buf, x + offset, y, ' ', row_style);
        }
        if row.selected
            && line_index == 0
            && let Some(symbol) = table.highlight_symbol
        {
            write_text_clamped(table.buf, x, y, symbol, row_style, table.gutter);
        }
        x += table.gutter;
    }

    for (index, width) in table.widths.iter().copied().enumerate() {
        // 列/单元格高亮:在行样式之上叠加该格的 extra_style。
        let cell_style = row_style.patch(
            row.cells
                .get(index)
                .map(|cell| cell.extra_style)
                .unwrap_or_default(),
        );

        for _ in 0..table.cell_padding {
            put(table.buf, x, y, ' ', cell_style);
            x += 1;
        }

        let line = row
            .cells
            .get(index)
            .and_then(|cell| cell.lines.get(line_index))
            .cloned()
            .unwrap_or_default();
        render_line(table.buf, x, y, width, line, cell_style);
        x += width;

        for _ in 0..table.cell_padding {
            put(table.buf, x, y, ' ', cell_style);
            x += 1;
        }

        if index + 1 < table.widths.len() {
            match table.border_mode {
                TableBorderMode::Grid => {
                    put(table.buf, x, y, '', table.border_style);
                    x += 1;
                }
                TableBorderMode::Outer => {}
                TableBorderMode::None => {
                    for _ in 0..table.column_spacing {
                        put(table.buf, x, y, ' ', row_style);
                        x += 1;
                    }
                }
            }
        }
    }

    if matches!(
        table.border_mode,
        TableBorderMode::Outer | TableBorderMode::Grid
    ) {
        put(table.buf, x, y, '', table.border_style);
    }
}

fn render_line(
    buf: &mut Buffer,
    x: u16,
    y: u16,
    width: u16,
    line: Line<'static>,
    row_style: Style,
) {
    let mut offset = 0u16;
    for span in line.spans {
        let style = row_style.patch(span.style);
        for c in span.content.chars() {
            let char_width = c.width().unwrap_or(0) as u16;
            if offset + char_width > width {
                return;
            }
            put(buf, x + offset, y, c, style);
            if char_width == 2 && offset + 1 < width {
                put(buf, x + offset + 1, y, ' ', style);
            }
            offset += char_width;
        }
    }

    while offset < width {
        put(buf, x + offset, y, ' ', row_style);
        offset += 1;
    }
}

fn write_text_clamped(buf: &mut Buffer, x: u16, y: u16, text: &str, style: Style, max_width: u16) {
    let mut offset = 0u16;
    for c in text.chars() {
        let char_width = c.width().unwrap_or(0) as u16;
        if offset + char_width > max_width {
            break;
        }
        put(buf, x + offset, y, c, style);
        if char_width == 2 && offset + 1 < max_width {
            put(buf, x + offset + 1, y, ' ', style);
        }
        offset += char_width;
    }
}

fn put(buf: &mut Buffer, x: u16, y: u16, c: char, style: Style) {
    let cell = &mut buf[(x, y)];
    cell.set_char(c);
    cell.set_style(style);
}