Skip to main content

fop_layout/layout/
inline.rs

1//! Inline-level layout and line breaking
2//!
3//! Handles horizontal positioning of inline elements and text,
4//! including line breaking when content exceeds available width.
5
6use crate::area::TraitSet;
7use fop_types::{FontRegistry, Length};
8use std::fmt;
9
10/// Text alignment options
11#[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
30/// Inline layout context for a single line
31pub struct InlineLayoutContext {
32    /// Available width for the line
33    pub available_width: Length,
34
35    /// Current X position (advances as content is added)
36    pub current_x: Length,
37
38    /// Line height (from line-height property or font metrics)
39    pub line_height: Length,
40
41    /// Areas in this line
42    pub inline_areas: Vec<InlineArea>,
43
44    /// Text alignment
45    pub text_align: TextAlign,
46
47    /// Letter spacing to apply between characters
48    pub letter_spacing: Length,
49
50    /// Word spacing to apply between words
51    pub word_spacing: Length,
52}
53
54/// An inline area waiting to be positioned
55#[derive(Debug, Clone)]
56pub struct InlineArea {
57    /// Width of the area
58    pub width: Length,
59
60    /// Height of the area
61    pub height: Length,
62
63    /// Content (for text areas)
64    pub content: Option<String>,
65
66    /// Rendering traits
67    pub traits: TraitSet,
68}
69
70impl InlineLayoutContext {
71    /// Create a new inline layout context
72    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    /// Set text alignment
85    pub fn with_text_align(mut self, align: TextAlign) -> Self {
86        self.text_align = align;
87        self
88    }
89
90    /// Set letter spacing
91    pub fn with_letter_spacing(mut self, spacing: Length) -> Self {
92        self.letter_spacing = spacing;
93        self
94    }
95
96    /// Set word spacing
97    pub fn with_word_spacing(mut self, spacing: Length) -> Self {
98        self.word_spacing = spacing;
99        self
100    }
101
102    /// Check if content fits on the current line
103    pub fn fits(&self, width: Length) -> bool {
104        self.current_x + width <= self.available_width
105    }
106
107    /// Add an inline area to the line
108    pub fn add(&mut self, area: InlineArea) -> bool {
109        if !self.fits(area.width) {
110            return false; // Doesn't fit
111        }
112
113        self.current_x += area.width;
114
115        // Update line height if this area is taller
116        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    /// Get the remaining width on this line
125    pub fn remaining_width(&self) -> Length {
126        self.available_width - self.current_x
127    }
128
129    /// Check if the line is empty
130    pub fn is_empty(&self) -> bool {
131        self.inline_areas.is_empty()
132    }
133
134    /// Get the total width used
135    pub fn used_width(&self) -> Length {
136        self.current_x
137    }
138
139    /// Calculate the starting X offset for aligned content
140    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, // Justify handled differently
147        }
148    }
149
150    /// Apply text alignment to all areas in the line
151    pub fn apply_alignment(&mut self) {
152        let offset = self.calculate_alignment_offset();
153        if offset > Length::ZERO {
154            // Shift all areas by the offset (implementation would adjust positions)
155            // This is a placeholder - actual implementation would modify area positions
156        }
157    }
158}
159
160/// Line breaker - breaks text into lines
161pub struct LineBreaker {
162    /// Available width for lines
163    available_width: Length,
164
165    /// Font registry for accurate text measurement
166    font_registry: FontRegistry,
167
168    /// Letter spacing to apply
169    letter_spacing: Length,
170
171    /// Word spacing to apply
172    word_spacing: Length,
173}
174
175impl LineBreaker {
176    /// Create a new line breaker
177    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    /// Set letter spacing for text measurement
187    pub fn with_letter_spacing(mut self, spacing: Length) -> Self {
188        self.letter_spacing = spacing;
189        self
190    }
191
192    /// Set word spacing for text measurement
193    pub fn with_word_spacing(mut self, spacing: Length) -> Self {
194        self.word_spacing = spacing;
195        self
196    }
197
198    /// Break text into words for line breaking
199    pub fn break_into_words(&self, text: &str) -> Vec<String> {
200        text.split_whitespace().map(|s| s.to_string()).collect()
201    }
202
203    /// Measure text width using font metrics
204    pub fn measure_text(&self, text: &str, font_size: Length) -> Length {
205        self.measure_text_with_font(text, font_size, "Helvetica")
206    }
207
208    /// Measure text width with specific font
209    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        // Add letter spacing: applied between every character (not after the last one)
214        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        // Add word spacing: applied to each space character
222        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    /// Break text into lines using greedy algorithm
229    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                // Word fits on current line
247                current_line = line_with_word;
248            } else {
249                // Word doesn't fit, start new line
250                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    // ── TextAlign Display ────────────────────────────────────────────────────
270
271    #[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    // ── InlineLayoutContext construction ─────────────────────────────────────
292
293    #[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    // ── InlineArea creation ──────────────────────────────────────────────────
327
328    #[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    // ── InlineLayoutContext add and fits ─────────────────────────────────────
365
366    #[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        // First area fits
431        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        // Second area does not fit (30+25=55 > 50)
439        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    // ── Line height from taller child ────────────────────────────────────────
450
451    #[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), // taller than initial line_height
457            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), // shorter than line_height
470            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    // ── Alignment offset calculations ────────────────────────────────────────
478
479    #[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        // unused = 100 - 60 = 40
503        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        // unused = 40, center = 20
517        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    // ── LineBreaker construction and word splitting ──────────────────────────
534
535    #[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    // ── Text measurement ─────────────────────────────────────────────────────
576
577    #[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        // 5 chars → 4 gaps × 1pt = 4pt extra
610        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        // 1 space → 1 × 3pt extra
623        assert!(w_spaced > w_plain);
624    }
625
626    // ── Line breaking ────────────────────────────────────────────────────────
627
628    #[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        // Rejoin all words from all lines
665        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        // Width too narrow for any two words together
676        let breaker = LineBreaker::new(Length::from_pt(1.0));
677        let lines = breaker.break_lines("one two three", Length::from_pt(12.0));
678        // Each word should be on its own line (or at minimum > 1 line)
679        assert!(!lines.is_empty());
680    }
681}