lv-tui 0.3.0

A reactive TUI framework for Rust, inspired by Textual and React
Documentation
use crate::geom::{Pos, Rect, Size};
use crate::style::{Border, Color, Style};

/// 单个字符格的样式
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct CellStyle {
    pub fg: Option<Color>,
    pub bg: Option<Color>,
    pub bold: bool,
    pub italic: bool,
    pub underline: bool,
}

/// 单个字符格
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Cell {
    pub symbol: String,
    pub style: CellStyle,
    /// 宽字符的延续格(第二个 cell),不独立渲染
    pub continuation: bool,
}

impl Default for Cell {
    fn default() -> Self {
        Self {
            symbol: " ".to_string(),
            style: CellStyle::default(),
            continuation: false,
        }
    }
}

/// diff 变更操作
#[derive(Debug, Clone)]
pub struct DiffOp {
    pub pos: Pos,
    pub cell: Cell,
}

/// 字符格缓冲区
pub struct Buffer {
    pub size: Size,
    pub cells: Vec<Cell>,
}

impl Buffer {
    /// Creates a new buffer of the given size, filled with default (blank) cells.
    pub fn new(size: Size) -> Self {
        let len = size.width as usize * size.height as usize;
        Self {
            size,
            cells: vec![Cell::default(); len],
        }
    }

    /// Resizes the buffer, discarding all existing content.
    pub fn resize(&mut self, size: Size) {
        self.size = size;
        let len = size.width as usize * size.height as usize;
        self.cells = vec![Cell::default(); len];
    }

    /// Resets every cell in the buffer to its default (blank) state.
    pub fn clear(&mut self) {
        for cell in &mut self.cells {
            *cell = Cell::default();
        }
    }

    /// Returns a mutable reference to the cell at `(x, y)`, or `None` if out of bounds.
    pub fn get_mut(&mut self, x: u16, y: u16) -> Option<&mut Cell> {
        if x >= self.size.width || y >= self.size.height {
            return None;
        }
        let index = y as usize * self.size.width as usize + x as usize;
        self.cells.get_mut(index)
    }

