htui 0.2.0

A terminal HTML/CSS renderer with an embedded JavaScript engine — build interactive TUIs from HTML, CSS and JS
use crossterm::style::Color;

use crate::{
    graphics::FontStyle,
    layout::{BorderStyle, LayoutBox, Rect},
};

use super::{BackgroundFill, PaintCanvas, background_at};

/// Render the four border edges of a block-level box.
///
/// Border characters are the Unicode box-drawing glyphs (─ │ ┌ ┐ └ ┘) so the
/// rect reads as a continuous frame in the terminal. Each side picks its
/// color from `border.color` if set, otherwise falls back to the box's text
/// color. Borders with width > 1 currently render as a single row/column —
/// terminals cannot express thicker edges without mis-aligning the inner
/// content area, so any width > 1 is treated as 1.
///
/// `clip` bounds where glyphs may land. Edge / corner positions always come
/// from the box's own `r`: a box that overflows a clipping ancestor has its
/// out-of-clip glyphs *dropped*, never relocated — shrinking the rect before
/// painting would drag the bottom edge up to the clip boundary and fabricate
/// a border row on top of whatever else paints there.
pub(crate) fn paint_borders(
    lb: &LayoutBox,
    r: Rect,
    canvas: &mut PaintCanvas<'_>,
    fg: Option<Color>,
    bg: Option<BackgroundFill<'_>>,
    clip: Rect,
) {
    let sides = &lb.paint_style.box_edges.border;
    if !sides.any_visible() {
        return;
    }
    if r.width == 0 || r.height == 0 {
        return;
    }

    let cols = canvas.columns as usize;
    let cell_start = canvas.cell_index(r.x, r.y);
    let max_cells = canvas.layout.len();

    // Whether the canvas cell at absolute (x, y) may receive a border glyph.
    let in_clip = |x: usize, y: usize| -> bool {
        x >= clip.x
            && y >= clip.y
            && x < clip.x.saturating_add(clip.width)
            && y < clip.y.saturating_add(clip.height)
    };

    // Effective border thickness per side, clamped to the rect size and
    // collapsed to 1 — see function-level note.
    let top_t = sides.top.thickness().clamp(0, r.height).min(1);
    let bottom_t = sides.bottom.thickness().clamp(0, r.height).min(1);
    let left_t = sides.left.thickness().clamp(0, r.width).min(1);
    let right_t = sides.right.thickness().clamp(0, r.width).min(1);

    // Edge colors: each side has its own color, falling back to the box fg.
    let top_color = sides.top.color.or(fg);
    let bottom_color = sides.bottom.color.or(fg);
    let left_color = sides.left.color.or(fg);
    let right_color = sides.right.color.or(fg);

    // Borders use no font style flags; their glyphs are the box-drawing set.
    let style = FontStyle::empty();

    let right_col_start = r.width.saturating_sub(right_t);

    // Top edge: rows 0..top_t, columns from left_t to right_col_start.
    if sides.top.style == BorderStyle::Solid && top_t > 0 {
        for row_offset in 0..top_t {
            let row_start = cell_start + row_offset * cols;
            for col in left_t..right_col_start {
                let idx = row_start + col;
                if idx < max_cells && canvas.in_region(idx) && in_clip(r.x + col, r.y + row_offset)
                {
                    // In-place: capsule-reuse the 4-byte buffer from
                    // `Block::default()` to push `─` (3 bytes) without
                    // reallocating.
                    let cell = &mut canvas.layout[idx];
                    cell.word.clear();
                    cell.word.push('─');
                    cell.fcolor = top_color;
                    cell.bcolor = background_at(bg, idx % cols, idx / cols);
                    cell.style = style;
                }
            }
        }
    }

    // Bottom edge: rows (height-bottom_t)..height, columns left_t..right_col_start.
    if sides.bottom.style == BorderStyle::Solid && bottom_t > 0 {
        let bottom_row = cell_start + (r.height - bottom_t) * cols;
        let bottom_y = r.y + r.height - bottom_t;
        for row_offset in 0..bottom_t {
            let row_start = bottom_row + row_offset * cols;
            for col in left_t..right_col_start {
                let idx = row_start + col;
                if idx < max_cells
                    && canvas.in_region(idx)
                    && in_clip(r.x + col, bottom_y + row_offset)
                {
                    let cell = &mut canvas.layout[idx];
                    cell.word.clear();
                    cell.word.push('─');
                    cell.fcolor = bottom_color;
                    cell.bcolor = background_at(bg, idx % cols, idx / cols);
                    cell.style = style;
                }
            }
        }
    }

    // Left edge: rows top_t..(height-bottom_t), columns 0..left_t.
    if sides.left.style == BorderStyle::Solid && left_t > 0 {
        for row_offset in top_t..r.height.saturating_sub(bottom_t) {
            let row_start = cell_start + row_offset * cols;
            for col_offset in 0..left_t {
                let idx = row_start + col_offset;
                if idx < max_cells
                    && canvas.in_region(idx)
                    && in_clip(r.x + col_offset, r.y + row_offset)
                {
                    let cell = &mut canvas.layout[idx];
                    cell.word.clear();
                    cell.word.push('│');
                    cell.fcolor = left_color;
                    cell.bcolor = background_at(bg, idx % cols, idx / cols);
                    cell.style = style;
                }
            }
        }
    }

    // Right edge: rows top_t..(height-bottom_t), columns right_col_start..width.
    if sides.right.style == BorderStyle::Solid && right_t > 0 {
        for row_offset in top_t..r.height.saturating_sub(bottom_t) {
            let row_start = cell_start + row_offset * cols;
            for col_offset in 0..right_t {
                let idx = row_start + right_col_start + col_offset;
                if idx < max_cells
                    && canvas.in_region(idx)
                    && in_clip(r.x + right_col_start + col_offset, r.y + row_offset)
                {
                    let cell = &mut canvas.layout[idx];
                    cell.word.clear();
                    cell.word.push('│');
                    cell.fcolor = right_color;
                    cell.bcolor = background_at(bg, idx % cols, idx / cols);
                    cell.style = style;
                }
            }
        }
    }

    // Corners: overwrite the 4 corner cells so the edges join cleanly. A
    // corner belongs to the pair of sides meeting there, so it only paints
    // when both are visible — a `border-left`-only box must not grow
    // fabricated `┌` / `└` corner glyphs (the browser draws just the edge).
    // `border-radius: …` (any non-zero corner) swaps the square glyphs for
    // the rounded box-drawing set; radii larger than 1 render identically
    // to radius 1 because the terminal grid has no finer rounding primitive.
    let corner_chars = if lb.paint_style.border_radius.is_rounded() {
        ['╭', '╮', '╰', '╯']
    } else {
        ['┌', '┐', '└', '┘']
    };
    let corner_positions: [(usize, usize); 4] = [
        (0, 0),
        (right_col_start, 0),
        (0, r.height - bottom_t),
        (right_col_start, r.height - bottom_t),
    ];
    let corner_colors = [
        top_color.or(left_color),
        top_color.or(right_color),
        bottom_color.or(left_color),
        bottom_color.or(right_color),
    ];
    let top_visible = sides.top.style == BorderStyle::Solid && top_t > 0;
    let bottom_visible = sides.bottom.style == BorderStyle::Solid && bottom_t > 0;
    let left_visible = sides.left.style == BorderStyle::Solid && left_t > 0;
    let right_visible = sides.right.style == BorderStyle::Solid && right_t > 0;
    let corner_visible = [
        top_visible && left_visible,
        top_visible && right_visible,
        bottom_visible && left_visible,
        bottom_visible && right_visible,
    ];
    for i in 0..4 {
        if !corner_visible[i] {
            continue;
        }
        let ch = corner_chars[i];
        let (cx, cy) = corner_positions[i];
        let color = corner_colors[i];
        let idx = cell_start + cy * cols + cx;
        if idx < max_cells && canvas.in_region(idx) && in_clip(r.x + cx, r.y + cy) {
            let cell = &mut canvas.layout[idx];
            cell.word.clear();
            cell.word.push(ch);
            cell.fcolor = color;
            cell.bcolor = background_at(bg, idx % cols, idx / cols);
            cell.style = style;
        }
    }

    // Corners / edges fully painted above; nothing more to do.
}