lv-tui 0.4.0

A reactive TUI framework for Rust
Documentation
use crate::component::{Component, EventCx, LayoutCx, MeasureCx};
use crate::event::Event;
use crate::geom::{Rect, Size};
use crate::layout::Constraint;
use crate::render::RenderCx;
use crate::style::{Style, TextWrap};
use crate::text::Text;

/// A text display widget. Accepts any type that converts to [`Text`]:
/// `&str`, `String`, [`Span`](crate::text::Span), [`Line`](crate::text::Line), etc.
pub struct Label {
    text: Text,
    style: Style,
    id: Option<String>,
    class: Option<String>,
}

impl Label {
    pub fn new(text: impl Into<Text>) -> Self {
        Self { text: text.into(), style: Style::default(), id: None, class: None }
    }

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

impl Component for Label {
    fn render(&self, cx: &mut RenderCx) {
        for line in &self.text.lines {
            if cx.wrap != TextWrap::None {
                let full: String = line.spans.iter().map(|s| s.text.as_str()).collect();
                let s = crate::style_parser::merge_styles(cx.style.clone(), &self.style);
                cx.set_style(s);
                cx.line(&full);
            } else {
                // Apply alignment offset before rendering spans
                let total_w: u16 = line.spans.iter().map(|s| s.width()).sum();
                cx.cursor.x = cx.cursor.x.saturating_add(cx.align_offset(total_w));
                for span in &line.spans {
                    // Start with the inherited parent style, layer label style and span style
                    let base = crate::style_parser::merge_styles(cx.style.clone(), &self.style);
                    let s = crate::style_parser::merge_styles(base, &span.style);
                    cx.set_style(s);
                    cx.text(&span.text);
                }
                cx.cursor.y = cx.cursor.y.saturating_add(1);
                cx.cursor.x = cx.rect.x;
            }
        }
    }

    fn measure(&self, constraint: Constraint, _cx: &mut MeasureCx) -> Size {
        let natural_w = self.text.max_width();
        let natural_h = self.text.height() as u16;
        // If text is wider than available space, calculate wrapped height
        if constraint.max.width > 0 && natural_w > constraint.max.width {
            let mut lines: u16 = 0;
            for line in &self.text.lines {
                let line_w: u16 = line.spans.iter().map(|s| s.width()).sum();
                if line_w == 0 { lines += 1; continue; }
                lines += (line_w + constraint.max.width - 1) / constraint.max.width;
            }
            Size { width: constraint.max.width.min(natural_w), height: lines }
        } else {
            Size { width: natural_w, height: natural_h }
        }
    }

    fn layout(&mut self, _rect: Rect, _cx: &mut LayoutCx) {}
    fn style(&self) -> Style { self.style.clone() }
    fn id(&self) -> Option<&str> { self.id.as_deref() }
    fn class(&self) -> Option<&str> { self.class.as_deref() }
    fn focusable(&self) -> bool { false }
    fn event(&mut self, _event: &Event, _cx: &mut EventCx) {}
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::style::Color;
    use crate::testbuffer::TestBuffer;

    #[test]
    fn test_basic_render() {
        let mut tb = TestBuffer::new(20, 1);
        tb.render(&Label::new("hello"));
        tb.assert_line(0, "hello");
    }

    #[test]
    fn test_multiline() {
        let mut tb = TestBuffer::new(20, 3);
        tb.render(&Label::new("line1\nline2\nline3"));
        tb.assert_text(0, 0, "l");
        tb.assert_text(0, 1, "l");
        tb.assert_text(0, 2, "l");
    }

    #[test]
    fn test_empty() {
        let mut tb = TestBuffer::new(20, 1);
        tb.render(&Label::new(""));
        assert!(tb.buffer.cells[0].symbol == " ");
    }

    #[test]
    fn test_styled_label() {
        let mut tb = TestBuffer::new(20, 1);
        tb.render(&Label::new("warn").style(Style::default().fg(Color::Red)));
        assert_eq!(tb.cell_fg(0, 0), Some(Color::Red));
    }
}