1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
14pub enum TextAlign {
15 #[default]
16 Left,
17 Center,
18 Right,
19}
20
21#[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 #[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 #[must_use]
46 pub fn with_color(mut self, color: Color) -> Self {
47 self.color = color;
48 self
49 }
50
51 #[must_use]
53 pub fn bold(mut self) -> Self {
54 self.bold = true;
55 self
56 }
57
58 #[must_use]
60 pub fn align(mut self, align: TextAlign) -> Self {
61 self.align = align;
62 self
63 }
64
65 #[must_use]
67 pub fn centered(mut self) -> Self {
68 self.align = TextAlign::Center;
69 self
70 }
71
72 #[must_use]
74 pub fn right(mut self) -> Self {
75 self.align = TextAlign::Right;
76 self
77 }
78
79 #[must_use]
81 pub fn content(&self) -> &str {
82 &self.content
83 }
84
85 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 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 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 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 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 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 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 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 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 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 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 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 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 assert_eq!(canvas.texts[0].0, "Hi");
533 }
534}