ansiq-runtime 0.1.0

Application loop, focus routing, subtree replacement, and redraw orchestration for Ansiq.
Documentation
use ansiq_core::{
    Alignment, Rect, Style, TableAlignment, TableProps, patch_style, styled_lines_from_text,
    table_column_layout, table_span_width,
};
use ansiq_render::FrameBuffer;

use crate::draw_border::{block_inner_rect, draw_block_frame};
use crate::draw_common::merge_highlight_style;
use crate::draw_text::draw_styled_line;

pub(crate) fn draw_table<Message>(
    buffer: &mut FrameBuffer,
    rect: Rect,
    props: &TableProps<Message>,
    style: Style,
) {
    if rect.is_empty() {
        return;
    }

    let (table_rect, table_style) = if let Some(block) = &props.block {
        let block_style = patch_style(style, block.style);
        draw_block_frame(buffer, rect, block, block_style);
        (block_inner_rect(rect, block), block_style)
    } else {
        (rect, style)
    };

    let column_count = props
        .header
        .as_ref()
        .map(ansiq_core::Row::column_count)
        .unwrap_or(0)
        .max(
            props
                .footer
                .as_ref()
                .map(ansiq_core::Row::column_count)
                .unwrap_or(0),
        )
        .max(
            props
                .rows
                .iter()
                .map(ansiq_core::Row::column_count)
                .max()
                .unwrap_or(0),
        );
    if column_count == 0 {
        return;
    }

    let highlight_symbol = props
        .highlight_symbol
        .as_ref()
        .and_then(|text| text.lines.first());
    let gutter = highlight_symbol
        .map(|line| line.width() as u16)
        .unwrap_or(0);
    let selection_spacing = props
        .highlight_spacing
        .should_add(props.state.selected().is_some())
        && gutter > 0;
    let table_rect = if selection_spacing {
        Rect::new(
            rect.x.saturating_add(gutter),
            rect.y,
            rect.width.saturating_sub(gutter),
            rect.height,
        )
    } else {
        table_rect
    };
    let (column_widths, column_positions) = table_column_layout(
        table_rect.width,
        column_count,
        &props.widths,
        props.column_spacing,
        props.flex,
    );
    let used_width = column_widths
        .iter()
        .zip(column_positions.iter())
        .map(|(width, position)| position.saturating_add(*width))
        .max()
        .unwrap_or(0)
        .min(table_rect.width);
    let table_rect = Rect::new(table_rect.x, table_rect.y, used_width, table_rect.height);
    let mut line_index = 0u16;

    if let Some(header) = &props.header {
        draw_table_row(
            buffer,
            table_rect,
            line_index,
            header,
            &column_widths,
            &column_positions,
            &props.alignments,
            table_style.bold(true),
            props.column_highlight_style,
            props.cell_highlight_style,
            None,
            None,
        );
        line_index = line_index.saturating_add(header.height_with_margin());
    }

    let footer_height = props
        .footer
        .as_ref()
        .map(ansiq_core::Row::height_with_margin)
        .unwrap_or(0);
    let body_height = table_rect
        .height
        .saturating_sub(line_index)
        .saturating_sub(footer_height);
    if body_height == 0 {
        if let Some(footer) = &props.footer {
            let footer_y = table_rect
                .height
                .saturating_sub(footer.height_with_margin());
            draw_table_row(
                buffer,
                table_rect,
                footer_y,
                footer,
                &column_widths,
                &column_positions,
                &props.alignments,
                table_style.bold(true),
                props.column_highlight_style,
                props.cell_highlight_style,
                None,
                None,
            );
        }
        return;
    }

    let mut start = props.state.offset().min(props.rows.len().saturating_sub(1));
    if let Some(selected) = props
        .state
        .selected()
        .map(|index| index.min(props.rows.len().saturating_sub(1)))
    {
        if selected < start {
            start = selected;
        } else {
            while !table_row_is_visible(&props.rows, start, selected, body_height)
                && start < selected
            {
                start = start.saturating_add(1);
            }
        }
    }

    let selected_cell = props.state.selected().zip(props.state.selected_column());
    let mut draw_y = line_index;
    let body_end = line_index.saturating_add(body_height);
    for (index, row) in props.rows.iter().enumerate().skip(start) {
        let row_height = row.height_with_margin();
        if row_height == 0 || draw_y >= body_end {
            break;
        }
        if draw_y.saturating_add(row_height) > body_end {
            break;
        }

        let selected = props.state.selected() == Some(index);
        let row_style = if selected {
            merge_highlight_style(table_style, props.row_highlight_style)
        } else {
            table_style
        };

        if selection_spacing {
            let gutter_rect = Rect::new(
                table_rect.x.saturating_sub(gutter),
                table_rect
                    .y
                    .saturating_add(draw_y.saturating_add(row.top_margin_value())),
                gutter,
                1,
            );
            if selected {
                if let Some(symbol) = highlight_symbol {
                    let symbol = ansiq_core::styled_line_from_line(symbol, row_style);
                    draw_styled_line(buffer, gutter_rect, 0, &symbol, 0);
                }
            }
        }

        draw_table_row(
            buffer,
            table_rect,
            draw_y,
            row,
            &column_widths,
            &column_positions,
            &props.alignments,
            row_style,
            props.column_highlight_style,
            props.cell_highlight_style,
            props.state.selected_column(),
            selected_cell.filter(|(selected_row, _)| *selected_row == index),
        );
        draw_y = draw_y.saturating_add(row_height);
    }

    if let Some(footer) = &props.footer {
        let footer_y = table_rect
            .height
            .saturating_sub(footer.height_with_margin());
        draw_table_row(
            buffer,
            table_rect,
            footer_y,
            footer,
            &column_widths,
            &column_positions,
            &props.alignments,
            table_style.bold(true),
            props.column_highlight_style,
            props.cell_highlight_style,
            None,
            None,
        );
    }
}

