1use crate::buffer::ScreenBuffer;
4use crate::cell::Cell;
5use crate::geometry::Rect;
6use crate::style::Style;
7use unicode_width::UnicodeWidthStr;
8
9use super::Widget;
10
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
13pub enum Alignment {
14 #[default]
16 Left,
17 Center,
19 Right,
21}
22
23#[derive(Clone, Debug)]
25pub struct Label {
26 text: String,
28 style: Style,
30 alignment: Alignment,
32}
33
34impl Label {
35 pub fn new(text: impl Into<String>) -> Self {
37 Self {
38 text: text.into(),
39 style: Style::default(),
40 alignment: Alignment::Left,
41 }
42 }
43
44 #[must_use]
46 pub fn style(mut self, style: Style) -> Self {
47 self.style = style;
48 self
49 }
50
51 #[must_use]
53 pub fn alignment(mut self, alignment: Alignment) -> Self {
54 self.alignment = alignment;
55 self
56 }
57
58 pub fn text(&self) -> &str {
60 &self.text
61 }
62
63 pub fn set_text(&mut self, text: impl Into<String>) {
65 self.text = text.into();
66 }
67
68 pub fn style_ref(&self) -> &Style {
70 &self.style
71 }
72
73 pub fn set_style(&mut self, style: Style) {
75 self.style = style;
76 }
77
78 pub fn alignment_value(&self) -> Alignment {
80 self.alignment
81 }
82
83 pub fn set_alignment(&mut self, alignment: Alignment) {
85 self.alignment = alignment;
86 }
87}
88
89impl Widget for Label {
90 fn render(&self, area: Rect, buf: &mut ScreenBuffer) {
91 if area.size.width == 0 || area.size.height == 0 {
92 return;
93 }
94
95 let width = usize::from(area.size.width);
96 let text_width = UnicodeWidthStr::width(self.text.as_str());
97
98 if self.style.bg.is_some() {
101 for x in 0..area.size.width {
102 buf.set(
103 area.position.x + x,
104 area.position.y,
105 Cell::new(" ", self.style.clone()),
106 );
107 }
108 }
109
110 let display_text = if text_width > width {
112 truncate_with_ellipsis(&self.text, width)
113 } else {
114 self.text.clone()
115 };
116
117 let display_width = UnicodeWidthStr::width(display_text.as_str());
118
119 let offset = match self.alignment {
121 Alignment::Left => 0,
122 Alignment::Center => (width.saturating_sub(display_width)) / 2,
123 Alignment::Right => width.saturating_sub(display_width),
124 };
125
126 let mut col = 0usize;
128 for ch in display_text.chars() {
129 let ch_width = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0);
130 let x = area.position.x + (offset + col) as u16;
131 if x >= area.position.x + area.size.width {
132 break;
133 }
134 buf.set(
135 x,
136 area.position.y,
137 Cell::new(ch.to_string(), self.style.clone()),
138 );
139 col += ch_width;
140 }
141 }
142}
143
144fn truncate_with_ellipsis(text: &str, max_width: usize) -> String {
146 if max_width == 0 {
147 return String::new();
148 }
149 if max_width == 1 {
150 return "\u{2026}".to_string(); }
152
153 let target = max_width - 1; let mut result = String::new();
155 let mut current_width = 0usize;
156
157 for ch in text.chars() {
158 let ch_width = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0);
159 if current_width + ch_width > target {
160 break;
161 }
162 result.push(ch);
163 current_width += ch_width;
164 }
165 result.push('\u{2026}'); result
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172 use crate::color::{Color, NamedColor};
173 use crate::geometry::Size;
174
175 #[test]
176 fn label_renders_left_aligned() {
177 let label = Label::new("Hello");
178 let mut buf = ScreenBuffer::new(Size::new(10, 1));
179 label.render(Rect::new(0, 0, 10, 1), &mut buf);
180 assert_eq!(buf.get(0, 0).map(|c| c.grapheme.as_str()), Some("H"));
181 assert_eq!(buf.get(4, 0).map(|c| c.grapheme.as_str()), Some("o"));
182 }
183
184 #[test]
185 fn label_renders_center_aligned() {
186 let label = Label::new("Hi").alignment(Alignment::Center);
187 let mut buf = ScreenBuffer::new(Size::new(10, 1));
188 label.render(Rect::new(0, 0, 10, 1), &mut buf);
189 assert_eq!(buf.get(4, 0).map(|c| c.grapheme.as_str()), Some("H"));
191 assert_eq!(buf.get(5, 0).map(|c| c.grapheme.as_str()), Some("i"));
192 }
193
194 #[test]
195 fn label_renders_right_aligned() {
196 let label = Label::new("Hi").alignment(Alignment::Right);
197 let mut buf = ScreenBuffer::new(Size::new(10, 1));
198 label.render(Rect::new(0, 0, 10, 1), &mut buf);
199 assert_eq!(buf.get(8, 0).map(|c| c.grapheme.as_str()), Some("H"));
201 assert_eq!(buf.get(9, 0).map(|c| c.grapheme.as_str()), Some("i"));
202 }
203
204 #[test]
205 fn label_truncates_with_ellipsis() {
206 let label = Label::new("Hello, World!");
207 let mut buf = ScreenBuffer::new(Size::new(8, 1));
208 label.render(Rect::new(0, 0, 8, 1), &mut buf);
209 assert_eq!(buf.get(0, 0).map(|c| c.grapheme.as_str()), Some("H"));
211 assert_eq!(buf.get(7, 0).map(|c| c.grapheme.as_str()), Some("\u{2026}"));
212 }
213
214 #[test]
215 fn label_with_style() {
216 let style = Style::new().fg(Color::Named(NamedColor::Red));
217 let label = Label::new("X").style(style.clone());
218 let mut buf = ScreenBuffer::new(Size::new(5, 1));
219 label.render(Rect::new(0, 0, 5, 1), &mut buf);
220 assert_eq!(buf.get(0, 0).map(|c| &c.style), Some(&style));
221 }
222
223 #[test]
224 fn label_empty_area() {
225 let label = Label::new("test");
226 let mut buf = ScreenBuffer::new(Size::new(10, 1));
227 label.render(Rect::new(0, 0, 0, 1), &mut buf);
229 }
230
231 #[test]
232 fn label_set_text() {
233 let mut label = Label::new("before");
234 assert_eq!(label.text(), "before");
235 label.set_text("after");
236 assert_eq!(label.text(), "after");
237 }
238}