lv-tui 0.1.1

A reactive TUI framework for Rust, inspired by Textual and React
Documentation
use crate::buffer::Buffer;
use crate::geom::{Pos, Rect};
use crate::node::NodeId;
use crate::style::{Border, Style, TextAlign, TextTruncate, TextWrap};

/// 渲染上下文——组件通过它输出内容到缓冲区
pub struct RenderCx<'a> {
    pub rect: Rect,
    pub buffer: &'a mut Buffer,
    pub cursor: Pos,
    pub style: Style,
    /// 当前焦点节点
    pub focused_id: Option<NodeId>,
    /// 裁剪区域(覆盖 rect 的默认裁剪,用于 Scroll 等)
    pub clip_rect: Option<Rect>,
    /// 文本换行模式
    pub wrap: TextWrap,
    /// 文本截断模式
    pub truncate: TextTruncate,
    /// 文本对齐
    pub align: TextAlign,
}

impl<'a> RenderCx<'a> {
    pub fn new(rect: Rect, buffer: &'a mut Buffer, style: Style) -> Self {
        let cursor = Pos {
            x: rect.x,
            y: rect.y,
        };
        Self {
            rect,
            buffer,
            cursor,
            style,
            focused_id: None,
            clip_rect: None,
            wrap: TextWrap::None,
            truncate: TextTruncate::None,
            align: TextAlign::Left,
        }
    }

    /// 检查指定节点是否为当前焦点
    pub fn is_focused(&self, id: NodeId) -> bool {
        self.focused_id == Some(id)
    }

    /// 计算对齐偏移(根据当前 align 设置和有效裁剪区域)
    pub fn align_offset(&self, text_width: u16) -> u16 {
        let clip = self.effective_clip();
        let available = clip.x.saturating_add(clip.width).saturating_sub(self.rect.x);
        match self.align {
            TextAlign::Left => 0,
            TextAlign::Center => available.saturating_sub(text_width) / 2,
            TextAlign::Right => available.saturating_sub(text_width),
        }
    }

    /// 写入文本使用的实际裁剪区域
    fn effective_clip(&self) -> Rect {
        self.clip_rect.unwrap_or(self.rect)
    }

    /// 换行时使用的裁剪(放宽垂直限制)
    fn wrap_clip(&self) -> Rect {
        let mut clip = self.effective_clip();
        if self.wrap == TextWrap::Char {
            clip.height = u16::MAX.saturating_sub(clip.y); // 垂直不裁剪
        }
        clip
    }

    /// 在 cursor 位置写入文本,推进 x(考虑宽字符和截断)
    pub fn text(&mut self, text: impl AsRef<str>) {
        let text = text.as_ref();
        let clip = self.effective_clip();
        let available = clip.x.saturating_add(clip.width).saturating_sub(self.cursor.x);

        if self.wrap == TextWrap::None && self.truncate == TextTruncate::Ellipsis {
            let total: u16 = text.chars().map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as u16).sum();
            if total > available && available >= 1 {
                let mut used: u16 = 0;
                let mut bytes = 0;
                for ch in text.chars() {
                    let w = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0) as u16;
                    if used + w > available.saturating_sub(1) { break; }
                    used += w;
                    bytes += ch.len_utf8();
                }
                if bytes > 0 {
                    self.buffer.write_text(self.cursor, clip, &text[..bytes], &self.style);
                    self.cursor.x = self.cursor.x.saturating_add(used);
                }
                self.buffer.write_text(self.cursor, clip, "", &self.style);
                self.cursor.x = self.cursor.x.saturating_add(1);
                return;
            }
        }

        self.buffer.write_text(self.cursor, clip, text, &self.style);
        let width: u16 = text.chars().map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as u16).sum();
        self.cursor.x = self.cursor.x.saturating_add(width);
    }

    /// 写入文本后将 cursor 移到下一行行首(支持换行和截断)
    pub fn line(&mut self, text: impl AsRef<str>) {
        let text = text.as_ref();
        let clip = self.wrap_clip();
        let available = if clip.width >= self.cursor.x.saturating_sub(clip.x) {
            clip.width.saturating_sub(self.cursor.x.saturating_sub(clip.x))
        } else {
            0
        };

        let saved_clip = self.clip_rect;
        if self.wrap == TextWrap::Char {
            self.clip_rect = Some(clip); // 换行时放宽垂直裁剪
        }

        match self.wrap {
            TextWrap::None => {
                let tw: u16 = text.chars().map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as u16).sum();
                self.cursor.x = self.rect.x.saturating_add(self.align_offset(tw));
                match self.truncate {
                    TextTruncate::None => {
                        self.text(text);
                    }
                    TextTruncate::Ellipsis => {
                        let total = tw;
                        if total > available && available >= 1 {
                            let mut used: u16 = 0;
                            let mut bytes = 0;
                            for ch in text.chars() {
                                let w = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0) as u16;
                                if used + w > available.saturating_sub(1) { break; }
                                used += w;
                                bytes += ch.len_utf8();
                            }
                            if bytes > 0 {
                                self.text(&text[..bytes]);
                            }
                            self.text("");
                        } else {
                            self.text(text);
                        }
                    }
                }
                self.cursor.y = self.cursor.y.saturating_add(1);
                self.cursor.x = self.rect.x;
            }
            TextWrap::Char => {
                let mut remaining = text;
                loop {
                    let line_widths: Vec<(usize, u16)> = remaining
                        .char_indices()
                        .map(|(i, c)| (i, unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as u16))
                        .filter(|&(_, w)| w > 0)
                        .collect();

                    let mut used: u16 = 0;
                    let mut bytes = 0;
                    for &(byte_idx, w) in &line_widths {
                        if used + w > available { break; }
                        used += w;
                        bytes = byte_idx + remaining[byte_idx..].chars().next().map(|c| c.len_utf8()).unwrap_or(0);
                    }
                    if bytes == 0 { break; }
                    let line_text = &remaining[..bytes];
                    let lw: u16 = line_text.chars().map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as u16).sum();
                    self.cursor.x = self.rect.x.saturating_add(self.align_offset(lw));
                    self.text(line_text);
                    remaining = &remaining[bytes..];
                    if remaining.is_empty() { break; }
                    self.cursor.y = self.cursor.y.saturating_add(1);
                    self.cursor.x = self.rect.x;
                }
                if text.is_empty() {
                    self.cursor.y = self.cursor.y.saturating_add(1);
                    self.cursor.x = self.rect.x;
                }
            }
        }

        self.clip_rect = saved_clip;
    }

    /// 修改当前渲染样式
    pub fn set_style(&mut self, style: Style) {
        self.style = style;
    }

    /// 绘制边框(使用当前样式,遵循 clip_rect)
    pub fn draw_border(&mut self, border: Border) {
        let clip = self.effective_clip();
        self.buffer.draw_border(clip, border, &self.style);
    }
}