1use crate::area::TraitSet;
7use fop_types::{FontRegistry, Length};
8use std::fmt;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum TextAlign {
13 Left,
14 Right,
15 Center,
16 Justify,
17}
18
19impl fmt::Display for TextAlign {
20 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21 match self {
22 TextAlign::Left => write!(f, "left"),
23 TextAlign::Right => write!(f, "right"),
24 TextAlign::Center => write!(f, "center"),
25 TextAlign::Justify => write!(f, "justify"),
26 }
27 }
28}
29
30pub struct InlineLayoutContext {
32 pub available_width: Length,
34
35 pub current_x: Length,
37
38 pub line_height: Length,
40
41 pub inline_areas: Vec<InlineArea>,
43
44 pub text_align: TextAlign,
46
47 pub letter_spacing: Length,
49
50 pub word_spacing: Length,
52}
53
54#[derive(Debug, Clone)]
56pub struct InlineArea {
57 pub width: Length,
59
60 pub height: Length,
62
63 pub content: Option<String>,
65
66 pub traits: TraitSet,
68}
69
70impl InlineLayoutContext {
71 pub fn new(available_width: Length, line_height: Length) -> Self {
73 Self {
74 available_width,
75 current_x: Length::ZERO,
76 line_height,
77 inline_areas: Vec::new(),
78 text_align: TextAlign::Left,
79 letter_spacing: Length::ZERO,
80 word_spacing: Length::ZERO,
81 }
82 }
83
84 pub fn with_text_align(mut self, align: TextAlign) -> Self {
86 self.text_align = align;
87 self
88 }
89
90 pub fn with_letter_spacing(mut self, spacing: Length) -> Self {
92 self.letter_spacing = spacing;
93 self
94 }
95
96 pub fn with_word_spacing(mut self, spacing: Length) -> Self {
98 self.word_spacing = spacing;
99 self
100 }
101
102 pub fn fits(&self, width: Length) -> bool {
104 self.current_x + width <= self.available_width
105 }
106
107 pub fn add(&mut self, area: InlineArea) -> bool {
109 if !self.fits(area.width) {
110 return false; }
112
113 self.current_x += area.width;
114
115 if area.height > self.line_height {
117 self.line_height = area.height;
118 }
119
120 self.inline_areas.push(area);
121 true
122 }
123
124 pub fn remaining_width(&self) -> Length {
126 self.available_width - self.current_x
127 }
128
129 pub fn is_empty(&self) -> bool {
131 self.inline_areas.is_empty()
132 }
133
134 pub fn used_width(&self) -> Length {
136 self.current_x
137 }
138
139 pub fn calculate_alignment_offset(&self) -> Length {
141 let unused_width = self.available_width - self.current_x;
142 match self.text_align {
143 TextAlign::Left => Length::ZERO,
144 TextAlign::Right => unused_width,
145 TextAlign::Center => unused_width / 2,
146 TextAlign::Justify => Length::ZERO, }
148 }
149
150 pub fn apply_alignment(&mut self) {
152 let offset = self.calculate_alignment_offset();
153 if offset > Length::ZERO {
154 }
157 }
158}
159
160pub struct LineBreaker {
162 available_width: Length,
164
165 font_registry: FontRegistry,
167
168 letter_spacing: Length,
170
171 word_spacing: Length,
173}
174
175impl LineBreaker {
176 pub fn new(available_width: Length) -> Self {
178 Self {
179 available_width,
180 font_registry: FontRegistry::new(),
181 letter_spacing: Length::ZERO,
182 word_spacing: Length::ZERO,
183 }
184 }
185
186 pub fn with_letter_spacing(mut self, spacing: Length) -> Self {
188 self.letter_spacing = spacing;
189 self
190 }
191
192 pub fn with_word_spacing(mut self, spacing: Length) -> Self {
194 self.word_spacing = spacing;
195 self
196 }
197
198 pub fn break_into_words(&self, text: &str) -> Vec<String> {
200 text.split_whitespace().map(|s| s.to_string()).collect()
201 }
202
203 pub fn measure_text(&self, text: &str, font_size: Length) -> Length {
205 self.measure_text_with_font(text, font_size, "Helvetica")
206 }
207
208 pub fn measure_text_with_font(&self, text: &str, font_size: Length, font_name: &str) -> Length {
210 let font_metrics = self.font_registry.get_or_default(font_name);
211 let base_width = font_metrics.measure_text(text, font_size);
212
213 let char_count = text.chars().count();
215 let letter_spacing_total = if char_count > 0 {
216 self.letter_spacing * (char_count.saturating_sub(1) as i32)
217 } else {
218 Length::ZERO
219 };
220
221 let space_count = text.chars().filter(|&c| c == ' ').count();
223 let word_spacing_total = self.word_spacing * (space_count as i32);
224
225 base_width + letter_spacing_total + word_spacing_total
226 }
227
228 pub fn break_lines(&self, text: &str, font_size: Length) -> Vec<String> {
230 let words = self.break_into_words(text);
231 let mut lines = Vec::new();
232 let mut current_line = String::new();
233 let _space_width = self.measure_text(" ", font_size);
234
235 for word in words {
236 let _word_width = self.measure_text(&word, font_size);
237 let line_with_word = if current_line.is_empty() {
238 word.clone()
239 } else {
240 format!("{} {}", current_line, word)
241 };
242
243 let total_width = self.measure_text(&line_with_word, font_size);
244
245 if total_width <= self.available_width {
246 current_line = line_with_word;
248 } else {
249 if !current_line.is_empty() {
251 lines.push(current_line);
252 }
253 current_line = word;
254 }
255 }
256
257 if !current_line.is_empty() {
258 lines.push(current_line);
259 }
260
261 lines
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[test]
272 fn test_text_align_display_left() {
273 assert_eq!(format!("{}", TextAlign::Left), "left");
274 }
275
276 #[test]
277 fn test_text_align_display_right() {
278 assert_eq!(format!("{}", TextAlign::Right), "right");
279 }
280
281 #[test]
282 fn test_text_align_display_center() {
283 assert_eq!(format!("{}", TextAlign::Center), "center");
284 }
285
286 #[test]
287 fn test_text_align_display_justify() {
288 assert_eq!(format!("{}", TextAlign::Justify), "justify");
289 }
290
291 #[test]
294 fn test_inline_context_initial_state() {
295 let ctx = InlineLayoutContext::new(Length::from_pt(100.0), Length::from_pt(12.0));
296 assert!(ctx.is_empty());
297 assert_eq!(ctx.available_width, Length::from_pt(100.0));
298 assert_eq!(ctx.current_x, Length::ZERO);
299 assert_eq!(ctx.line_height, Length::from_pt(12.0));
300 assert_eq!(ctx.text_align, TextAlign::Left);
301 assert_eq!(ctx.letter_spacing, Length::ZERO);
302 assert_eq!(ctx.word_spacing, Length::ZERO);
303 }
304
305 #[test]
306 fn test_inline_context_with_text_align() {
307 let ctx = InlineLayoutContext::new(Length::from_pt(100.0), Length::from_pt(12.0))
308 .with_text_align(TextAlign::Center);
309 assert_eq!(ctx.text_align, TextAlign::Center);
310 }
311
312 #[test]
313 fn test_inline_context_with_letter_spacing() {
314 let ctx = InlineLayoutContext::new(Length::from_pt(100.0), Length::from_pt(12.0))
315 .with_letter_spacing(Length::from_pt(1.0));
316 assert_eq!(ctx.letter_spacing, Length::from_pt(1.0));
317 }
318
319 #[test]
320 fn test_inline_context_with_word_spacing() {
321 let ctx = InlineLayoutContext::new(Length::from_pt(100.0), Length::from_pt(12.0))
322 .with_word_spacing(Length::from_pt(2.0));
323 assert_eq!(ctx.word_spacing, Length::from_pt(2.0));
324 }
325
326 #[test]
329 fn test_inline_area_text_creation() {
330 let area = InlineArea {
331 width: Length::from_pt(30.0),
332 height: Length::from_pt(12.0),
333 content: Some("hello".to_string()),
334 traits: TraitSet::default(),
335 };
336 assert_eq!(area.width, Length::from_pt(30.0));
337 assert_eq!(area.height, Length::from_pt(12.0));
338 assert_eq!(area.content.as_deref(), Some("hello"));
339 }
340
341 #[test]
342 fn test_inline_area_space_creation() {
343 let area = InlineArea {
344 width: Length::from_pt(5.0),
345 height: Length::from_pt(12.0),
346 content: None,
347 traits: TraitSet::default(),
348 };
349 assert_eq!(area.width, Length::from_pt(5.0));
350 assert!(area.content.is_none());
351 }
352
353 #[test]
354 fn test_inline_area_glue_zero_width() {
355 let area = InlineArea {
356 width: Length::ZERO,
357 height: Length::from_pt(12.0),
358 content: None,
359 traits: TraitSet::default(),
360 };
361 assert_eq!(area.width, Length::ZERO);
362 }
363
364 #[test]
367 fn test_inline_context_add_single_area() {
368 let mut ctx = InlineLayoutContext::new(Length::from_pt(100.0), Length::from_pt(12.0));
369 let area = InlineArea {
370 width: Length::from_pt(30.0),
371 height: Length::from_pt(12.0),
372 content: Some("test".to_string()),
373 traits: TraitSet::default(),
374 };
375 let result = ctx.add(area);
376 assert!(result);
377 assert!(!ctx.is_empty());
378 assert_eq!(ctx.used_width(), Length::from_pt(30.0));
379 assert_eq!(ctx.remaining_width(), Length::from_pt(70.0));
380 }
381
382 #[test]
383 fn test_inline_context_add_multiple_areas() {
384 let mut ctx = InlineLayoutContext::new(Length::from_pt(100.0), Length::from_pt(12.0));
385 for _ in 0..3 {
386 let area = InlineArea {
387 width: Length::from_pt(20.0),
388 height: Length::from_pt(12.0),
389 content: Some("x".to_string()),
390 traits: TraitSet::default(),
391 };
392 assert!(ctx.add(area));
393 }
394 assert_eq!(ctx.inline_areas.len(), 3);
395 assert_eq!(ctx.used_width(), Length::from_pt(60.0));
396 }
397
398 #[test]
399 fn test_inline_area_width_overflow_not_added() {
400 let mut ctx = InlineLayoutContext::new(Length::from_pt(50.0), Length::from_pt(12.0));
401 let area = InlineArea {
402 width: Length::from_pt(60.0),
403 height: Length::from_pt(12.0),
404 content: Some("toolong".to_string()),
405 traits: TraitSet::default(),
406 };
407 let result = ctx.add(area);
408 assert!(!result);
409 assert!(ctx.is_empty());
410 assert_eq!(ctx.used_width(), Length::ZERO);
411 }
412
413 #[test]
414 fn test_inline_context_fits_exact_width() {
415 let mut ctx = InlineLayoutContext::new(Length::from_pt(50.0), Length::from_pt(12.0));
416 let area = InlineArea {
417 width: Length::from_pt(50.0),
418 height: Length::from_pt(12.0),
419 content: Some("exact".to_string()),
420 traits: TraitSet::default(),
421 };
422 assert!(ctx.add(area));
423 assert_eq!(ctx.used_width(), Length::from_pt(50.0));
424 assert_eq!(ctx.remaining_width(), Length::ZERO);
425 }
426
427 #[test]
428 fn test_inline_context_overflow_detection() {
429 let mut ctx = InlineLayoutContext::new(Length::from_pt(50.0), Length::from_pt(12.0));
430 let area1 = InlineArea {
432 width: Length::from_pt(30.0),
433 height: Length::from_pt(12.0),
434 content: Some("ok".to_string()),
435 traits: TraitSet::default(),
436 };
437 assert!(ctx.add(area1));
438 let area2 = InlineArea {
440 width: Length::from_pt(25.0),
441 height: Length::from_pt(12.0),
442 content: Some("overflow".to_string()),
443 traits: TraitSet::default(),
444 };
445 assert!(!ctx.add(area2));
446 assert_eq!(ctx.inline_areas.len(), 1);
447 }
448
449 #[test]
452 fn test_line_height_updated_by_taller_area() {
453 let mut ctx = InlineLayoutContext::new(Length::from_pt(100.0), Length::from_pt(12.0));
454 let area = InlineArea {
455 width: Length::from_pt(20.0),
456 height: Length::from_pt(20.0), content: None,
458 traits: TraitSet::default(),
459 };
460 ctx.add(area);
461 assert_eq!(ctx.line_height, Length::from_pt(20.0));
462 }
463
464 #[test]
465 fn test_line_height_not_reduced_by_shorter_area() {
466 let mut ctx = InlineLayoutContext::new(Length::from_pt(100.0), Length::from_pt(14.0));
467 let area = InlineArea {
468 width: Length::from_pt(20.0),
469 height: Length::from_pt(10.0), content: None,
471 traits: TraitSet::default(),
472 };
473 ctx.add(area);
474 assert_eq!(ctx.line_height, Length::from_pt(14.0));
475 }
476
477 #[test]
480 fn test_alignment_offset_left_is_zero() {
481 let mut ctx = InlineLayoutContext::new(Length::from_pt(100.0), Length::from_pt(12.0))
482 .with_text_align(TextAlign::Left);
483 ctx.add(InlineArea {
484 width: Length::from_pt(60.0),
485 height: Length::from_pt(12.0),
486 content: None,
487 traits: TraitSet::default(),
488 });
489 assert_eq!(ctx.calculate_alignment_offset(), Length::ZERO);
490 }
491
492 #[test]
493 fn test_alignment_offset_right_is_unused_width() {
494 let mut ctx = InlineLayoutContext::new(Length::from_pt(100.0), Length::from_pt(12.0))
495 .with_text_align(TextAlign::Right);
496 ctx.add(InlineArea {
497 width: Length::from_pt(60.0),
498 height: Length::from_pt(12.0),
499 content: None,
500 traits: TraitSet::default(),
501 });
502 assert_eq!(ctx.calculate_alignment_offset(), Length::from_pt(40.0));
504 }
505
506 #[test]
507 fn test_alignment_offset_center_is_half_unused() {
508 let mut ctx = InlineLayoutContext::new(Length::from_pt(100.0), Length::from_pt(12.0))
509 .with_text_align(TextAlign::Center);
510 ctx.add(InlineArea {
511 width: Length::from_pt(60.0),
512 height: Length::from_pt(12.0),
513 content: None,
514 traits: TraitSet::default(),
515 });
516 assert_eq!(ctx.calculate_alignment_offset(), Length::from_pt(20.0));
518 }
519
520 #[test]
521 fn test_alignment_offset_justify_is_zero() {
522 let mut ctx = InlineLayoutContext::new(Length::from_pt(100.0), Length::from_pt(12.0))
523 .with_text_align(TextAlign::Justify);
524 ctx.add(InlineArea {
525 width: Length::from_pt(60.0),
526 height: Length::from_pt(12.0),
527 content: None,
528 traits: TraitSet::default(),
529 });
530 assert_eq!(ctx.calculate_alignment_offset(), Length::ZERO);
531 }
532
533 #[test]
536 fn test_line_breaker_new() {
537 let breaker = LineBreaker::new(Length::from_pt(100.0));
538 assert_eq!(breaker.available_width, Length::from_pt(100.0));
539 }
540
541 #[test]
542 fn test_break_into_words_basic() {
543 let breaker = LineBreaker::new(Length::from_pt(100.0));
544 let words = breaker.break_into_words("Hello world test");
545 assert_eq!(words.len(), 3);
546 assert_eq!(words[0], "Hello");
547 assert_eq!(words[1], "world");
548 assert_eq!(words[2], "test");
549 }
550
551 #[test]
552 fn test_break_into_words_empty_string() {
553 let breaker = LineBreaker::new(Length::from_pt(100.0));
554 let words = breaker.break_into_words("");
555 assert!(words.is_empty());
556 }
557
558 #[test]
559 fn test_break_into_words_single_word() {
560 let breaker = LineBreaker::new(Length::from_pt(100.0));
561 let words = breaker.break_into_words("Hello");
562 assert_eq!(words.len(), 1);
563 assert_eq!(words[0], "Hello");
564 }
565
566 #[test]
567 fn test_break_into_words_extra_whitespace() {
568 let breaker = LineBreaker::new(Length::from_pt(100.0));
569 let words = breaker.break_into_words(" one two ");
570 assert_eq!(words.len(), 2);
571 assert_eq!(words[0], "one");
572 assert_eq!(words[1], "two");
573 }
574
575 #[test]
578 fn test_measure_text_positive_width() {
579 let breaker = LineBreaker::new(Length::from_pt(100.0));
580 let width = breaker.measure_text("test", Length::from_pt(12.0));
581 assert!(width > Length::ZERO);
582 }
583
584 #[test]
585 fn test_measure_text_longer_text_wider() {
586 let breaker = LineBreaker::new(Length::from_pt(100.0));
587 let w1 = breaker.measure_text("hi", Length::from_pt(12.0));
588 let w2 = breaker.measure_text("hello world", Length::from_pt(12.0));
589 assert!(w2 > w1);
590 }
591
592 #[test]
593 fn test_measure_text_larger_font_wider() {
594 let breaker = LineBreaker::new(Length::from_pt(100.0));
595 let w_small = breaker.measure_text("test", Length::from_pt(10.0));
596 let w_large = breaker.measure_text("test", Length::from_pt(20.0));
597 assert!(w_large > w_small);
598 }
599
600 #[test]
601 fn test_measure_text_with_letter_spacing() {
602 let breaker_plain = LineBreaker::new(Length::from_pt(200.0));
603 let breaker_spaced =
604 LineBreaker::new(Length::from_pt(200.0)).with_letter_spacing(Length::from_pt(1.0));
605 let text = "hello";
606 let font_size = Length::from_pt(12.0);
607 let w_plain = breaker_plain.measure_text(text, font_size);
608 let w_spaced = breaker_spaced.measure_text(text, font_size);
609 assert!(w_spaced > w_plain);
611 }
612
613 #[test]
614 fn test_measure_text_with_word_spacing() {
615 let breaker_plain = LineBreaker::new(Length::from_pt(200.0));
616 let breaker_spaced =
617 LineBreaker::new(Length::from_pt(200.0)).with_word_spacing(Length::from_pt(3.0));
618 let text = "hello world";
619 let font_size = Length::from_pt(12.0);
620 let w_plain = breaker_plain.measure_text(text, font_size);
621 let w_spaced = breaker_spaced.measure_text(text, font_size);
622 assert!(w_spaced > w_plain);
624 }
625
626 #[test]
629 fn test_break_lines_short_text_one_line() {
630 let breaker = LineBreaker::new(Length::from_pt(300.0));
631 let lines = breaker.break_lines("Hello world", Length::from_pt(12.0));
632 assert_eq!(lines.len(), 1);
633 assert_eq!(lines[0], "Hello world");
634 }
635
636 #[test]
637 fn test_break_lines_long_text_multiple_lines() {
638 let breaker = LineBreaker::new(Length::from_pt(100.0));
639 let long_text = "This is a very long piece of text that definitely needs breaking";
640 let lines = breaker.break_lines(long_text, Length::from_pt(12.0));
641 assert!(lines.len() > 1);
642 }
643
644 #[test]
645 fn test_break_lines_single_word_fits_one_line() {
646 let breaker = LineBreaker::new(Length::from_pt(200.0));
647 let lines = breaker.break_lines("Hello", Length::from_pt(12.0));
648 assert_eq!(lines.len(), 1);
649 assert_eq!(lines[0], "Hello");
650 }
651
652 #[test]
653 fn test_break_lines_empty_string_produces_no_lines() {
654 let breaker = LineBreaker::new(Length::from_pt(100.0));
655 let lines = breaker.break_lines("", Length::from_pt(12.0));
656 assert!(lines.is_empty());
657 }
658
659 #[test]
660 fn test_break_lines_all_words_preserved() {
661 let breaker = LineBreaker::new(Length::from_pt(80.0));
662 let text = "alpha beta gamma delta";
663 let lines = breaker.break_lines(text, Length::from_pt(12.0));
664 let all_words: Vec<String> = lines
666 .iter()
667 .flat_map(|l| l.split_whitespace().map(|s| s.to_string()))
668 .collect();
669 let expected_words: Vec<String> = text.split_whitespace().map(|s| s.to_string()).collect();
670 assert_eq!(all_words, expected_words);
671 }
672
673 #[test]
674 fn test_break_lines_very_narrow_width_one_word_per_line() {
675 let breaker = LineBreaker::new(Length::from_pt(1.0));
677 let lines = breaker.break_lines("one two three", Length::from_pt(12.0));
678 assert!(!lines.is_empty());
680 }
681}