lv-tui 0.1.1

A reactive TUI framework for Rust, inspired by Textual and React
Documentation
use std::cell::Cell;
use std::sync::atomic::{AtomicU64, Ordering};

use crate::buffer::Buffer;
use crate::component::{Component, EventCx, LayoutCx, MeasureCx};
use crate::dirty::Dirty;
use crate::event::{Event, EventPhase};
use crate::geom::{Pos, Rect, Size};
use crate::layout::Constraint;
use crate::render::RenderCx;
use crate::style::{Style, TextAlign, TextTruncate, TextWrap};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct NodeId(u64);

impl NodeId {
    pub fn new() -> Self {
        static NEXT: AtomicU64 = AtomicU64::new(1);
        Self(NEXT.fetch_add(1, Ordering::Relaxed))
    }
    pub const ROOT: NodeId = NodeId(0);
}

pub struct Node {
    pub id: NodeId,
    pub rect: Cell<Rect>,
    pub dirty: Dirty,
    pub parent: Option<NodeId>,
    pub component: Box<dyn Component>,
    pub children: Vec<Node>,
}

impl Node {
    pub fn rect(&self) -> Rect { self.rect.get() }
    pub fn set_rect(&self, r: Rect) { self.rect.set(r); }

    pub fn new(component: impl Component + 'static) -> Self {
        Self {
            id: NodeId::new(),
            rect: Cell::new(Rect::default()),
            dirty: Dirty::NONE,
            parent: None,
            component: Box::new(component),
            children: Vec::new(),
        }
    }

    pub fn root(component: impl Component + 'static) -> Self {
        Self {
            id: NodeId::ROOT,
            rect: Cell::new(Rect::default()),
            dirty: Dirty::NONE,
            parent: None,
            component: Box::new(component),
            children: Vec::new(),
        }
    }

    pub fn add_child(&mut self, child: Node) { self.children.push(child); }

    pub fn render(&self, buffer: &mut Buffer, focused_id: Option<NodeId>) {
        self.render_with_clip(buffer, focused_id, None);
    }

    pub fn render_with_clip(&self, buffer: &mut Buffer, focused_id: Option<NodeId>, clip_rect: Option<Rect>) {
        self.render_with_clip_and_wrap(buffer, focused_id, clip_rect, TextWrap::None, TextTruncate::None, TextAlign::Left);
    }

    pub fn render_with_clip_and_wrap(
        &self,
        buffer: &mut Buffer,
        focused_id: Option<NodeId>,
        clip_rect: Option<Rect>,
        wrap: crate::style::TextWrap,
        truncate: crate::style::TextTruncate,
        align: crate::style::TextAlign,
    ) {
        self.render_inner(buffer, focused_id, clip_rect, wrap, truncate, align, None, None);
    }

    /// 带父样式继承的渲染
    pub fn render_with_parent(
        &self,
        buffer: &mut Buffer,
        focused_id: Option<NodeId>,
        clip_rect: Option<Rect>,
        wrap: crate::style::TextWrap,
        truncate: crate::style::TextTruncate,
        align: crate::style::TextAlign,
        parent_style: Option<&crate::style::Style>,
    ) {
        self.render_inner(buffer, focused_id, clip_rect, wrap, truncate, align, None, parent_style);
    }

    /// 带样式表的渲染
    pub fn render_inner(
        &self,
        buffer: &mut Buffer,
        focused_id: Option<NodeId>,
        clip_rect: Option<Rect>,
        wrap: crate::style::TextWrap,
        truncate: crate::style::TextTruncate,
        align: crate::style::TextAlign,
        sheet: Option<&crate::style_parser::StyleSheet>,
        parent_style: Option<&Style>,
    ) {
        let mut style = self.component.style();
        if let Some(p) = parent_style {
            style = crate::style_parser::inherit_style(p, &style);
        }
        if let Some(s) = sheet {
            let r = s.resolve(self.component.type_name(), self.component.id(), self.component.class());
            style = crate::style_parser::merge_styles(r, &style);
        }
        let mut cx = RenderCx::new(self.rect(), buffer, style);
        cx.focused_id = focused_id;
        cx.clip_rect = clip_rect;
        cx.wrap = wrap;
        cx.truncate = truncate;
        cx.align = align;
        self.component.render(&mut cx);
    }

    pub fn event(&mut self, event: &Event, global_dirty: &mut Dirty, quit: &mut bool, task_tx: Option<std::sync::mpsc::Sender<String>>) {
        let mut stopped = false;
        let mut cx = EventCx::with_task_sender(&mut self.dirty, global_dirty, quit, EventPhase::Target, &mut stopped, task_tx.clone());
        self.component.update(&mut cx);
        self.component.event(event, &mut cx);
        if !stopped {
            for child in &mut self.children { child.event(event, global_dirty, quit, task_tx.clone()); }
        }
    }

