oxidize_pdf/text/
flow.rs

1use crate::error::Result;
2use crate::page::Margins;
3use crate::text::{measure_text, split_into_words, Font};
4use std::fmt::Write;
5
6#[derive(Debug, Clone, Copy, PartialEq)]
7pub enum TextAlign {
8    Left,
9    Right,
10    Center,
11    Justified,
12}
13
14pub struct TextFlowContext {
15    operations: String,
16    current_font: Font,
17    font_size: f64,
18    line_height: f64,
19    cursor_x: f64,
20    cursor_y: f64,
21    alignment: TextAlign,
22    page_width: f64,
23    #[allow(dead_code)]
24    page_height: f64,
25    margins: Margins,
26}
27
28impl TextFlowContext {
29    pub fn new(page_width: f64, page_height: f64, margins: Margins) -> Self {
30        Self {
31            operations: String::new(),
32            current_font: Font::Helvetica,
33            font_size: 12.0,
34            line_height: 1.2,
35            cursor_x: margins.left,
36            cursor_y: page_height - margins.top,
37            alignment: TextAlign::Left,
38            page_width,
39            page_height,
40            margins,
41        }
42    }
43
44    pub fn set_font(&mut self, font: Font, size: f64) -> &mut Self {
45        self.current_font = font;
46        self.font_size = size;
47        self
48    }
49
50    pub fn set_line_height(&mut self, multiplier: f64) -> &mut Self {
51        self.line_height = multiplier;
52        self
53    }
54
55    pub fn set_alignment(&mut self, alignment: TextAlign) -> &mut Self {
56        self.alignment = alignment;
57        self
58    }
59
60    pub fn at(&mut self, x: f64, y: f64) -> &mut Self {
61        self.cursor_x = x;
62        self.cursor_y = y;
63        self
64    }
65
66    pub fn content_width(&self) -> f64 {
67        self.page_width - self.margins.left - self.margins.right
68    }
69
70    pub fn write_wrapped(&mut self, text: &str) -> Result<&mut Self> {
71        let content_width = self.content_width();
72
73        // Split text into words
74        let words = split_into_words(text);
75        let mut lines: Vec<Vec<&str>> = Vec::new();
76        let mut current_line: Vec<&str> = Vec::new();
77        let mut current_width = 0.0;
78
79        // Build lines based on width constraints
80        for word in words {
81            let word_width = measure_text(word, self.current_font.clone(), self.font_size);
82
83            // Check if we need to start a new line
84            if !current_line.is_empty() && current_width + word_width > content_width {
85                lines.push(current_line);
86                current_line = vec![word];
87                current_width = word_width;
88            } else {
89                current_line.push(word);
90                current_width += word_width;
91            }
92        }
93
94        if !current_line.is_empty() {
95            lines.push(current_line);
96        }
97
98        // Render each line
99        for (i, line) in lines.iter().enumerate() {
100            let line_text = line.join("");
101            let line_width = measure_text(&line_text, self.current_font.clone(), self.font_size);
102
103            // Calculate x position based on alignment
104            let x = match self.alignment {
105                TextAlign::Left => self.margins.left,
106                TextAlign::Right => self.page_width - self.margins.right - line_width,
107                TextAlign::Center => self.margins.left + (content_width - line_width) / 2.0,
108                TextAlign::Justified => {
109                    if i < lines.len() - 1 && line.len() > 1 {
110                        // We'll handle justification below
111                        self.margins.left
112                    } else {
113                        self.margins.left
114                    }
115                }
116            };
117
118            // Begin text object
119            self.operations.push_str("BT\n");
120
121            // Set font
122            writeln!(
123                &mut self.operations,
124                "/{} {} Tf",
125                self.current_font.pdf_name(),
126                self.font_size
127            )
128            .expect("Writing to String should never fail");
129
130            // Set text position
131            writeln!(&mut self.operations, "{:.2} {:.2} Td", x, self.cursor_y)
132                .expect("Writing to String should never fail");
133
134            // Handle justification
135            if self.alignment == TextAlign::Justified && i < lines.len() - 1 && line.len() > 1 {
136                // Calculate extra space to distribute
137                let spaces_count = line.iter().filter(|w| w.trim().is_empty()).count();
138                if spaces_count > 0 {
139                    let extra_space = content_width - line_width;
140                    let space_adjustment = extra_space / spaces_count as f64;
141
142                    // Set word spacing
143                    writeln!(&mut self.operations, "{space_adjustment:.2} Tw")
144                        .expect("Writing to String should never fail");
145                }
146            }
147
148            // Show text
149            self.operations.push('(');
150            for ch in line_text.chars() {
151                match ch {
152                    '(' => self.operations.push_str("\\("),
153                    ')' => self.operations.push_str("\\)"),
154                    '\\' => self.operations.push_str("\\\\"),
155                    '\n' => self.operations.push_str("\\n"),
156                    '\r' => self.operations.push_str("\\r"),
157                    '\t' => self.operations.push_str("\\t"),
158                    _ => self.operations.push(ch),
159                }
160            }
161            self.operations.push_str(") Tj\n");
162
163            // Reset word spacing if it was set
164            if self.alignment == TextAlign::Justified && i < lines.len() - 1 {
165                self.operations.push_str("0 Tw\n");
166            }
167
168            // End text object
169            self.operations.push_str("ET\n");
170
171            // Move cursor down for next line
172            self.cursor_y -= self.font_size * self.line_height;
173        }
174
175        Ok(self)
176    }
177
178    pub fn write_paragraph(&mut self, text: &str) -> Result<&mut Self> {
179        self.write_wrapped(text)?;
180        // Add extra space after paragraph
181        self.cursor_y -= self.font_size * self.line_height * 0.5;
182        Ok(self)
183    }
184
185    pub fn newline(&mut self) -> &mut Self {
186        self.cursor_y -= self.font_size * self.line_height;
187        self.cursor_x = self.margins.left;
188        self
189    }
190
191    pub fn cursor_position(&self) -> (f64, f64) {
192        (self.cursor_x, self.cursor_y)
193    }
194
195    pub fn generate_operations(&self) -> Vec<u8> {
196        self.operations.as_bytes().to_vec()
197    }
198
199    /// Get the current alignment
200    pub fn alignment(&self) -> TextAlign {
201        self.alignment
202    }
203
204    /// Get the page dimensions
205    pub fn page_dimensions(&self) -> (f64, f64) {
206        (self.page_width, self.page_height)
207    }
208
209    /// Get the margins
210    pub fn margins(&self) -> &Margins {
211        &self.margins
212    }
213
214    /// Get current line height multiplier
215    pub fn line_height(&self) -> f64 {
216        self.line_height
217    }
218
219    /// Get the operations string
220    pub fn operations(&self) -> &str {
221        &self.operations
222    }
223
224    /// Clear all operations
225    pub fn clear(&mut self) {
226        self.operations.clear();
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233    use crate::page::Margins;
234
235    fn create_test_margins() -> Margins {
236        Margins {
237            left: 50.0,
238            right: 50.0,
239            top: 50.0,
240            bottom: 50.0,
241        }
242    }
243
244    #[test]
245    fn test_text_flow_context_new() {
246        let margins = create_test_margins();
247        let context = TextFlowContext::new(400.0, 600.0, margins.clone());
248
249        assert_eq!(context.current_font, Font::Helvetica);
250        assert_eq!(context.font_size, 12.0);
251        assert_eq!(context.line_height, 1.2);
252        assert_eq!(context.alignment, TextAlign::Left);
253        assert_eq!(context.page_width, 400.0);
254        assert_eq!(context.page_height, 600.0);
255        assert_eq!(context.cursor_x, 50.0); // margins.left
256        assert_eq!(context.cursor_y, 550.0); // page_height - margins.top
257    }
258
259    #[test]
260    fn test_set_font() {
261        let margins = create_test_margins();
262        let mut context = TextFlowContext::new(400.0, 600.0, margins.clone());
263
264        context.set_font(Font::TimesBold, 16.0);
265        assert_eq!(context.current_font, Font::TimesBold);
266        assert_eq!(context.font_size, 16.0);
267    }
268
269    #[test]
270    fn test_set_line_height() {
271        let margins = create_test_margins();
272        let mut context = TextFlowContext::new(400.0, 600.0, margins.clone());
273
274        context.set_line_height(1.5);
275        assert_eq!(context.line_height(), 1.5);
276    }
277
278    #[test]
279    fn test_set_alignment() {
280        let margins = create_test_margins();
281        let mut context = TextFlowContext::new(400.0, 600.0, margins.clone());
282
283        context.set_alignment(TextAlign::Center);
284        assert_eq!(context.alignment(), TextAlign::Center);
285    }
286
287    #[test]
288    fn test_at_position() {
289        let margins = create_test_margins();
290        let mut context = TextFlowContext::new(400.0, 600.0, margins.clone());
291
292        context.at(100.0, 200.0);
293        let (x, y) = context.cursor_position();
294        assert_eq!(x, 100.0);
295        assert_eq!(y, 200.0);
296    }
297
298    #[test]
299    fn test_content_width() {
300        let margins = create_test_margins();
301        let context = TextFlowContext::new(400.0, 600.0, margins.clone());
302
303        let content_width = context.content_width();
304        assert_eq!(content_width, 300.0); // 400 - 50 - 50
305    }
306
307    #[test]
308    fn test_text_align_variants() {
309        assert_eq!(TextAlign::Left, TextAlign::Left);
310        assert_eq!(TextAlign::Right, TextAlign::Right);
311        assert_eq!(TextAlign::Center, TextAlign::Center);
312        assert_eq!(TextAlign::Justified, TextAlign::Justified);
313
314        assert_ne!(TextAlign::Left, TextAlign::Right);
315    }
316
317    #[test]
318    fn test_write_wrapped_simple() {
319        let margins = create_test_margins();
320        let mut context = TextFlowContext::new(400.0, 600.0, margins.clone());
321
322        context.write_wrapped("Hello World").unwrap();
323
324        let ops = context.operations();
325        assert!(ops.contains("BT\n"));
326        assert!(ops.contains("ET\n"));
327        assert!(ops.contains("/Helvetica 12 Tf"));
328        assert!(ops.contains("(Hello World) Tj"));
329    }
330
331    #[test]
332    fn test_write_paragraph() {
333        let margins = create_test_margins();
334        let mut context = TextFlowContext::new(400.0, 600.0, margins.clone());
335
336        let initial_y = context.cursor_y;
337        context.write_paragraph("Test paragraph").unwrap();
338
339        // Y position should have moved down more than just line height
340        assert!(context.cursor_y < initial_y);
341    }
342
343    #[test]
344    fn test_newline() {
345        let margins = create_test_margins();
346        let mut context = TextFlowContext::new(400.0, 600.0, margins.clone());
347
348        let initial_y = context.cursor_y;
349        context.newline();
350
351        assert_eq!(context.cursor_x, margins.left);
352        assert!(context.cursor_y < initial_y);
353        assert_eq!(
354            context.cursor_y,
355            initial_y - context.font_size * context.line_height
356        );
357    }
358
359    #[test]
360    fn test_cursor_position() {
361        let margins = create_test_margins();
362        let mut context = TextFlowContext::new(400.0, 600.0, margins.clone());
363
364        context.at(75.0, 125.0);
365        let (x, y) = context.cursor_position();
366        assert_eq!(x, 75.0);
367        assert_eq!(y, 125.0);
368    }
369
370    #[test]
371    fn test_generate_operations() {
372        let margins = create_test_margins();
373        let mut context = TextFlowContext::new(400.0, 600.0, margins.clone());
374
375        context.write_wrapped("Test").unwrap();
376        let ops_bytes = context.generate_operations();
377        let ops_string = String::from_utf8(ops_bytes).unwrap();
378
379        assert_eq!(ops_string, context.operations());
380    }
381
382    #[test]
383    fn test_clear_operations() {
384        let margins = create_test_margins();
385        let mut context = TextFlowContext::new(400.0, 600.0, margins.clone());
386
387        context.write_wrapped("Test").unwrap();
388        assert!(!context.operations().is_empty());
389
390        context.clear();
391        assert!(context.operations().is_empty());
392    }
393
394    #[test]
395    fn test_page_dimensions() {
396        let margins = create_test_margins();
397        let context = TextFlowContext::new(400.0, 600.0, margins.clone());
398
399        let (width, height) = context.page_dimensions();
400        assert_eq!(width, 400.0);
401        assert_eq!(height, 600.0);
402    }
403
404    #[test]
405    fn test_margins_access() {
406        let margins = create_test_margins();
407        let context = TextFlowContext::new(400.0, 600.0, margins.clone());
408
409        let ctx_margins = context.margins();
410        assert_eq!(ctx_margins.left, 50.0);
411        assert_eq!(ctx_margins.right, 50.0);
412        assert_eq!(ctx_margins.top, 50.0);
413        assert_eq!(ctx_margins.bottom, 50.0);
414    }
415
416    #[test]
417    fn test_method_chaining() {
418        let margins = create_test_margins();
419        let mut context = TextFlowContext::new(400.0, 600.0, margins.clone());
420
421        context
422            .set_font(Font::Courier, 10.0)
423            .set_line_height(1.5)
424            .set_alignment(TextAlign::Center)
425            .at(100.0, 200.0);
426
427        assert_eq!(context.current_font, Font::Courier);
428        assert_eq!(context.font_size, 10.0);
429        assert_eq!(context.line_height(), 1.5);
430        assert_eq!(context.alignment(), TextAlign::Center);
431        let (x, y) = context.cursor_position();
432        assert_eq!(x, 100.0);
433        assert_eq!(y, 200.0);
434    }
435
436    #[test]
437    fn test_text_align_debug() {
438        let align = TextAlign::Center;
439        let debug_str = format!("{align:?}");
440        assert_eq!(debug_str, "Center");
441    }
442
443    #[test]
444    fn test_text_align_clone() {
445        let align1 = TextAlign::Justified;
446        let align2 = align1;
447        assert_eq!(align1, align2);
448    }
449
450    #[test]
451    fn test_text_align_copy() {
452        let align1 = TextAlign::Right;
453        let align2 = align1; // Copy semantics
454        assert_eq!(align1, align2);
455
456        // Both variables should still be usable
457        assert_eq!(align1, TextAlign::Right);
458        assert_eq!(align2, TextAlign::Right);
459    }
460
461    #[test]
462    fn test_write_wrapped_with_alignment_right() {
463        let margins = create_test_margins();
464        let mut context = TextFlowContext::new(400.0, 600.0, margins);
465
466        context.set_alignment(TextAlign::Right);
467        context.write_wrapped("Right aligned text").unwrap();
468
469        let ops = context.operations();
470        assert!(ops.contains("BT\n"));
471        assert!(ops.contains("ET\n"));
472        // Right alignment should position text differently
473        assert!(ops.contains("Td"));
474    }
475
476    #[test]
477    fn test_write_wrapped_with_alignment_center() {
478        let margins = create_test_margins();
479        let mut context = TextFlowContext::new(400.0, 600.0, margins);
480
481        context.set_alignment(TextAlign::Center);
482        context.write_wrapped("Centered text").unwrap();
483
484        let ops = context.operations();
485        assert!(ops.contains("BT\n"));
486        assert!(ops.contains("(Centered text) Tj"));
487    }
488
489    #[test]
490    fn test_write_wrapped_with_alignment_justified() {
491        let margins = create_test_margins();
492        let mut context = TextFlowContext::new(400.0, 600.0, margins);
493
494        context.set_alignment(TextAlign::Justified);
495        // Long text that will wrap and justify
496        context.write_wrapped("This is a longer text that should wrap across multiple lines to test justification").unwrap();
497
498        let ops = context.operations();
499        assert!(ops.contains("BT\n"));
500        // Justified text may have word spacing adjustments
501        assert!(ops.contains("Tw") || ops.contains("0 Tw"));
502    }
503
504    #[test]
505    fn test_write_wrapped_empty_text() {
506        let margins = create_test_margins();
507        let mut context = TextFlowContext::new(400.0, 600.0, margins);
508
509        context.write_wrapped("").unwrap();
510
511        // Empty text should not generate operations
512        assert!(context.operations().is_empty());
513    }
514
515    #[test]
516    fn test_write_wrapped_whitespace_only() {
517        let margins = create_test_margins();
518        let mut context = TextFlowContext::new(400.0, 600.0, margins);
519
520        context.write_wrapped("   ").unwrap();
521
522        let ops = context.operations();
523        // Should handle whitespace-only text
524        assert!(ops.contains("BT\n") || ops.is_empty());
525    }
526
527    #[test]
528    fn test_write_wrapped_special_characters() {
529        let margins = create_test_margins();
530        let mut context = TextFlowContext::new(400.0, 600.0, margins);
531
532        context
533            .write_wrapped("Text with (parentheses) and \\backslash\\")
534            .unwrap();
535
536        let ops = context.operations();
537        // Special characters should be escaped
538        assert!(ops.contains("\\(parentheses\\)"));
539        assert!(ops.contains("\\\\backslash\\\\"));
540    }
541
542    #[test]
543    fn test_write_wrapped_newlines_tabs() {
544        let margins = create_test_margins();
545        let mut context = TextFlowContext::new(400.0, 600.0, margins);
546
547        context.write_wrapped("Line1\nLine2\tTabbed").unwrap();
548
549        let ops = context.operations();
550        // Newlines and tabs should be escaped
551        assert!(ops.contains("\\n") || ops.contains("\\t"));
552    }
553
554    #[test]
555    fn test_write_wrapped_very_long_word() {
556        let margins = create_test_margins();
557        let mut context = TextFlowContext::new(200.0, 600.0, margins); // Narrow page
558
559        let long_word = "a".repeat(100);
560        context.write_wrapped(&long_word).unwrap();
561
562        let ops = context.operations();
563        assert!(ops.contains("BT\n"));
564        assert!(ops.contains(&long_word));
565    }
566
567    #[test]
568    fn test_write_wrapped_cursor_movement() {
569        let margins = create_test_margins();
570        let mut context = TextFlowContext::new(400.0, 600.0, margins);
571
572        let initial_y = context.cursor_y;
573
574        context.write_wrapped("Line 1").unwrap();
575        let y_after_line1 = context.cursor_y;
576
577        context.write_wrapped("Line 2").unwrap();
578        let y_after_line2 = context.cursor_y;
579
580        // Cursor should move down after each line
581        assert!(y_after_line1 < initial_y);
582        assert!(y_after_line2 < y_after_line1);
583    }
584
585    #[test]
586    fn test_write_paragraph_spacing() {
587        let margins = create_test_margins();
588        let mut context = TextFlowContext::new(400.0, 600.0, margins);
589
590        let initial_y = context.cursor_y;
591        context.write_paragraph("Paragraph 1").unwrap();
592        let y_after_p1 = context.cursor_y;
593
594        context.write_paragraph("Paragraph 2").unwrap();
595        let y_after_p2 = context.cursor_y;
596
597        // Paragraphs should have extra spacing
598        let spacing1 = initial_y - y_after_p1;
599        let spacing2 = y_after_p1 - y_after_p2;
600
601        assert!(spacing1 > 0.0);
602        assert!(spacing2 > 0.0);
603    }
604
605    #[test]
606    fn test_multiple_newlines() {
607        let margins = create_test_margins();
608        let mut context = TextFlowContext::new(400.0, 600.0, margins);
609
610        let initial_y = context.cursor_y;
611
612        context.newline();
613        let y1 = context.cursor_y;
614
615        context.newline();
616        let y2 = context.cursor_y;
617
618        context.newline();
619        let y3 = context.cursor_y;
620
621        // Each newline should move cursor down by same amount
622        let spacing1 = initial_y - y1;
623        let spacing2 = y1 - y2;
624        let spacing3 = y2 - y3;
625
626        // Use approximate equality for floating point comparisons
627        assert!((spacing1 - spacing2).abs() < 1e-10);
628        assert!((spacing2 - spacing3).abs() < 1e-10);
629        assert!((spacing1 - context.font_size * context.line_height).abs() < 1e-10);
630    }
631
632    #[test]
633    fn test_content_width_different_margins() {
634        let margins = Margins {
635            left: 30.0,
636            right: 70.0,
637            top: 40.0,
638            bottom: 60.0,
639        };
640        let context = TextFlowContext::new(500.0, 700.0, margins);
641
642        let content_width = context.content_width();
643        assert_eq!(content_width, 400.0); // 500 - 30 - 70
644    }
645
646    #[test]
647    fn test_custom_line_height() {
648        let margins = create_test_margins();
649        let mut context = TextFlowContext::new(400.0, 600.0, margins);
650
651        context.set_line_height(2.0);
652
653        let initial_y = context.cursor_y;
654        context.newline();
655        let y_after = context.cursor_y;
656
657        let spacing = initial_y - y_after;
658        assert_eq!(spacing, context.font_size * 2.0); // line_height = 2.0
659    }
660
661    #[test]
662    fn test_different_fonts() {
663        let margins = create_test_margins();
664        let mut context = TextFlowContext::new(400.0, 600.0, margins);
665
666        let fonts = vec![
667            Font::Helvetica,
668            Font::HelveticaBold,
669            Font::TimesRoman,
670            Font::TimesBold,
671            Font::Courier,
672            Font::CourierBold,
673        ];
674
675        for font in fonts {
676            context.clear();
677            let font_name = font.pdf_name();
678            context.set_font(font, 14.0);
679            context.write_wrapped("Test text").unwrap();
680
681            let ops = context.operations();
682            assert!(ops.contains(&format!("/{font_name} 14 Tf")));
683        }
684    }
685
686    #[test]
687    fn test_font_size_variations() {
688        let margins = create_test_margins();
689        let mut context = TextFlowContext::new(400.0, 600.0, margins);
690
691        let sizes = vec![8.0, 10.0, 12.0, 14.0, 16.0, 24.0, 36.0];
692
693        for size in sizes {
694            context.clear();
695            context.set_font(Font::Helvetica, size);
696            context.write_wrapped("Test").unwrap();
697
698            let ops = context.operations();
699            assert!(ops.contains(&format!("/Helvetica {size} Tf")));
700        }
701    }
702
703    #[test]
704    fn test_at_position_edge_cases() {
705        let margins = create_test_margins();
706        let mut context = TextFlowContext::new(400.0, 600.0, margins);
707
708        // Test zero position
709        context.at(0.0, 0.0);
710        assert_eq!(context.cursor_position(), (0.0, 0.0));
711
712        // Test negative position
713        context.at(-10.0, -20.0);
714        assert_eq!(context.cursor_position(), (-10.0, -20.0));
715
716        // Test large position
717        context.at(10000.0, 20000.0);
718        assert_eq!(context.cursor_position(), (10000.0, 20000.0));
719    }
720
721    #[test]
722    fn test_write_wrapped_with_narrow_content() {
723        let margins = Margins {
724            left: 190.0,
725            right: 190.0,
726            top: 50.0,
727            bottom: 50.0,
728        };
729        let mut context = TextFlowContext::new(400.0, 600.0, margins);
730
731        // Content width is only 20.0 units
732        context
733            .write_wrapped("This text should wrap a lot")
734            .unwrap();
735
736        let ops = context.operations();
737        // Should have multiple text objects for wrapped lines
738        let bt_count = ops.matches("BT\n").count();
739        assert!(bt_count > 1);
740    }
741
742    #[test]
743    fn test_justified_text_single_word_line() {
744        let margins = create_test_margins();
745        let mut context = TextFlowContext::new(400.0, 600.0, margins);
746
747        context.set_alignment(TextAlign::Justified);
748        context.write_wrapped("SingleWord").unwrap();
749
750        let ops = context.operations();
751        // Single word lines should not have word spacing
752        assert!(!ops.contains(" Tw") || ops.contains("0 Tw"));
753    }
754
755    #[test]
756    fn test_justified_text_last_line() {
757        let margins = create_test_margins();
758        let mut context = TextFlowContext::new(400.0, 600.0, margins);
759
760        context.set_alignment(TextAlign::Justified);
761        // Text that will create multiple lines
762        context.write_wrapped("This is a test of justified text alignment where the last line should not be justified").unwrap();
763
764        let ops = context.operations();
765        // Should reset word spacing (0 Tw) for last line
766        assert!(ops.contains("0 Tw"));
767    }
768
769    #[test]
770    fn test_generate_operations_encoding() {
771        let margins = create_test_margins();
772        let mut context = TextFlowContext::new(400.0, 600.0, margins);
773
774        context.write_wrapped("UTF-8 Text: Ñ").unwrap();
775
776        let ops_bytes = context.generate_operations();
777        let ops_string = String::from_utf8(ops_bytes.clone()).unwrap();
778
779        assert_eq!(ops_bytes, context.operations().as_bytes());
780        assert_eq!(ops_string, context.operations());
781    }
782
783    #[test]
784    fn test_clear_resets_operations_only() {
785        let margins = create_test_margins();
786        let mut context = TextFlowContext::new(400.0, 600.0, margins);
787
788        context.set_font(Font::TimesBold, 18.0);
789        context.set_alignment(TextAlign::Right);
790        context.at(100.0, 200.0);
791        context.write_wrapped("Text").unwrap();
792
793        context.clear();
794
795        // Operations should be cleared
796        assert!(context.operations().is_empty());
797
798        // But other settings should remain
799        assert_eq!(context.current_font, Font::TimesBold);
800        assert_eq!(context.font_size, 18.0);
801        assert_eq!(context.alignment(), TextAlign::Right);
802        // Cursor position should reflect where we are after writing text (moved down by line height)
803        let (x, y) = context.cursor_position();
804        assert_eq!(x, 100.0); // X position should be unchanged
805        assert!(y < 200.0); // Y position should have moved down after writing text
806    }
807
808    #[test]
809    fn test_long_text_wrapping() {
810        let margins = create_test_margins();
811        let mut context = TextFlowContext::new(400.0, 600.0, margins);
812
813        let long_text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. \
814                        Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \
815                        Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.";
816
817        context.write_wrapped(long_text).unwrap();
818
819        let ops = context.operations();
820        // Should have multiple lines
821        let tj_count = ops.matches(") Tj").count();
822        assert!(tj_count > 1);
823    }
824
825    #[test]
826    fn test_empty_operations_initially() {
827        let margins = create_test_margins();
828        let context = TextFlowContext::new(400.0, 600.0, margins);
829
830        assert!(context.operations().is_empty());
831        assert_eq!(context.generate_operations().len(), 0);
832    }
833
834    #[test]
835    fn test_write_paragraph_empty() {
836        let margins = create_test_margins();
837        let mut context = TextFlowContext::new(400.0, 600.0, margins);
838
839        let initial_y = context.cursor_y;
840        context.write_paragraph("").unwrap();
841
842        // Empty paragraph should still add spacing
843        assert!(context.cursor_y < initial_y);
844    }
845
846    #[test]
847    fn test_extreme_line_height() {
848        let margins = create_test_margins();
849        let mut context = TextFlowContext::new(400.0, 600.0, margins);
850
851        // Very small line height
852        context.set_line_height(0.1);
853        let initial_y = context.cursor_y;
854        context.newline();
855        assert_eq!(context.cursor_y, initial_y - context.font_size * 0.1);
856
857        // Very large line height
858        context.set_line_height(10.0);
859        let initial_y2 = context.cursor_y;
860        context.newline();
861        assert_eq!(context.cursor_y, initial_y2 - context.font_size * 10.0);
862    }
863
864    #[test]
865    fn test_zero_content_width() {
866        let margins = Margins {
867            left: 200.0,
868            right: 200.0,
869            top: 50.0,
870            bottom: 50.0,
871        };
872        let context = TextFlowContext::new(400.0, 600.0, margins);
873
874        assert_eq!(context.content_width(), 0.0);
875    }
876
877    #[test]
878    fn test_cursor_x_reset_on_newline() {
879        let margins = create_test_margins();
880        let mut context = TextFlowContext::new(400.0, 600.0, margins.clone());
881
882        context.at(250.0, 300.0); // Move cursor to custom position
883        context.newline();
884
885        // X should reset to left margin
886        assert_eq!(context.cursor_x, margins.left);
887        // Y should decrease by line height
888        assert_eq!(
889            context.cursor_y,
890            300.0 - context.font_size * context.line_height
891        );
892    }
893}