    /// 写入文本到缓冲区(支持宽字符)
    ///
    /// CJK 等宽字符占 2 cell,第二格标记为 continuation。
    pub fn write_text(&mut self, pos: Pos, clip: Rect, text: &str, style: &Style) {
        let y = pos.y;

        if y < clip.y || y >= clip.y.saturating_add(clip.height) {
            return;
        }

        let cell_style = style.into_cell_style();
        let clip_end = clip.x.saturating_add(clip.width);

        let mut x = pos.x;
        for ch in text.chars() {
            let w = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0) as u16;
            if w == 0 {
                continue; // 组合标记,暂不处理
            }

            // 宽字符不能部分在 clip 外
            if x.saturating_add(w) > clip_end {
                break;
            }

            // 跳过 clip 左侧的字符
            if x < clip.x {
                x = x.saturating_add(w);
                continue;
            }

            if let Some(cell) = self.get_mut(x, y) {
                cell.symbol = ch.to_string();
                cell.style = cell_style;
                cell.continuation = false;
            }

            // 宽字符的第二格标记为 continuation
            if w > 1 {
                for i in 1..w {
                    if let Some(cont) = self.get_mut(x + i, y) {
                        cont.symbol = " ".to_string();
                        cont.style = cell_style;
                        cont.continuation = true;
                    }
                }
            }

            x = x.saturating_add(w);
        }
    }

    /// 在指定 rect 边框绘制边框字符
    pub fn draw_border(&mut self, rect: Rect, border: Border, style: &Style) {
        let (tl, t, tr, l, r, bl, b, br) = match border {
            Border::None => return,
            Border::Plain => ("", "", "", "", "", "", "", ""),
            Border::Rounded => ("", "", "", "", "", "", "", ""),
            Border::Double => ("", "", "", "", "", "", "", ""),
        };

        let cell_style = style.into_cell_style();
        let right = rect.x + rect.width.saturating_sub(1);
        let bottom = rect.y + rect.height.saturating_sub(1);

        // 四角
        self.set_cell(rect.x, rect.y, tl, cell_style);
        self.set_cell(right, rect.y, tr, cell_style);
        self.set_cell(rect.x, bottom, bl, cell_style);
        self.set_cell(right, bottom, br, cell_style);

        // 上下边
        for x in (rect.x + 1)..right {
            self.set_cell(x, rect.y, t, cell_style);
            self.set_cell(x, bottom, b, cell_style);
        }

        // 左右边
        for y in (rect.y + 1)..bottom {
            self.set_cell(rect.x, y, l, cell_style);
            self.set_cell(right, y, r, cell_style);
        }
    }

    fn set_cell(&mut self, x: u16, y: u16, symbol: &str, style: CellStyle) {
        if let Some(cell) = self.get_mut(x, y) {
            cell.symbol = symbol.to_string();
            cell.style = style;
        }
    }

    /// 返回所有 cell 的 DiffOp(跳过 continuation)
    pub fn all_ops(&self) -> Vec<DiffOp> {
        let mut ops = Vec::with_capacity(self.cells.len());

        for y in 0..self.size.height {
            for x in 0..self.size.width {
                let idx = y as usize * self.size.width as usize + x as usize;
                if self.cells[idx].continuation {
                    continue;
                }
                ops.push(DiffOp {
                    pos: Pos { x, y },
                    cell: self.cells[idx].clone(),
                });
            }
        }

        ops
    }

    /// 比较两个 buffer,返回所有变更的 DiffOp(跳过 continuation)
    ///
    /// 支持不同尺寸的 buffer 比较,以 `next` 的尺寸为基准输出。
    pub fn diff(&self, next: &Buffer) -> Vec<DiffOp> {
        let mut ops = Vec::new();

        for y in 0..next.size.height {
            for x in 0..next.size.width {
                let next_idx = y as usize * next.size.width as usize + x as usize;
                let next_cell = &next.cells[next_idx];

                if next_cell.continuation {
                    continue;
                }

                let changed = if x < self.size.width && y < self.size.height {
                    let self_idx = y as usize * self.size.width as usize + x as usize;
                    &self.cells[self_idx] != next_cell
                } else {
                    true
                };

                if changed {
                    ops.push(DiffOp {
                        pos: Pos { x, y },
                        cell: next_cell.clone(),
                    });
                }
            }
        }

        ops
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::geom::Size;
    use crate::style::Style;

    #[test]
    fn test_new_buffer() {
        let buf = Buffer::new(Size { width: 3, height: 2 });
        assert_eq!(buf.size.width, 3);
        assert_eq!(buf.size.height, 2);
        assert_eq!(buf.cells.len(), 6);
    }

    #[test]
    fn test_clear() {
        let mut buf = Buffer::new(Size { width: 2, height: 1 });
        buf.get_mut(0, 0).unwrap().symbol = "X".into();
        buf.clear();
        assert_eq!(buf.cells[0].symbol, " ");
    }

    #[test]
    fn test_write_text_clipped() {
        let mut buf = Buffer::new(Size { width: 5, height: 1 });
        buf.write_text(Pos::default(), Rect { x: 0, y: 0, width: 3, height: 1 }, "hello", &Style::default());
        assert_eq!(buf.cells[0].symbol, "h");
        assert_eq!(buf.cells[2].symbol, "l");
        assert_eq!(buf.cells[3].symbol, " "); // clipped
    }

    #[test]
    fn test_diff() {
        let front = Buffer::new(Size { width: 2, height: 1 });
        let mut back = Buffer::new(Size { width: 2, height: 1 });
        back.get_mut(0, 0).unwrap().symbol = "X".into();
        let ops = front.diff(&back);
        assert_eq!(ops.len(), 1);
        assert_eq!(ops[0].pos.x, 0);
    }
}