Skip to main content

lv_tui/widgets/
label.rs

1use crate::component::{Component, EventCx, LayoutCx, MeasureCx};
2use crate::event::Event;
3use crate::geom::{Rect, Size};
4use crate::layout::Constraint;
5use crate::render::RenderCx;
6use crate::style::{Style, TextWrap};
7use crate::text::Text;
8
9/// A text display widget. Accepts any type that converts to [`Text`]:
10/// `&str`, `String`, [`Span`](crate::text::Span), [`Line`](crate::text::Line), etc.
11pub struct Label {
12    text: Text,
13    style: Style,
14    id: Option<String>,
15    class: Option<String>,
16}
17
18impl Label {
19    pub fn new(text: impl Into<Text>) -> Self {
20        Self { text: text.into(), style: Style::default(), id: None, class: None }
21    }
22
23    pub fn style(mut self, style: Style) -> Self { self.style = style; self }
24    pub fn id(mut self, id: impl Into<String>) -> Self { self.id = Some(id.into()); self }
25    pub fn class(mut self, class: impl Into<String>) -> Self { self.class = Some(class.into()); self }
26}
27
28impl Component for Label {
29    fn render(&self, cx: &mut RenderCx) {
30        for line in &self.text.lines {
31            if cx.wrap != TextWrap::None {
32                let full: String = line.spans.iter().map(|s| s.text.as_str()).collect();
33                let s = crate::style_parser::merge_styles(cx.style.clone(), &self.style);
34                cx.set_style(s);
35                cx.line(&full);
36            } else {
37                // Apply alignment offset before rendering spans
38                let total_w: u16 = line.spans.iter().map(|s| s.width()).sum();
39                cx.cursor.x = cx.cursor.x.saturating_add(cx.align_offset(total_w));
40                for span in &line.spans {
41                    // Start with the inherited parent style, layer label style and span style
42                    let base = crate::style_parser::merge_styles(cx.style.clone(), &self.style);
43                    let s = crate::style_parser::merge_styles(base, &span.style);
44                    cx.set_style(s);
45                    cx.text(&span.text);
46                }
47                cx.cursor.y = cx.cursor.y.saturating_add(1);
48                cx.cursor.x = cx.rect.x;
49            }
50        }
51    }
52
53    fn measure(&self, constraint: Constraint, _cx: &mut MeasureCx) -> Size {
54        let natural_w = self.text.max_width();
55        let natural_h = self.text.height() as u16;
56        // If text is wider than available space, calculate wrapped height
57        if constraint.max.width > 0 && natural_w > constraint.max.width {
58            let mut lines: u16 = 0;
59            for line in &self.text.lines {
60                let line_w: u16 = line.spans.iter().map(|s| s.width()).sum();
61                if line_w == 0 { lines += 1; continue; }
62                lines += (line_w + constraint.max.width - 1) / constraint.max.width;
63            }
64            Size { width: constraint.max.width.min(natural_w), height: lines }
65        } else {
66            Size { width: natural_w, height: natural_h }
67        }
68    }
69
70    fn layout(&mut self, _rect: Rect, _cx: &mut LayoutCx) {}
71    fn style(&self) -> Style { self.style.clone() }
72    fn id(&self) -> Option<&str> { self.id.as_deref() }
73    fn class(&self) -> Option<&str> { self.class.as_deref() }
74    fn focusable(&self) -> bool { false }
75    fn event(&mut self, _event: &Event, _cx: &mut EventCx) {}
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use crate::style::Color;
82    use crate::testbuffer::TestBuffer;
83
84    #[test]
85    fn test_basic_render() {
86        let mut tb = TestBuffer::new(20, 1);
87        tb.render(&Label::new("hello"));
88        tb.assert_line(0, "hello");
89    }
90
91    #[test]
92    fn test_multiline() {
93        let mut tb = TestBuffer::new(20, 3);
94        tb.render(&Label::new("line1\nline2\nline3"));
95        tb.assert_text(0, 0, "l");
96        tb.assert_text(0, 1, "l");
97        tb.assert_text(0, 2, "l");
98    }
99
100    #[test]
101    fn test_empty() {
102        let mut tb = TestBuffer::new(20, 1);
103        tb.render(&Label::new(""));
104        assert!(tb.buffer.cells[0].symbol == " ");
105    }
106
107    #[test]
108    fn test_styled_label() {
109        let mut tb = TestBuffer::new(20, 1);
110        tb.render(&Label::new("warn").style(Style::default().fg(Color::Red)));
111        assert_eq!(tb.cell_fg(0, 0), Some(Color::Red));
112    }
113}