1use crate::font::FontInfo;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum TextAlignment {
11 Left,
12 Center,
13 Right,
14 Justify,
15}
16
17#[derive(Debug, Clone)]
19pub struct LayoutLine {
20 pub text: String,
22 pub x_offset: f64,
24 pub y_offset: f64,
26 pub width: f64,
28 pub word_spacing: f64,
30}
31
32#[derive(Debug, Clone)]
34pub struct LayoutResult {
35 pub lines: Vec<LayoutLine>,
37 pub total_height: f64,
39 pub overflow: bool,
41}
42
43#[derive(Debug, Clone)]
45pub struct LayoutOptions {
46 pub font_size: f64,
48 pub line_height_factor: f64,
50 pub alignment: TextAlignment,
52 pub max_width: Option<f64>,
54 pub max_height: Option<f64>,
56 pub first_line_indent: f64,
58}
59
60impl Default for LayoutOptions {
61 fn default() -> Self {
62 Self {
63 font_size: 12.0,
64 line_height_factor: 1.2,
65 alignment: TextAlignment::Left,
66 max_width: None,
67 max_height: None,
68 first_line_indent: 0.0,
69 }
70 }
71}
72
73pub fn char_width(ch: char, font: &FontInfo) -> f64 {
75 let code = ch as u32;
76 font.widths.get_width(code)
77}
78
79pub fn measure_text_width(text: &str, font: &FontInfo, font_size: f64) -> f64 {
81 let total_units: f64 = text.chars().map(|ch| char_width(ch, font)).sum();
82 total_units * font_size / 1000.0
83}
84
85pub fn layout_text(text: &str, font: &FontInfo, options: &LayoutOptions) -> LayoutResult {
90 let line_height = options.font_size * options.line_height_factor;
91 let max_width = options.max_width;
92
93 let paragraphs: Vec<&str> = text.split('\n').collect();
95
96 let mut raw_lines: Vec<(String, bool)> = Vec::new(); for (para_idx, para) in paragraphs.iter().enumerate() {
99 let trimmed = *para;
100 if trimmed.is_empty() {
101 raw_lines.push((String::new(), true));
103 continue;
104 }
105
106 let indent = if para_idx == 0 {
107 options.first_line_indent
108 } else {
109 0.0
111 };
112
113 let wrapped = wrap_paragraph(trimmed, font, options.font_size, max_width, indent);
114 let count = wrapped.len();
115 for (i, line_text) in wrapped.into_iter().enumerate() {
116 let is_last = i == count - 1;
117 raw_lines.push((line_text, is_last));
118 }
119 }
120
121 let mut lines = Vec::new();
123 let mut y_offset = 0.0;
124 let mut overflow = false;
125
126 for (i, (line_text, is_last_of_para)) in raw_lines.into_iter().enumerate() {
127 if let Some(max_h) = options.max_height {
129 if y_offset + line_height > max_h + 1e-9 {
130 overflow = true;
131 break;
132 }
133 }
134
135 let line_width = measure_text_width(&line_text, font, options.font_size);
136
137 let effective_max = max_width.unwrap_or(line_width);
138
139 let (x_offset, word_spacing) = match options.alignment {
140 TextAlignment::Left => {
141 let indent = if i == 0 { options.first_line_indent } else { 0.0 };
142 (indent, 0.0)
143 }
144 TextAlignment::Center => {
145 let offset = (effective_max - line_width) / 2.0;
146 (offset.max(0.0), 0.0)
147 }
148 TextAlignment::Right => {
149 let offset = effective_max - line_width;
150 (offset.max(0.0), 0.0)
151 }
152 TextAlignment::Justify => {
153 let indent = if i == 0 { options.first_line_indent } else { 0.0 };
154 if is_last_of_para || max_width.is_none() {
155 (indent, 0.0)
157 } else {
158 let word_count = line_text.split_whitespace().count();
159 let gap_count = if word_count > 1 { word_count - 1 } else { 0 };
160 if gap_count > 0 {
161 let extra = effective_max - line_width - indent;
162 let ws = if extra > 0.0 {
163 extra / gap_count as f64
164 } else {
165 0.0
166 };
167 (indent, ws)
168 } else {
169 (indent, 0.0)
170 }
171 }
172 }
173 };
174
175 lines.push(LayoutLine {
176 text: line_text,
177 x_offset,
178 y_offset,
179 width: line_width,
180 word_spacing,
181 });
182
183 y_offset += line_height;
184 }
185
186 let total_height = if lines.is_empty() {
187 0.0
188 } else {
189 y_offset
190 };
191
192 LayoutResult {
193 lines,
194 total_height,
195 overflow,
196 }
197}
198
199fn wrap_paragraph(
201 text: &str,
202 font: &FontInfo,
203 font_size: f64,
204 max_width: Option<f64>,
205 first_line_indent: f64,
206) -> Vec<String> {
207 let max_w = match max_width {
208 Some(w) => w,
209 None => return vec![text.to_string()],
210 };
211
212 let words: Vec<&str> = text.split_whitespace().collect();
213 if words.is_empty() {
214 return vec![String::new()];
215 }
216
217 let space_width = measure_text_width(" ", font, font_size);
218
219 let mut lines: Vec<String> = Vec::new();
220 let mut current_line = String::new();
221 let mut current_width: f64 = 0.0;
222 let mut is_first_line = true;
223
224 for word in &words {
225 let word_width = measure_text_width(word, font, font_size);
226 let indent = if is_first_line { first_line_indent } else { 0.0 };
227 let available = max_w - indent;
228
229 if current_line.is_empty() {
230 if word_width > available {
232 let broken = force_break_word(word, font, font_size, available);
234 for (j, part) in broken.into_iter().enumerate() {
235 if j > 0 || !current_line.is_empty() {
236 if !current_line.is_empty() {
237 lines.push(current_line);
238 }
239 is_first_line = false;
240 }
241 current_line = part.clone();
242 current_width = measure_text_width(&part, font, font_size);
243 }
244 } else {
245 current_line = word.to_string();
246 current_width = word_width;
247 }
248 } else {
249 let new_width = current_width + space_width + word_width;
251 if new_width <= available + 1e-9 {
252 current_line.push(' ');
253 current_line.push_str(word);
254 current_width = new_width;
255 } else {
256 lines.push(current_line);
258 is_first_line = false;
259
260 let new_indent = 0.0; let new_available = max_w - new_indent;
262
263 if word_width > new_available {
264 let broken = force_break_word(word, font, font_size, new_available);
265 current_line = String::new();
266 current_width = 0.0;
267 for (j, part) in broken.into_iter().enumerate() {
268 if j > 0 && !current_line.is_empty() {
269 lines.push(current_line);
270 }
271 current_line = part.clone();
272 current_width = measure_text_width(&part, font, font_size);
273 }
274 } else {
275 current_line = word.to_string();
276 current_width = word_width;
277 }
278 }
279 }
280 }
281
282 if !current_line.is_empty() {
283 lines.push(current_line);
284 }
285
286 if lines.is_empty() {
287 lines.push(String::new());
288 }
289
290 lines
291}
292
293fn force_break_word(
295 word: &str,
296 font: &FontInfo,
297 font_size: f64,
298 max_width: f64,
299) -> Vec<String> {
300 let mut parts = Vec::new();
301 let mut current = String::new();
302 let mut current_width = 0.0;
303
304 for ch in word.chars() {
305 let ch_width = char_width(ch, font) * font_size / 1000.0;
306 if current_width + ch_width > max_width + 1e-9 && !current.is_empty() {
307 parts.push(current);
308 current = String::new();
309 current_width = 0.0;
310 }
311 current.push(ch);
312 current_width += ch_width;
313 }
314
315 if !current.is_empty() {
316 parts.push(current);
317 }
318
319 if parts.is_empty() {
320 parts.push(String::new());
321 }
322
323 parts
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329 use crate::font::{Encoding, FontWidths};
330
331 fn test_font() -> FontInfo {
333 FontInfo {
336 base_font: b"TestFont".to_vec(),
337 subtype: b"Type1".to_vec(),
338 encoding: Encoding::StandardEncoding,
339 widths: FontWidths::None { default_width: 600.0 },
340 to_unicode: None,
341 is_standard14: false,
342 descriptor: None,
343 }
344 }
345
346 fn variable_width_font() -> FontInfo {
348 let mut widths = vec![0.0; 128];
350 widths[32] = 250.0; for i in 33..127u32 {
352 widths[i as usize] = 500.0; }
354 widths[b'i' as usize] = 250.0;
356 widths[b'l' as usize] = 250.0;
357 widths[b'm' as usize] = 750.0;
358 widths[b'w' as usize] = 750.0;
359 widths[b'W' as usize] = 750.0;
360
361 FontInfo {
362 base_font: b"VarFont".to_vec(),
363 subtype: b"Type1".to_vec(),
364 encoding: Encoding::StandardEncoding,
365 widths: FontWidths::Simple {
366 first_char: 0,
367 widths,
368 default_width: 500.0,
369 },
370 to_unicode: None,
371 is_standard14: false,
372 descriptor: None,
373 }
374 }
375
376 #[test]
377 fn test_measure_text_width() {
378 let font = test_font();
379 let w = measure_text_width("Hello", &font, 10.0);
381 assert!((w - 30.0).abs() < 0.01, "expected 30.0, got {w}");
382 }
383
384 #[test]
385 fn test_measure_text_width_empty() {
386 let font = test_font();
387 let w = measure_text_width("", &font, 12.0);
388 assert!((w - 0.0).abs() < 0.01);
389 }
390
391 #[test]
392 fn test_measure_text_width_variable() {
393 let font = variable_width_font();
394 let w = measure_text_width("i", &font, 10.0);
396 assert!((w - 2.5).abs() < 0.01, "expected 2.5, got {w}");
397 }
398
399 #[test]
400 fn test_simple_single_line() {
401 let font = test_font();
402 let options = LayoutOptions {
403 font_size: 10.0,
404 ..Default::default()
405 };
406 let result = layout_text("Hello", &font, &options);
407 assert_eq!(result.lines.len(), 1);
408 assert_eq!(result.lines[0].text, "Hello");
409 assert!(!result.overflow);
410 assert!((result.lines[0].width - 30.0).abs() < 0.01);
411 }
412
413 #[test]
414 fn test_word_wrapping() {
415 let font = test_font();
416 let options = LayoutOptions {
419 font_size: 10.0,
420 max_width: Some(40.0),
421 ..Default::default()
422 };
423 let result = layout_text("Hello World", &font, &options);
424 assert_eq!(result.lines.len(), 2);
425 assert_eq!(result.lines[0].text, "Hello");
426 assert_eq!(result.lines[1].text, "World");
427 }
428
429 #[test]
430 fn test_explicit_line_break() {
431 let font = test_font();
432 let options = LayoutOptions {
433 font_size: 10.0,
434 ..Default::default()
435 };
436 let result = layout_text("Line one\nLine two", &font, &options);
437 assert_eq!(result.lines.len(), 2);
438 assert_eq!(result.lines[0].text, "Line one");
439 assert_eq!(result.lines[1].text, "Line two");
440 }
441
442 #[test]
443 fn test_center_alignment() {
444 let font = test_font();
445 let options = LayoutOptions {
447 font_size: 10.0,
448 alignment: TextAlignment::Center,
449 max_width: Some(100.0),
450 ..Default::default()
451 };
452 let result = layout_text("Hi", &font, &options);
453 assert_eq!(result.lines.len(), 1);
454 assert!((result.lines[0].x_offset - 44.0).abs() < 0.01,
455 "expected x_offset ~44.0, got {}", result.lines[0].x_offset);
456 }
457
458 #[test]
459 fn test_right_alignment() {
460 let font = test_font();
461 let options = LayoutOptions {
463 font_size: 10.0,
464 alignment: TextAlignment::Right,
465 max_width: Some(100.0),
466 ..Default::default()
467 };
468 let result = layout_text("Hi", &font, &options);
469 assert_eq!(result.lines.len(), 1);
470 assert!((result.lines[0].x_offset - 88.0).abs() < 0.01,
471 "expected x_offset ~88.0, got {}", result.lines[0].x_offset);
472 }
473
474 #[test]
475 fn test_justify_alignment() {
476 let font = test_font();
477 let options = LayoutOptions {
481 font_size: 10.0,
482 alignment: TextAlignment::Justify,
483 max_width: Some(60.0),
484 ..Default::default()
485 };
486 let result = layout_text("A B C", &font, &options);
487 assert_eq!(result.lines.len(), 1);
488 assert!((result.lines[0].word_spacing - 0.0).abs() < 0.01);
490
491 let result2 = layout_text("AA BB CC DD", &font, &LayoutOptions {
496 font_size: 10.0,
497 alignment: TextAlignment::Justify,
498 max_width: Some(35.0),
499 ..Default::default()
500 });
501 assert!(result2.lines.len() >= 2);
502 if result2.lines[0].text == "AA BB" {
504 assert!((result2.lines[0].word_spacing - 5.0).abs() < 0.5,
505 "expected word_spacing ~5.0, got {}", result2.lines[0].word_spacing);
506 }
507 }
508
509 #[test]
510 fn test_first_line_indent() {
511 let font = test_font();
512 let options = LayoutOptions {
513 font_size: 10.0,
514 alignment: TextAlignment::Left,
515 first_line_indent: 20.0,
516 ..Default::default()
517 };
518 let result = layout_text("Hello World", &font, &options);
519 assert_eq!(result.lines.len(), 1);
520 assert!((result.lines[0].x_offset - 20.0).abs() < 0.01);
521 }
522
523 #[test]
524 fn test_first_line_indent_with_wrapping() {
525 let font = test_font();
526 let options = LayoutOptions {
530 font_size: 10.0,
531 alignment: TextAlignment::Left,
532 max_width: Some(55.0),
533 first_line_indent: 20.0,
534 ..Default::default()
535 };
536 let result = layout_text("Hello World", &font, &options);
537 assert_eq!(result.lines.len(), 2);
538 assert!((result.lines[0].x_offset - 20.0).abs() < 0.01);
539 assert!((result.lines[1].x_offset - 0.0).abs() < 0.01);
540 }
541
542 #[test]
543 fn test_overflow_detection() {
544 let font = test_font();
545 let options = LayoutOptions {
548 font_size: 10.0,
549 max_height: Some(20.0),
550 ..Default::default()
551 };
552 let result = layout_text("Line1\nLine2\nLine3", &font, &options);
553 assert!(result.overflow);
554 assert!(result.lines.len() < 3);
556 }
557
558 #[test]
559 fn test_empty_text() {
560 let font = test_font();
561 let options = LayoutOptions::default();
562 let result = layout_text("", &font, &options);
563 assert_eq!(result.lines.len(), 1);
565 assert_eq!(result.lines[0].text, "");
566 assert!((result.lines[0].width - 0.0).abs() < 0.01);
567 assert!(!result.overflow);
568 }
569
570 #[test]
571 fn test_force_break_wide_word() {
572 let font = test_font();
573 let options = LayoutOptions {
576 font_size: 10.0,
577 max_width: Some(20.0),
578 ..Default::default()
579 };
580 let result = layout_text("ABCDEFGHIJ", &font, &options);
581 assert!(result.lines.len() >= 3, "expected >=3 lines, got {}", result.lines.len());
582 let all_text: String = result.lines.iter().map(|l| l.text.as_str()).collect();
584 assert_eq!(all_text, "ABCDEFGHIJ");
585 }
586
587 #[test]
588 fn test_vertical_layout() {
589 let font = test_font();
590 let options = LayoutOptions {
591 font_size: 10.0,
592 line_height_factor: 1.5,
593 ..Default::default()
594 };
595 let result = layout_text("A\nB\nC", &font, &options);
596 assert_eq!(result.lines.len(), 3);
597 assert!((result.lines[0].y_offset - 0.0).abs() < 0.01);
599 assert!((result.lines[1].y_offset - 15.0).abs() < 0.01);
600 assert!((result.lines[2].y_offset - 30.0).abs() < 0.01);
601 assert!((result.total_height - 45.0).abs() < 0.01);
602 }
603
604 #[test]
605 fn test_default_options() {
606 let opts = LayoutOptions::default();
607 assert!((opts.font_size - 12.0).abs() < 0.01);
608 assert!((opts.line_height_factor - 1.2).abs() < 0.01);
609 assert_eq!(opts.alignment, TextAlignment::Left);
610 assert!(opts.max_width.is_none());
611 assert!(opts.max_height.is_none());
612 assert!((opts.first_line_indent - 0.0).abs() < 0.01);
613 }
614
615 #[test]
616 fn test_multiple_spaces_collapsed() {
617 let font = test_font();
618 let options = LayoutOptions {
620 font_size: 10.0,
621 max_width: Some(500.0),
622 ..Default::default()
623 };
624 let result = layout_text("Hello World", &font, &options);
625 assert_eq!(result.lines.len(), 1);
626 assert_eq!(result.lines[0].text, "Hello World");
627 }
628
629 #[test]
630 fn test_no_overflow_when_fits() {
631 let font = test_font();
632 let options = LayoutOptions {
633 font_size: 10.0,
634 max_height: Some(100.0),
635 ..Default::default()
636 };
637 let result = layout_text("Short", &font, &options);
638 assert!(!result.overflow);
639 }
640
641 #[test]
642 fn test_char_width_function() {
643 let font = test_font();
644 let w = char_width('A', &font);
645 assert!((w - 600.0).abs() < 0.01);
646 }
647
648 #[test]
649 fn test_char_width_variable_font() {
650 let font = variable_width_font();
651 assert!((char_width('i', &font) - 250.0).abs() < 0.01);
652 assert!((char_width('m', &font) - 750.0).abs() < 0.01);
653 assert!((char_width(' ', &font) - 250.0).abs() < 0.01);
654 }
655}