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
9pub 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 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 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 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}