fn draw_table_row(
    buffer: &mut FrameBuffer,
    table_rect: Rect,
    row_y: u16,
    row: &ansiq_core::Row,
    column_widths: &[u16],
    column_positions: &[u16],
    alignments: &[ansiq_core::TableAlignment],
    row_style: Style,
    column_highlight_style: Style,
    cell_highlight_style: Style,
    selected_column: Option<usize>,
    selected_cell: Option<(usize, usize)>,
) {
    let content_y = row_y.saturating_add(row.top_margin_value());
    let content_height = row.height_value();
    if content_height == 0 {
        return;
    }

    let mut column_index = 0usize;
    for cell in row.cells_ref() {
        let span = cell.column_span_value().max(1) as usize;
        let Some(&column_x) = column_positions.get(column_index) else {
            column_index = column_index.saturating_add(span);
            continue;
        };
        let x = table_rect.x.saturating_add(column_x);
        let cell_width = table_span_width(
            column_widths,
            column_positions,
            table_rect.width,
            column_index,
            span,
        );
        if cell_width == 0 || x >= table_rect.right() {
            column_index = column_index.saturating_add(span);
            continue;
        }

        let cell_rect = Rect::new(
            x,
            table_rect.y.saturating_add(content_y),
            cell_width.min(table_rect.right().saturating_sub(x)),
            content_height.min(
                table_rect
                    .bottom()
                    .saturating_sub(table_rect.y.saturating_add(content_y)),
            ),
        );
        let mut cell_style = patch_style(row_style, cell.style_value());
        if selected_column
            .map(|selected| (column_index..column_index.saturating_add(span)).contains(&selected))
            .unwrap_or(false)
        {
            cell_style = merge_highlight_style(cell_style, column_highlight_style);
        }
        if let Some((_, selected_column)) = selected_cell {
            if (column_index..column_index.saturating_add(span)).contains(&selected_column) {
                cell_style = merge_highlight_style(cell_style, cell_highlight_style);
            }
        }

        let alignment = cell
            .text()
            .alignment
            .unwrap_or_else(|| table_alignment(alignments.get(column_index).copied()));
        let lines = styled_lines_from_text(cell.text(), cell_style, alignment);
        for (line_index, line) in lines.into_iter().take(content_height as usize).enumerate() {
            draw_styled_line(buffer, cell_rect, line_index as u16, &line, 0);
        }

        column_index = column_index.saturating_add(span);
    }
}

fn table_alignment(alignment: Option<TableAlignment>) -> Alignment {
    match alignment.unwrap_or(TableAlignment::Left) {
        TableAlignment::Left => Alignment::Left,
        TableAlignment::Center => Alignment::Center,
        TableAlignment::Right => Alignment::Right,
    }
}

fn table_row_is_visible(
    rows: &[ansiq_core::Row],
    start: usize,
    target: usize,
    available_height: u16,
) -> bool {
    if target < start {
        return false;
    }

    let mut used = 0u16;
    for (index, row) in rows.iter().enumerate().skip(start) {
        let row_height = row.height_with_margin().max(1);
        if used.saturating_add(row_height) > available_height {
            return false;
        }
        if index == target {
            return true;
        }
        used = used.saturating_add(row_height);
    }

    false
}