    pub fn measure(&self, constraint: Constraint) -> Size {
        let mut cx = MeasureCx { constraint };
        self.component.measure(constraint, &mut cx)
    }

    /// 递归调用 mount
    pub fn mount(&mut self, global_dirty: &mut Dirty, quit: &mut bool, task_tx: Option<std::sync::mpsc::Sender<String>>) {
        let mut stopped = false;
        let mut cx = EventCx::with_task_sender(&mut self.dirty, global_dirty, quit, EventPhase::Target, &mut stopped, task_tx.clone());
        self.component.mount(&mut cx);
        for child in &mut self.children { child.mount(global_dirty, quit, task_tx.clone()); }
    }

    /// 调试渲染:递归绘制组件边框和类型标签
    pub fn debug_render(&self, buffer: &mut Buffer, focused_id: Option<NodeId>) {
        let type_name = self.component.type_name();
        // 取最后一段(去掉 crate 前缀)
        let short = type_name.rsplit("::").next().unwrap_or(type_name);

        let border = if focused_id == Some(self.id) {
            crate::style::Border::Double
        } else {
            crate::style::Border::Rounded
        };
        let style = crate::style::Style::default().fg(crate::style::Color::Yellow);
        buffer.draw_border(self.rect(), border, &style);

        // 短标签
        buffer.write_text(
            Pos { x: self.rect().x.saturating_add(2), y: self.rect().y },
            self.rect(),
            short,
            &style,
        );

        self.component.for_each_child(&mut |child: &Node| {
            child.debug_render(buffer, focused_id);
        });
    }

    /// 递归调用 unmount (reverse order)
    pub fn unmount(&mut self, global_dirty: &mut Dirty, quit: &mut bool, task_tx: Option<std::sync::mpsc::Sender<String>>) {
        for child in &mut self.children { child.unmount(global_dirty, quit, task_tx.clone()); }
        let mut stopped = false;
        let mut cx = EventCx::with_task_sender(&mut self.dirty, global_dirty, quit, EventPhase::Target, &mut stopped, task_tx);
        self.component.unmount(&mut cx);
    }

    pub fn layout(&mut self, rect: Rect) {
        self.set_rect(rect);
        for child in &mut self.children { child.parent = Some(self.id); }
        let mut cx = LayoutCx::new(&mut self.children);
        self.component.layout(rect, &mut cx);
    }

    pub fn find_path_to(&self, target_id: NodeId) -> Option<Vec<NodeId>> {
        if self.id == target_id { return Some(vec![self.id]); }
        let mut result: Option<Vec<NodeId>> = None;
        self.component.for_each_child(&mut |child: &Node| {
            if result.is_none() {
                if let Some(child_path) = child.find_path_to(target_id) {
                    let mut full = vec![self.id];
                    full.extend(child_path);
                    result = Some(full);
                }
            }
        });
        result
    }

    pub fn any_needs_paint(&self) -> bool {
        self.dirty.contains(Dirty::PAINT) || self.children.iter().any(|c| c.any_needs_paint())
    }

    pub fn any_needs_layout(&self) -> bool {
        self.dirty.contains(Dirty::LAYOUT) || self.children.iter().any(|c| c.any_needs_layout())
    }

    pub fn clear_dirty(&mut self) {
        self.dirty = Dirty::NONE;
    }

    pub fn collect_focusable(&self, ids: &mut Vec<NodeId>) {
        if self.component.focusable() { ids.push(self.id); }
        self.component.for_each_child(&mut |child: &Node| child.collect_focusable(ids));
    }

    pub fn hit_test(&self, pos: Pos) -> Option<NodeId> {
        let mut hit: Option<NodeId> = None;
        self.component.for_each_child(&mut |child: &Node| {
            if let Some(id) = child.hit_test(pos) { hit = Some(id); }
        });
        if hit.is_none() && self.rect().contains(pos) { hit = Some(self.id); }
        hit
    }

    pub fn send_event(&mut self, target_id: NodeId, event: &Event, global_dirty: &mut Dirty, quit: &mut bool, phase: EventPhase, stopped: &mut bool, task_tx: Option<std::sync::mpsc::Sender<String>>) -> bool {
        if *stopped { return true; }
        if self.id == target_id {
            let mut cx = EventCx::with_task_sender(&mut self.dirty, global_dirty, quit, phase, stopped, task_tx.clone());
            self.component.event(event, &mut cx);
            return true;
        }
        let mut found = false;
        self.component.for_each_child_mut(&mut |child: &mut Node| {
            if !found && !*stopped {
                found = child.send_event(target_id, event, global_dirty, quit, phase, stopped, task_tx.clone());
            }
        });
        found
    }
}