Skip to main content

presentar_terminal/widgets/
text.rs

1//! Simple text display widget.
2//!
3//! Renders text with optional styling.
4
5use presentar_core::{
6    Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event,
7    FontWeight, LayoutResult, Point, Rect, Size, TextStyle, TypeId, Widget,
8};
9use std::any::Any;
10use std::time::Duration;
11
12/// Text alignment.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
14pub enum TextAlign {
15    #[default]
16    Left,
17    Center,
18    Right,
19}
20
21/// Simple text display widget.
22#[derive(Debug, Clone)]
23pub struct Text {
24    content: String,
25    color: Color,
26    bold: bool,
27    align: TextAlign,
28    bounds: Rect,
29}
30
31impl Text {
32    /// Create a new text widget.
33    #[must_use]
34    pub fn new(content: impl Into<String>) -> Self {
35        Self {
36            content: content.into(),
37            color: Color::new(0.8, 0.8, 0.8, 1.0),
38            bold: false,
39            align: TextAlign::Left,
40            bounds: Rect::default(),
41        }
42    }
43
44    /// Set text color.
45    #[must_use]
46    pub fn with_color(mut self, color: Color) -> Self {
47        self.color = color;
48        self
49    }
50
51    /// Make text bold.
52    #[must_use]
53    pub fn bold(mut self) -> Self {
54        self.bold = true;
55        self
56    }
57
58    /// Set text alignment.
59    #[must_use]
60    pub fn align(mut self, align: TextAlign) -> Self {
61        self.align = align;
62        self
63    }
64
65    /// Center-align text.
66    #[must_use]
67    pub fn centered(mut self) -> Self {
68        self.align = TextAlign::Center;
69        self
70    }
71
72    /// Right-align text.
73    #[must_use]
74    pub fn right(mut self) -> Self {
75        self.align = TextAlign::Right;
76        self
77    }
78
79    /// Get the text content.
80    #[must_use]
81    pub fn content(&self) -> &str {
82        &self.content
83    }
84
85    /// Set the text content.
86    pub fn set_content(&mut self, content: impl Into<String>) {
87        self.content = content.into();
88    }
89}
90
91impl Default for Text {
92    fn default() -> Self {
93        Self::new("")
94    }
95}
96
97impl Brick for Text {
98    fn brick_name(&self) -> &'static str {
99        "text"
100    }
101
102    fn assertions(&self) -> &[BrickAssertion] {
103        static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(1)];
104        ASSERTIONS
105    }
106
107    fn budget(&self) -> BrickBudget {
108        BrickBudget::uniform(1)
109    }
110
111    fn verify(&self) -> BrickVerification {
112        BrickVerification {
113            passed: vec![BrickAssertion::max_latency_ms(1)],
114            failed: vec![],
115            verification_time: Duration::from_micros(1),
116        }
117    }
118
119    fn to_html(&self) -> String {
120        format!("<span>{}</span>", self.content)
121    }
122
123    fn to_css(&self) -> String {
124        String::new()
125    }
126}
127
128impl Widget for Text {
129    fn type_id(&self) -> TypeId {
130        TypeId::of::<Self>()
131    }
132
133    fn measure(&self, constraints: Constraints) -> Size {
134        let char_count = self.content.chars().count() as f32;
135        let width = char_count.min(constraints.max_width);
136        let height = 1.0_f32.min(constraints.max_height);
137        constraints.constrain(Size::new(width, height))
138    }
139
140    fn layout(&mut self, bounds: Rect) -> LayoutResult {
141        self.bounds = bounds;
142        LayoutResult {
143            size: Size::new(bounds.width, bounds.height.min(1.0)),
144        }
145    }
146
147    fn paint(&self, canvas: &mut dyn Canvas) {
148        if self.bounds.width < 1.0 || self.bounds.height < 1.0 {
149            return;
150        }
151
152        let style = TextStyle {
153            color: self.color,
154            weight: if self.bold {
155                FontWeight::Bold
156            } else {
157                FontWeight::Normal
158            },
159            ..Default::default()
160        };
161
162        let width = self.bounds.width as usize;
163        let text_len = self.content.chars().count();
164
165        let x_offset = match self.align {
166            TextAlign::Left => 0.0,
167            TextAlign::Center => ((width.saturating_sub(text_len)) / 2) as f32,
168            TextAlign::Right => (width.saturating_sub(text_len)) as f32,
169        };
170
171        // Truncate if needed
172        let display: String = if text_len > width {
173            if width > 3 {
174                format!(
175                    "{}...",
176                    self.content.chars().take(width - 3).collect::<String>()
177                )
178            } else {
179                self.content.chars().take(width).collect()
180            }
181        } else {
182            self.content.clone()
183        };
184
185        canvas.draw_text(
186            &display,
187            Point::new(self.bounds.x + x_offset, self.bounds.y),
188            &style,
189        );
190    }
191
192    fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
193        None
194    }
195
196    fn children(&self) -> &[Box<dyn Widget>] {
197        &[]
198    }
199
200    fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
201        &mut []
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    #[test]
210    fn test_text_new() {
211        let text = Text::new("Hello");
212        assert_eq!(text.content(), "Hello");
213    }
214
215    #[test]
216    fn test_text_with_color() {
217        let text = Text::new("Test").with_color(Color::RED);
218        assert_eq!(text.color, Color::RED);
219    }
220
221    #[test]
222    fn test_text_bold() {
223        let text = Text::new("Test").bold();
224        assert!(text.bold);
225    }
226
227    #[test]
228    fn test_text_align() {
229        let text = Text::new("Test").align(TextAlign::Center);
230        assert_eq!(text.align, TextAlign::Center);
231    }
232
233    #[test]
234    fn test_text_centered() {
235        let text = Text::new("Test").centered();
236        assert_eq!(text.align, TextAlign::Center);
237    }
238
239    #[test]
240    fn test_text_right() {
241        let text = Text::new("Test").right();
242        assert_eq!(text.align, TextAlign::Right);
243    }
244
245    #[test]
246    fn test_text_set_content() {
247        let mut text = Text::new("Old");
248        text.set_content("New");
249        assert_eq!(text.content(), "New");
250    }
251
252    #[test]
253    fn test_text_default() {
254        let text = Text::default();
255        assert_eq!(text.content(), "");
256    }
257
258    #[test]
259    fn test_text_brick_name() {
260        let text = Text::new("Test");
261        assert_eq!(text.brick_name(), "text");
262    }
263
264    #[test]
265    fn test_text_verify() {
266        let text = Text::new("Test");
267        assert!(text.verify().is_valid());
268    }
269
270    #[test]
271    fn test_text_measure() {
272        let text = Text::new("Hello");
273        let size = text.measure(Constraints::new(0.0, 100.0, 0.0, 10.0));
274        assert_eq!(size.width, 5.0);
275        assert_eq!(size.height, 1.0);
276    }
277
278    #[test]
279    fn test_text_layout() {
280        let mut text = Text::new("Test");
281        let result = text.layout(Rect::new(0.0, 0.0, 20.0, 5.0));
282        assert_eq!(result.size.height, 1.0);
283    }
284
285    #[test]
286    fn test_text_to_html() {
287        let text = Text::new("Hello");
288        assert_eq!(text.to_html(), "<span>Hello</span>");
289    }
290
291    #[test]
292    fn test_text_type_id() {
293        let text = Text::new("Test");
294        assert_eq!(Widget::type_id(&text), TypeId::of::<Text>());
295    }
296
297    #[test]
298    fn test_text_children() {
299        let text = Text::new("Test");
300        assert!(text.children().is_empty());
301    }
302
303    #[test]
304    fn test_text_children_mut() {
305        let mut text = Text::new("Test");
306        assert!(text.children_mut().is_empty());
307    }
308
309    // ========================================================================
310    // Additional tests for paint() and improved coverage
311    // ========================================================================
312
313    struct MockCanvas {
314        texts: Vec<(String, Point)>,
315    }
316
317    impl MockCanvas {
318        fn new() -> Self {
319            Self { texts: vec![] }
320        }
321    }
322
323    impl Canvas for MockCanvas {
324        fn fill_rect(&mut self, _rect: Rect, _color: Color) {}
325        fn stroke_rect(&mut self, _rect: Rect, _color: Color, _width: f32) {}
326        fn draw_text(&mut self, text: &str, position: Point, _style: &TextStyle) {
327            self.texts.push((text.to_string(), position));
328        }
329        fn draw_line(&mut self, _from: Point, _to: Point, _color: Color, _width: f32) {}
330        fn fill_circle(&mut self, _center: Point, _radius: f32, _color: Color) {}
331        fn stroke_circle(&mut self, _center: Point, _radius: f32, _color: Color, _width: f32) {}
332        fn fill_arc(&mut self, _c: Point, _r: f32, _s: f32, _e: f32, _color: Color) {}
333        fn draw_path(&mut self, _points: &[Point], _color: Color, _width: f32) {}
334        fn fill_polygon(&mut self, _points: &[Point], _color: Color) {}
335        fn push_clip(&mut self, _rect: Rect) {}
336        fn pop_clip(&mut self) {}
337        fn push_transform(&mut self, _transform: presentar_core::Transform2D) {}
338        fn pop_transform(&mut self) {}
339    }
340
341    #[test]
342    fn test_text_paint_basic() {
343        let mut text = Text::new("Hello");
344        text.bounds = Rect::new(0.0, 0.0, 20.0, 1.0);
345
346        let mut canvas = MockCanvas::new();
347        text.paint(&mut canvas);
348
349        assert_eq!(canvas.texts.len(), 1);
350        assert_eq!(canvas.texts[0].0, "Hello");
351    }
352
353    #[test]
354    fn test_text_paint_centered() {
355        let mut text = Text::new("Hi").centered();
356        text.bounds = Rect::new(0.0, 0.0, 10.0, 1.0);
357
358        let mut canvas = MockCanvas::new();
359        text.paint(&mut canvas);
360
361        // "Hi" is 2 chars, width is 10, so offset should be (10-2)/2 = 4
362        assert_eq!(canvas.texts[0].1.x, 4.0);
363    }
364
365    #[test]
366    fn test_text_paint_right() {
367        let mut text = Text::new("Hi").right();
368        text.bounds = Rect::new(0.0, 0.0, 10.0, 1.0);
369
370        let mut canvas = MockCanvas::new();
371        text.paint(&mut canvas);
372
373        // "Hi" is 2 chars, width is 10, so offset should be 10-2 = 8
374        assert_eq!(canvas.texts[0].1.x, 8.0);
375    }
376
377    #[test]
378    fn test_text_paint_bold() {
379        let mut text = Text::new("Bold").bold();
380        text.bounds = Rect::new(0.0, 0.0, 20.0, 1.0);
381
382        let mut canvas = MockCanvas::new();
383        text.paint(&mut canvas);
384
385        // Should render with bold style (just verify it runs)
386        assert_eq!(canvas.texts.len(), 1);
387        assert_eq!(canvas.texts[0].0, "Bold");
388    }
389
390    #[test]
391    fn test_text_paint_truncation() {
392        let mut text = Text::new("This is a very long text");
393        text.bounds = Rect::new(0.0, 0.0, 10.0, 1.0);
394
395        let mut canvas = MockCanvas::new();
396        text.paint(&mut canvas);
397
398        // Text should be truncated with "..."
399        assert!(canvas.texts[0].0.ends_with("..."));
400        assert!(canvas.texts[0].0.len() <= 10);
401    }
402
403    #[test]
404    fn test_text_paint_truncation_short_width() {
405        let mut text = Text::new("Hello");
406        text.bounds = Rect::new(0.0, 0.0, 3.0, 1.0);
407
408        let mut canvas = MockCanvas::new();
409        text.paint(&mut canvas);
410
411        // Width <= 3, so just truncate without "..."
412        assert_eq!(canvas.texts[0].0.len(), 3);
413    }
414
415    #[test]
416    fn test_text_paint_zero_bounds() {
417        let mut text = Text::new("Test");
418        text.bounds = Rect::new(0.0, 0.0, 0.0, 0.0);
419
420        let mut canvas = MockCanvas::new();
421        text.paint(&mut canvas);
422
423        // Should return early, no output
424        assert!(canvas.texts.is_empty());
425    }
426
427    #[test]
428    fn test_text_paint_zero_height() {
429        let mut text = Text::new("Test");
430        text.bounds = Rect::new(0.0, 0.0, 10.0, 0.5);
431
432        let mut canvas = MockCanvas::new();
433        text.paint(&mut canvas);
434
435        // Should return early since height < 1
436        assert!(canvas.texts.is_empty());
437    }
438
439    #[test]
440    fn test_text_event() {
441        let mut text = Text::new("Test");
442        let event = Event::key_down(presentar_core::Key::Enter);
443        assert!(text.event(&event).is_none());
444    }
445
446    #[test]
447    fn test_text_assertions() {
448        let text = Text::new("Test");
449        assert!(!text.assertions().is_empty());
450    }
451
452    #[test]
453    fn test_text_budget() {
454        let text = Text::new("Test");
455        let budget = text.budget();
456        // Budget should have total_ms set (uniform(1) sets to 1ms)
457        assert!(budget.total_ms > 0);
458    }
459
460    #[test]
461    fn test_text_to_css() {
462        let text = Text::new("Test");
463        assert!(text.to_css().is_empty());
464    }
465
466    #[test]
467    fn test_text_align_default() {
468        assert_eq!(TextAlign::default(), TextAlign::Left);
469    }
470
471    #[test]
472    fn test_text_align_debug() {
473        let align = TextAlign::Center;
474        let debug = format!("{:?}", align);
475        assert!(debug.contains("Center"));
476    }
477
478    #[test]
479    fn test_text_clone() {
480        let text = Text::new("Test").with_color(Color::RED).bold().centered();
481        let cloned = text.clone();
482        assert_eq!(cloned.content(), text.content());
483        assert_eq!(cloned.color, text.color);
484        assert_eq!(cloned.bold, text.bold);
485        assert_eq!(cloned.align, text.align);
486    }
487
488    #[test]
489    fn test_text_debug() {
490        let text = Text::new("Test");
491        let debug = format!("{:?}", text);
492        assert!(debug.contains("Text"));
493    }
494
495    #[test]
496    fn test_text_measure_constrained() {
497        let text = Text::new("Hello World!");
498        // Constrain width to less than text length
499        let size = text.measure(Constraints::new(0.0, 5.0, 0.0, 10.0));
500        assert_eq!(size.width, 5.0);
501    }
502
503    #[test]
504    fn test_text_empty() {
505        let text = Text::new("");
506        let size = text.measure(Constraints::new(0.0, 100.0, 0.0, 10.0));
507        assert_eq!(size.width, 0.0);
508    }
509
510    #[test]
511    fn test_text_paint_at_position() {
512        let mut text = Text::new("Test");
513        text.bounds = Rect::new(5.0, 10.0, 20.0, 1.0);
514
515        let mut canvas = MockCanvas::new();
516        text.paint(&mut canvas);
517
518        // Should render at bounds position
519        assert_eq!(canvas.texts[0].1.x, 5.0);
520        assert_eq!(canvas.texts[0].1.y, 10.0);
521    }
522
523    #[test]
524    fn test_text_paint_exact_fit() {
525        let mut text = Text::new("Hi");
526        text.bounds = Rect::new(0.0, 0.0, 2.0, 1.0);
527
528        let mut canvas = MockCanvas::new();
529        text.paint(&mut canvas);
530
531        // Text fits exactly
532        assert_eq!(canvas.texts[0].0, "Hi");
533    }
534}