lv-tui 0.2.0

A reactive TUI framework for Rust, inspired by Textual and React
Documentation
use crate::component::{Component, EventCx, LayoutCx, MeasureCx};
use crate::event::Event;
use crate::geom::{Insets, Pos, Rect, Size};
use crate::layout::Constraint;
use crate::node::Node;
use crate::render::RenderCx;
use crate::style::{Border, Style};

/// 边框 + 内边距包装容器
pub struct Block {
    child: Option<Node>,
    style: Style,
    title: Option<String>,
}

impl Block {
    pub fn new(child: impl Component + 'static) -> Self {
        Self {
            child: Some(Node::new(child)),
            style: Style::default(),
            title: None,
        }
    }

    pub fn border(mut self, border: Border) -> Self {
        self.style = self.style.border(border);
        self
    }

    pub fn padding(mut self, value: u16) -> Self {
        self.style = self.style.padding(value);
        self
    }

    pub fn title(mut self, title: impl Into<String>) -> Self {
        self.title = Some(title.into());
        self
    }

    pub fn style(mut self, style: Style) -> Self {
        self.style = style;
        self
    }
}

impl Component for Block {
    fn render(&self, cx: &mut RenderCx) {
        cx.draw_border(self.style.border);

        // 边框标题(top-left corner 右侧)
        if let Some(title) = &self.title {
            let pos = Pos { x: cx.rect.x.saturating_add(2), y: cx.rect.y };
            cx.buffer.write_text(pos, cx.rect, title, &cx.style);
        }

        if let Some(child) = &self.child {
            child.render_with_parent(cx.buffer, cx.focused_id, cx.clip_rect, cx.wrap, cx.truncate, cx.align, Some(&cx.style));
        }
    }

    fn for_each_child(&self, f: &mut dyn FnMut(&Node)) {
        if let Some(child) = &self.child {
            f(child);
        }
    }

    fn for_each_child_mut(&mut self, f: &mut dyn FnMut(&mut Node)) {
        if let Some(child) = &mut self.child {
            f(child);
        }
    }

    fn measure(&self, constraint: Constraint, _cx: &mut MeasureCx) -> Size {
        let pad = self.effective_padding();
        let child_constraint = Constraint {
            min: Size::default(),
            max: Size {
                width: constraint.max.width.saturating_sub(pad.left + pad.right),
                height: constraint.max.height.saturating_sub(pad.top + pad.bottom),
            },
        };

        let child_size = self
            .child
            .as_ref()
            .map(|c| c.measure(child_constraint))
            .unwrap_or_default();

        Size {
            width: child_size.width.saturating_add(pad.left + pad.right),
            height: child_size.height.saturating_add(pad.top + pad.bottom),
        }
    }

    fn focusable(&self) -> bool {
        false
    }

    fn event(&mut self, event: &Event, cx: &mut EventCx) {
        if matches!(event, Event::Focus | Event::Blur | Event::Tick) {
            return;
        }

        if let Some(child) = &mut self.child {
            let mut child_cx = EventCx::with_task_sender(&mut child.dirty, cx.global_dirty, cx.quit, cx.phase, cx.propagation_stopped, cx.task_sender.clone());
            child.component.event(event, &mut child_cx);
        }
    }

    fn layout(&mut self, rect: Rect, _cx: &mut LayoutCx) {
        let inner = rect.inner(self.effective_padding());
        if let Some(child) = &mut self.child {
            child.layout(inner);
        }
    }

    fn style(&self) -> Style {
        self.style.clone()
    }
}

impl Block {
    fn effective_padding(&self) -> Insets {
        let border_width: u16 = match self.style.border {
            Border::None => 0,
            _ => 1,
        };
        Insets {
            top: self.style.padding.top + border_width,
            right: self.style.padding.right + border_width,
            bottom: self.style.padding.bottom + border_width,
            left: self.style.padding.left + border_width,
        }
    }
}