1use presentar_core::{
4 widget::{FontStyle, FontWeight, LayoutResult, TextStyle},
5 Canvas, Color, Constraints, Event, Rect, Size, TypeId, Widget,
6};
7use serde::{Deserialize, Serialize};
8use std::any::Any;
9
10#[derive(Clone, Serialize, Deserialize)]
12pub struct Text {
13 content: String,
15 color: Color,
17 font_size: f32,
19 font_weight: FontWeight,
21 font_style: FontStyle,
23 line_height: f32,
25 max_width: Option<f32>,
27 test_id_value: Option<String>,
29 #[serde(skip)]
31 bounds: Rect,
32}
33
34impl Text {
35 #[must_use]
37 pub fn new(content: impl Into<String>) -> Self {
38 Self {
39 content: content.into(),
40 color: Color::BLACK,
41 font_size: 16.0,
42 font_weight: FontWeight::Normal,
43 font_style: FontStyle::Normal,
44 line_height: 1.2,
45 max_width: None,
46 test_id_value: None,
47 bounds: Rect::default(),
48 }
49 }
50
51 #[must_use]
53 pub const fn color(mut self, color: Color) -> Self {
54 self.color = color;
55 self
56 }
57
58 #[must_use]
60 pub const fn font_size(mut self, size: f32) -> Self {
61 self.font_size = size;
62 self
63 }
64
65 #[must_use]
67 pub const fn font_weight(mut self, weight: FontWeight) -> Self {
68 self.font_weight = weight;
69 self
70 }
71
72 #[must_use]
74 pub const fn font_style(mut self, style: FontStyle) -> Self {
75 self.font_style = style;
76 self
77 }
78
79 #[must_use]
81 pub const fn line_height(mut self, multiplier: f32) -> Self {
82 self.line_height = multiplier;
83 self
84 }
85
86 #[must_use]
88 pub const fn max_width(mut self, width: f32) -> Self {
89 self.max_width = Some(width);
90 self
91 }
92
93 #[must_use]
95 pub fn with_test_id(mut self, id: impl Into<String>) -> Self {
96 self.test_id_value = Some(id.into());
97 self
98 }
99
100 #[must_use]
102 pub fn content(&self) -> &str {
103 &self.content
104 }
105
106 fn estimate_size(&self, max_width: f32) -> Size {
108 let char_width = self.font_size * 0.6;
110 let line_height = self.font_size * self.line_height;
111
112 if self.content.is_empty() {
113 return Size::new(0.0, line_height);
114 }
115
116 let total_width = self.content.len() as f32 * char_width;
117
118 if let Some(max_w) = self.max_width {
119 let effective_max = max_w.min(max_width);
120 if total_width > effective_max {
121 let lines = (total_width / effective_max).ceil();
122 return Size::new(effective_max, lines * line_height);
123 }
124 }
125
126 Size::new(total_width.min(max_width), line_height)
127 }
128}
129
130impl Widget for Text {
131 fn type_id(&self) -> TypeId {
132 TypeId::of::<Self>()
133 }
134
135 fn measure(&self, constraints: Constraints) -> Size {
136 let size = self.estimate_size(constraints.max_width);
137 constraints.constrain(size)
138 }
139
140 fn layout(&mut self, bounds: Rect) -> LayoutResult {
141 self.bounds = bounds;
142 LayoutResult {
143 size: bounds.size(),
144 }
145 }
146
147 fn paint(&self, canvas: &mut dyn Canvas) {
148 let style = TextStyle {
149 size: self.font_size,
150 color: self.color,
151 weight: self.font_weight,
152 style: self.font_style,
153 };
154
155 canvas.draw_text(&self.content, self.bounds.origin(), &style);
156 }
157
158 fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
159 None }
161
162 fn children(&self) -> &[Box<dyn Widget>] {
163 &[]
164 }
165
166 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
167 &mut []
168 }
169
170 fn test_id(&self) -> Option<&str> {
171 self.test_id_value.as_deref()
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use presentar_core::draw::DrawCommand;
179 use presentar_core::{Point, RecordingCanvas, Widget};
180
181 #[test]
182 fn test_text_new() {
183 let t = Text::new("Hello");
184 assert_eq!(t.content(), "Hello");
185 assert_eq!(t.font_size, 16.0);
186 }
187
188 #[test]
189 fn test_text_builder() {
190 let t = Text::new("Test")
191 .color(Color::WHITE)
192 .font_size(24.0)
193 .font_weight(FontWeight::Bold)
194 .with_test_id("my-text");
195
196 assert_eq!(t.color, Color::WHITE);
197 assert_eq!(t.font_size, 24.0);
198 assert_eq!(t.font_weight, FontWeight::Bold);
199 assert_eq!(Widget::test_id(&t), Some("my-text"));
200 }
201
202 #[test]
203 fn test_text_measure() {
204 let t = Text::new("Hello");
205 let size = t.measure(Constraints::loose(Size::new(1000.0, 1000.0)));
206 assert!(size.width > 0.0);
207 assert!(size.height > 0.0);
208 }
209
210 #[test]
211 fn test_text_empty() {
212 let t = Text::new("");
213 let size = t.measure(Constraints::loose(Size::new(1000.0, 1000.0)));
214 assert_eq!(size.width, 0.0);
215 assert!(size.height > 0.0); }
217
218 #[test]
221 fn test_text_paint_draws_text() {
222 let mut text = Text::new("Hello World");
223 text.layout(Rect::new(10.0, 20.0, 200.0, 30.0));
224
225 let mut canvas = RecordingCanvas::new();
226 text.paint(&mut canvas);
227
228 assert_eq!(canvas.command_count(), 1);
229 match &canvas.commands()[0] {
230 DrawCommand::Text {
231 content, position, ..
232 } => {
233 assert_eq!(content, "Hello World");
234 assert_eq!(*position, Point::new(10.0, 20.0));
235 }
236 _ => panic!("Expected Text command"),
237 }
238 }
239
240 #[test]
241 fn test_text_paint_uses_color() {
242 let mut text = Text::new("Colored").color(Color::RED);
243 text.layout(Rect::new(0.0, 0.0, 100.0, 20.0));
244
245 let mut canvas = RecordingCanvas::new();
246 text.paint(&mut canvas);
247
248 match &canvas.commands()[0] {
249 DrawCommand::Text { style, .. } => {
250 assert_eq!(style.color, Color::RED);
251 }
252 _ => panic!("Expected Text command"),
253 }
254 }
255
256 #[test]
257 fn test_text_paint_uses_font_size() {
258 let mut text = Text::new("Large").font_size(32.0);
259 text.layout(Rect::new(0.0, 0.0, 200.0, 40.0));
260
261 let mut canvas = RecordingCanvas::new();
262 text.paint(&mut canvas);
263
264 match &canvas.commands()[0] {
265 DrawCommand::Text { style, .. } => {
266 assert_eq!(style.size, 32.0);
267 }
268 _ => panic!("Expected Text command"),
269 }
270 }
271
272 #[test]
273 fn test_text_paint_uses_font_weight() {
274 let mut text = Text::new("Bold").font_weight(FontWeight::Bold);
275 text.layout(Rect::new(0.0, 0.0, 100.0, 20.0));
276
277 let mut canvas = RecordingCanvas::new();
278 text.paint(&mut canvas);
279
280 match &canvas.commands()[0] {
281 DrawCommand::Text { style, .. } => {
282 assert_eq!(style.weight, FontWeight::Bold);
283 }
284 _ => panic!("Expected Text command"),
285 }
286 }
287
288 #[test]
289 fn test_text_paint_uses_font_style() {
290 let mut text = Text::new("Italic").font_style(FontStyle::Italic);
291 text.layout(Rect::new(0.0, 0.0, 100.0, 20.0));
292
293 let mut canvas = RecordingCanvas::new();
294 text.paint(&mut canvas);
295
296 match &canvas.commands()[0] {
297 DrawCommand::Text { style, .. } => {
298 assert_eq!(style.style, FontStyle::Italic);
299 }
300 _ => panic!("Expected Text command"),
301 }
302 }
303
304 #[test]
305 fn test_text_paint_empty() {
306 let mut text = Text::new("");
307 text.layout(Rect::new(0.0, 0.0, 100.0, 20.0));
308
309 let mut canvas = RecordingCanvas::new();
310 text.paint(&mut canvas);
311
312 assert_eq!(canvas.command_count(), 1);
314 match &canvas.commands()[0] {
315 DrawCommand::Text { content, .. } => {
316 assert!(content.is_empty());
317 }
318 _ => panic!("Expected Text command"),
319 }
320 }
321
322 #[test]
323 fn test_text_paint_position_from_layout() {
324 let mut text = Text::new("Positioned");
325 text.layout(Rect::new(50.0, 100.0, 200.0, 30.0));
326
327 let mut canvas = RecordingCanvas::new();
328 text.paint(&mut canvas);
329
330 match &canvas.commands()[0] {
331 DrawCommand::Text { position, .. } => {
332 assert_eq!(position.x, 50.0);
333 assert_eq!(position.y, 100.0);
334 }
335 _ => panic!("Expected Text command"),
336 }
337 }
338
339 #[test]
342 fn test_text_type_id() {
343 let t = Text::new("test");
344 assert_eq!(Widget::type_id(&t), TypeId::of::<Text>());
345 }
346
347 #[test]
348 fn test_text_layout_sets_bounds() {
349 let mut t = Text::new("test");
350 let result = t.layout(Rect::new(10.0, 20.0, 100.0, 30.0));
351 assert_eq!(result.size, Size::new(100.0, 30.0));
352 assert_eq!(t.bounds, Rect::new(10.0, 20.0, 100.0, 30.0));
353 }
354
355 #[test]
356 fn test_text_children_empty() {
357 let t = Text::new("test");
358 assert!(t.children().is_empty());
359 }
360
361 #[test]
362 fn test_text_event_returns_none() {
363 let mut t = Text::new("test");
364 t.layout(Rect::new(0.0, 0.0, 100.0, 20.0));
365 let result = t.event(&Event::MouseEnter);
366 assert!(result.is_none());
367 }
368
369 #[test]
370 fn test_text_line_height() {
371 let t = Text::new("test").line_height(1.5);
372 assert_eq!(t.line_height, 1.5);
373 }
374
375 #[test]
376 fn test_text_max_width() {
377 let t = Text::new("test").max_width(200.0);
378 assert_eq!(t.max_width, Some(200.0));
379 }
380
381 #[test]
382 fn test_text_measure_with_max_width() {
383 let t = Text::new("A very long text that should wrap").max_width(50.0);
384 let size = t.measure(Constraints::loose(Size::new(1000.0, 1000.0)));
385 assert!(size.width <= 50.0);
386 assert!(size.height > t.font_size); }
388
389 #[test]
390 fn test_text_content_accessor() {
391 let t = Text::new("Hello World");
392 assert_eq!(t.content(), "Hello World");
393 }
394}