1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
//! Text widget
//!
//! A simple text widget that internally uses RichText for rendering.
//! This ensures consistent text rendering across all widgets.
use super::richtext::{RichText, Style};
use crate::style::Color;
use crate::widget::theme::PLACEHOLDER_FG;
use crate::widget::traits::{RenderContext, View, WidgetProps};
use crate::{impl_props_builders, impl_styled_view};
/// Text alignment
#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)]
pub enum Alignment {
/// Left-aligned text (default)
#[default]
Left,
/// Center-aligned text
Center,
/// Right-aligned text
Right,
/// Justified text (both edges aligned)
Justify,
}
/// A text display widget
#[derive(Clone, Debug)]
pub struct Text {
content: String,
fg: Option<Color>,
bg: Option<Color>,
bold: bool,
italic: bool,
underline: bool,
dim: bool,
reverse: bool,
align: Alignment,
/// CSS styling properties (id, classes)
props: WidgetProps,
}
impl Text {
/// Create a new text widget
pub fn new(content: impl Into<String>) -> Self {
Self {
content: content.into(),
fg: None,
bg: None,
bold: false,
italic: false,
underline: false,
dim: false,
reverse: false,
align: Alignment::Left,
props: WidgetProps::new(),
}
}
// ─────────────────────────────────────────────────────────────────────────
// Preset builders
// ─────────────────────────────────────────────────────────────────────────
/// Create a heading (bold white text)
pub fn heading(content: impl Into<String>) -> Self {
Self::new(content).bold().fg(Color::WHITE)
}
/// Create muted/secondary text (dimmed gray)
pub fn muted(content: impl Into<String>) -> Self {
Self::new(content).fg(PLACEHOLDER_FG)
}
/// Create error text (red)
pub fn error(content: impl Into<String>) -> Self {
Self::new(content).fg(Color::RED)
}
/// Create success text (green)
pub fn success(content: impl Into<String>) -> Self {
Self::new(content).fg(Color::GREEN)
}
/// Create warning text (yellow)
pub fn warning(content: impl Into<String>) -> Self {
Self::new(content).fg(Color::YELLOW)
}
/// Create info text (cyan)
pub fn info(content: impl Into<String>) -> Self {
Self::new(content).fg(Color::CYAN)
}
/// Create a label (bold)
pub fn label(content: impl Into<String>) -> Self {
Self::new(content).bold()
}
// ─────────────────────────────────────────────────────────────────────────
// Builder methods
// ─────────────────────────────────────────────────────────────────────────
/// Set foreground color
pub fn fg(mut self, color: Color) -> Self {
self.fg = Some(color);
self
}
/// Set background color
pub fn bg(mut self, color: Color) -> Self {
self.bg = Some(color);
self
}
/// Make text bold
pub fn bold(mut self) -> Self {
self.bold = true;
self
}
/// Make text italic
pub fn italic(mut self) -> Self {
self.italic = true;
self
}
/// Underline text
pub fn underline(mut self) -> Self {
self.underline = true;
self
}
/// Dim text (reduced intensity/bright)
pub fn dim(mut self) -> Self {
self.dim = true;
self
}
/// Reverse video (swap foreground/background colors)
pub fn reverse(mut self) -> Self {
self.reverse = true;
self
}
/// Set text alignment
pub fn align(mut self, align: Alignment) -> Self {
self.align = align;
self
}
/// Get the text content
pub fn content(&self) -> &str {
&self.content
}
}
impl Text {
/// Convert to RichText for rendering with CSS support
fn to_rich_text_with_ctx(&self, ctx: &RenderContext) -> RichText {
let mut style = Style::new();
// Get foreground color: inline > CSS > none
let fg = self.fg.or_else(|| {
ctx.style.and_then(|s| {
let c = s.visual.color;
if c != Color::default() {
Some(c)
} else {
None
}
})
});
if let Some(fg) = fg {
style = style.fg(fg);
}
// Get background color: inline > CSS > none
let bg = self.bg.or_else(|| {
ctx.style.and_then(|s| {
let c = s.visual.background;
if c != Color::default() {
Some(c)
} else {
None
}
})
});
if let Some(bg) = bg {
style = style.bg(bg);
}
if self.bold {
style = style.bold();
}
if self.italic {
style = style.italic();
}
if self.underline {
style = style.underline();
}
if self.dim {
style = style.dim();
}
if self.reverse {
style = style.reverse();
}
RichText::new().push(&self.content, style)
}
/// Render text with justify alignment (distribute space between words)
fn render_justified(&self, ctx: &mut RenderContext) {
use crate::render::{Cell, Modifier};
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
let area = ctx.area;
let words: Vec<&str> = self.content.split_whitespace().collect();
// If no words or single word, fall back to left alignment
if words.len() <= 1 {
let rich_text = self.to_rich_text_with_ctx(ctx);
rich_text.render(ctx);
return;
}
// Calculate total text width (without spaces)
let text_width: usize = words.iter().map(|w| w.width()).sum();
let available_width = area.width as usize;
// If text is too wide, fall back to left alignment
if text_width >= available_width {
let rich_text = self.to_rich_text_with_ctx(ctx);
rich_text.render(ctx);
return;
}
// Calculate space distribution
let total_space = available_width - text_width;
let gap_count = words.len() - 1;
let base_space = total_space / gap_count;
let extra_spaces = total_space % gap_count;
// Build modifier from style
let mut modifier = Modifier::empty();
if self.bold {
modifier |= Modifier::BOLD;
}
if self.italic {
modifier |= Modifier::ITALIC;
}
if self.underline {
modifier |= Modifier::UNDERLINE;
}
if self.dim {
modifier |= Modifier::DIM;
}
if self.reverse {
modifier |= Modifier::REVERSE;
}
// Render words with distributed spacing
let mut x: u16 = 0;
for (i, word) in words.iter().enumerate() {
// Render word
for ch in word.chars() {
if x >= area.width {
break;
}
let mut cell = Cell::new(ch);
cell.fg = self.fg;
cell.bg = self.bg;
cell.modifier = modifier;
ctx.set(x, 0, cell);
x += UnicodeWidthChar::width(ch).unwrap_or(0) as u16;
}
// Add spacing after word (except last word)
if i < gap_count {
let spaces = base_space + if i < extra_spaces { 1 } else { 0 };
x += spaces as u16;
}
}
}
}
impl View for Text {
fn render(&self, ctx: &mut RenderContext) {
let area = ctx.area;
if area.width == 0 || area.height == 0 {
return;
}
// Handle Justify alignment specially
if self.align == Alignment::Justify {
self.render_justified(ctx);
return;
}
// Extract CSS colors before creating adjusted context (avoids borrow conflict)
let rich_text = self.to_rich_text_with_ctx(ctx);
// Calculate start position based on alignment
let text_width = unicode_width::UnicodeWidthStr::width(self.content.as_str()) as u16;
let x_offset = match self.align {
Alignment::Left | Alignment::Justify => 0,
Alignment::Center => area.width.saturating_sub(text_width) / 2,
Alignment::Right => area.width.saturating_sub(text_width),
};
// Create adjusted context with alignment offset
let adjusted_area = ctx.sub_area(
x_offset,
0,
area.width.saturating_sub(x_offset),
area.height,
);
let mut adjusted_ctx = RenderContext::new(ctx.buffer, adjusted_area);
// Delegate to RichText for actual rendering
rich_text.render(&mut adjusted_ctx);
}
crate::impl_view_meta!("Text");
}
impl Default for Text {
fn default() -> Self {
Self::new("")
}
}
impl_styled_view!(Text);
impl_props_builders!(Text);
// Tests moved to tests/widget/display/text.rs
// Tests below access private fields and must stay inline
#[cfg(test)]
mod tests {
// KEEP HERE - These tests access private fields and must stay inline
// Public API tests have been extracted to tests/widget/display/text.rs
#[test]
fn test_text_private_initialization() {
// Test private field initialization that can't be tested via public API
use super::*;
let text = Text::new("Test");
// Test that private fields are properly initialized
assert_eq!(text.content, "Test");
assert!(text.fg.is_none());
assert!(text.bg.is_none());
assert!(!text.bold);
assert!(!text.italic);
assert!(!text.underline);
assert!(!text.dim);
assert!(!text.reverse);
}
#[test]
fn test_text_private_builder_patterns() {
// Test builder pattern implementation on private fields
use super::*;
let text = Text::new("Test")
.fg(Color::RED)
.bg(Color::BLUE)
.bold()
.italic();
assert_eq!(text.content, "Test");
assert_eq!(text.fg, Some(Color::RED));
assert_eq!(text.bg, Some(Color::BLUE));
assert!(text.bold);
assert!(text.italic);
}
#[test]
fn test_text_private_alignment() {
// Test private alignment field that can't be tested via public API
use super::*;
let text = Text::new("Test").align(Alignment::Center);
assert_eq!(text.align, Alignment::Center);
}
#[test]
fn test_text_private_reverse() {
// Test reverse private field implementation
use super::*;
let text = Text::new("Test").reverse();
assert!(text.reverse);
}
}