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 available_width(&self) -> f64 {
76 (self.page_width - self.margins.right - self.cursor_x).max(0.0)
77 }
78
79 pub fn write_wrapped(&mut self, text: &str) -> Result<&mut Self> {
80 let start_x = self.cursor_x;
81 let available_width = self.available_width();
82
83 let words = split_into_words(text);
85 let mut lines: Vec<Vec<&str>> = Vec::new();
86 let mut current_line: Vec<&str> = Vec::new();
87 let mut current_width = 0.0;
88
89 for word in words {
91 let word_width = measure_text(word, &self.current_font, self.font_size);
92
93 if !current_line.is_empty() && current_width + word_width > available_width {
95 lines.push(current_line);
96 current_line = vec![word];
97 current_width = word_width;
98 } else {
99 current_line.push(word);
100 current_width += word_width;
101 }
102 }
103
104 if !current_line.is_empty() {
105 lines.push(current_line);
106 }
107
108 for (i, line) in lines.iter().enumerate() {
110 let line_text = line.join("");
111 let line_width = measure_text(&line_text, &self.current_font, self.font_size);
112
113 let x = match self.alignment {
118 TextAlign::Left => start_x,
119 TextAlign::Right => self.page_width - self.margins.right - line_width,
120 TextAlign::Center => start_x + (available_width - line_width) / 2.0,
121 TextAlign::Justified => start_x,
122 };
123
124 self.operations.push_str("BT\n");
126
127 writeln!(
129 &mut self.operations,
130 "/{} {} Tf",
131 self.current_font.pdf_name(),
132 self.font_size
133 )
134 .expect("Writing to String should never fail");
135
136 writeln!(&mut self.operations, "{:.2} {:.2} Td", x, self.cursor_y)
138 .expect("Writing to String should never fail");
139
140 if self.alignment == TextAlign::Justified && i < lines.len() - 1 && line.len() > 1 {
142 let spaces_count = line.iter().filter(|w| w.trim().is_empty()).count();
144 if spaces_count > 0 {
145 let extra_space = available_width - line_width;
146 let space_adjustment = extra_space / spaces_count as f64;
147
148 writeln!(&mut self.operations, "{space_adjustment:.2} Tw")
150 .expect("Writing to String should never fail");
151 }
152 }
153
154 self.operations.push('(');
156 for ch in line_text.chars() {
157 match ch {
158 '(' => self.operations.push_str("\\("),
159 ')' => self.operations.push_str("\\)"),
160 '\\' => self.operations.push_str("\\\\"),
161 '\n' => self.operations.push_str("\\n"),
162 '\r' => self.operations.push_str("\\r"),
163 '\t' => self.operations.push_str("\\t"),
164 _ => self.operations.push(ch),
165 }
166 }
167 self.operations.push_str(") Tj\n");
168
169 if self.alignment == TextAlign::Justified && i < lines.len() - 1 {
171 self.operations.push_str("0 Tw\n");
172 }
173
174 self.operations.push_str("ET\n");
176
177 self.cursor_y -= self.font_size * self.line_height;
179 }
180
181 Ok(self)
182 }
183
184 pub fn write_paragraph(&mut self, text: &str) -> Result<&mut Self> {
185 self.write_wrapped(text)?;
186 self.cursor_y -= self.font_size * self.line_height * 0.5;
188 Ok(self)
189 }
190
191 pub fn newline(&mut self) -> &mut Self {
192 self.cursor_y -= self.font_size * self.line_height;
193 self.cursor_x = self.margins.left;
194 self
195 }
196
197 pub fn cursor_position(&self) -> (f64, f64) {
198 (self.cursor_x, self.cursor_y)
199 }
200
201 pub fn generate_operations(&self) -> Vec<u8> {
202 self.operations.as_bytes().to_vec()
203 }
204
205 pub fn alignment(&self) -> TextAlign {
207 self.alignment
208 }
209
210 pub fn page_dimensions(&self) -> (f64, f64) {
212 (self.page_width, self.page_height)
213 }
214
215 pub fn margins(&self) -> &Margins {
217 &self.margins
218 }
219
220 pub fn line_height(&self) -> f64 {
222 self.line_height
223 }
224
225 pub fn operations(&self) -> &str {
227 &self.operations
228 }
229
230 pub fn clear(&mut self) {
232 self.operations.clear();
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239 use crate::page::Margins;
240
241 fn create_test_margins() -> Margins {
242 Margins {
243 left: 50.0,
244 right: 50.0,
245 top: 50.0,
246 bottom: 50.0,
247 }
248 }
249
250 #[test]
251 fn test_text_flow_context_new() {
252 let margins = create_test_margins();
253 let context = TextFlowContext::new(400.0, 600.0, margins);
254
255 assert_eq!(context.current_font, Font::Helvetica);
256 assert_eq!(context.font_size, 12.0);
257 assert_eq!(context.line_height, 1.2);
258 assert_eq!(context.alignment, TextAlign::Left);
259 assert_eq!(context.page_width, 400.0);
260 assert_eq!(context.page_height, 600.0);
261 assert_eq!(context.cursor_x, 50.0); assert_eq!(context.cursor_y, 550.0); }
264
265 #[test]
266 fn test_set_font() {
267 let margins = create_test_margins();
268 let mut context = TextFlowContext::new(400.0, 600.0, margins);
269
270 context.set_font(Font::TimesBold, 16.0);
271 assert_eq!(context.current_font, Font::TimesBold);
272 assert_eq!(context.font_size, 16.0);
273 }
274
275 #[test]
276 fn test_set_line_height() {
277 let margins = create_test_margins();
278 let mut context = TextFlowContext::new(400.0, 600.0, margins);
279
280 context.set_line_height(1.5);
281 assert_eq!(context.line_height(), 1.5);
282 }
283
284 #[test]
285 fn test_set_alignment() {
286 let margins = create_test_margins();
287 let mut context = TextFlowContext::new(400.0, 600.0, margins);
288
289 context.set_alignment(TextAlign::Center);
290 assert_eq!(context.alignment(), TextAlign::Center);
291 }
292
293 #[test]
294 fn test_at_position() {
295 let margins = create_test_margins();
296 let mut context = TextFlowContext::new(400.0, 600.0, margins);
297
298 context.at(100.0, 200.0);
299 let (x, y) = context.cursor_position();
300 assert_eq!(x, 100.0);
301 assert_eq!(y, 200.0);
302 }
303
304 #[test]
305 fn test_content_width() {
306 let margins = create_test_margins();
307 let context = TextFlowContext::new(400.0, 600.0, margins);
308
309 let content_width = context.content_width();
310 assert_eq!(content_width, 300.0); }
312
313 #[test]
314 fn test_text_align_variants() {
315 assert_eq!(TextAlign::Left, TextAlign::Left);
316 assert_eq!(TextAlign::Right, TextAlign::Right);
317 assert_eq!(TextAlign::Center, TextAlign::Center);
318 assert_eq!(TextAlign::Justified, TextAlign::Justified);
319
320 assert_ne!(TextAlign::Left, TextAlign::Right);
321 }
322
323 #[test]
324 fn test_write_wrapped_simple() {
325 let margins = create_test_margins();
326 let mut context = TextFlowContext::new(400.0, 600.0, margins);
327
328 context.write_wrapped("Hello World").unwrap();
329
330 let ops = context.operations();
331 assert!(ops.contains("BT\n"));
332 assert!(ops.contains("ET\n"));
333 assert!(ops.contains("/Helvetica 12 Tf"));
334 assert!(ops.contains("(Hello World) Tj"));
335 }
336
337 #[test]
338 fn test_write_paragraph() {
339 let margins = create_test_margins();
340 let mut context = TextFlowContext::new(400.0, 600.0, margins);
341
342 let initial_y = context.cursor_y;
343 context.write_paragraph("Test paragraph").unwrap();
344
345 assert!(context.cursor_y < initial_y);
347 }
348
349 #[test]
350 fn test_newline() {
351 let margins = create_test_margins();
352 let mut context = TextFlowContext::new(400.0, 600.0, margins.clone());
353
354 let initial_y = context.cursor_y;
355 context.newline();
356
357 assert_eq!(context.cursor_x, margins.left);
358 assert!(context.cursor_y < initial_y);
359 assert_eq!(
360 context.cursor_y,
361 initial_y - context.font_size * context.line_height
362 );
363 }
364
365 #[test]
366 fn test_cursor_position() {
367 let margins = create_test_margins();
368 let mut context = TextFlowContext::new(400.0, 600.0, margins);
369
370 context.at(75.0, 125.0);
371 let (x, y) = context.cursor_position();
372 assert_eq!(x, 75.0);
373 assert_eq!(y, 125.0);
374 }
375
376 #[test]
377 fn test_generate_operations() {
378 let margins = create_test_margins();
379 let mut context = TextFlowContext::new(400.0, 600.0, margins);
380
381 context.write_wrapped("Test").unwrap();
382 let ops_bytes = context.generate_operations();
383 let ops_string = String::from_utf8(ops_bytes).unwrap();
384
385 assert_eq!(ops_string, context.operations());
386 }
387
388 #[test]
389 fn test_clear_operations() {
390 let margins = create_test_margins();
391 let mut context = TextFlowContext::new(400.0, 600.0, margins);
392
393 context.write_wrapped("Test").unwrap();
394 assert!(!context.operations().is_empty());
395
396 context.clear();
397 assert!(context.operations().is_empty());
398 }
399
400 #[test]
401 fn test_page_dimensions() {
402 let margins = create_test_margins();
403 let context = TextFlowContext::new(400.0, 600.0, margins);
404
405 let (width, height) = context.page_dimensions();
406 assert_eq!(width, 400.0);
407 assert_eq!(height, 600.0);
408 }
409
410 #[test]
411 fn test_margins_access() {
412 let margins = create_test_margins();
413 let context = TextFlowContext::new(400.0, 600.0, margins);
414
415 let ctx_margins = context.margins();
416 assert_eq!(ctx_margins.left, 50.0);
417 assert_eq!(ctx_margins.right, 50.0);
418 assert_eq!(ctx_margins.top, 50.0);
419 assert_eq!(ctx_margins.bottom, 50.0);
420 }
421
422 #[test]
423 fn test_method_chaining() {
424 let margins = create_test_margins();
425 let mut context = TextFlowContext::new(400.0, 600.0, margins);
426
427 context
428 .set_font(Font::Courier, 10.0)
429 .set_line_height(1.5)
430 .set_alignment(TextAlign::Center)
431 .at(100.0, 200.0);
432
433 assert_eq!(context.current_font, Font::Courier);
434 assert_eq!(context.font_size, 10.0);
435 assert_eq!(context.line_height(), 1.5);
436 assert_eq!(context.alignment(), TextAlign::Center);
437 let (x, y) = context.cursor_position();
438 assert_eq!(x, 100.0);
439 assert_eq!(y, 200.0);
440 }
441
442 #[test]
443 fn test_text_align_debug() {
444 let align = TextAlign::Center;
445 let debug_str = format!("{align:?}");
446 assert_eq!(debug_str, "Center");
447 }
448
449 #[test]
450 fn test_text_align_clone() {
451 let align1 = TextAlign::Justified;
452 let align2 = align1;
453 assert_eq!(align1, align2);
454 }
455
456 #[test]
457 fn test_text_align_copy() {
458 let align1 = TextAlign::Right;
459 let align2 = align1; assert_eq!(align1, align2);
461
462 assert_eq!(align1, TextAlign::Right);
464 assert_eq!(align2, TextAlign::Right);
465 }
466
467 #[test]
468 fn test_write_wrapped_with_alignment_right() {
469 let margins = create_test_margins();
470 let mut context = TextFlowContext::new(400.0, 600.0, margins);
471
472 context.set_alignment(TextAlign::Right);
473 context.write_wrapped("Right aligned text").unwrap();
474
475 let ops = context.operations();
476 assert!(ops.contains("BT\n"));
477 assert!(ops.contains("ET\n"));
478 assert!(ops.contains("Td"));
480 }
481
482 #[test]
483 fn test_write_wrapped_with_alignment_center() {
484 let margins = create_test_margins();
485 let mut context = TextFlowContext::new(400.0, 600.0, margins);
486
487 context.set_alignment(TextAlign::Center);
488 context.write_wrapped("Centered text").unwrap();
489
490 let ops = context.operations();
491 assert!(ops.contains("BT\n"));
492 assert!(ops.contains("(Centered text) Tj"));
493 }
494
495 #[test]
496 fn test_write_wrapped_with_alignment_justified() {
497 let margins = create_test_margins();
498 let mut context = TextFlowContext::new(400.0, 600.0, margins);
499
500 context.set_alignment(TextAlign::Justified);
501 context.write_wrapped("This is a longer text that should wrap across multiple lines to test justification").unwrap();
503
504 let ops = context.operations();
505 assert!(ops.contains("BT\n"));
506 assert!(ops.contains("Tw") || ops.contains("0 Tw"));
508 }
509
510 #[test]
511 fn test_write_wrapped_empty_text() {
512 let margins = create_test_margins();
513 let mut context = TextFlowContext::new(400.0, 600.0, margins);
514
515 context.write_wrapped("").unwrap();
516
517 assert!(context.operations().is_empty());
519 }
520
521 #[test]
522 fn test_write_wrapped_whitespace_only() {
523 let margins = create_test_margins();
524 let mut context = TextFlowContext::new(400.0, 600.0, margins);
525
526 context.write_wrapped(" ").unwrap();
527
528 let ops = context.operations();
529 assert!(ops.contains("BT\n") || ops.is_empty());
531 }
532
533 #[test]
534 fn test_write_wrapped_special_characters() {
535 let margins = create_test_margins();
536 let mut context = TextFlowContext::new(400.0, 600.0, margins);
537
538 context
539 .write_wrapped("Text with (parentheses) and \\backslash\\")
540 .unwrap();
541
542 let ops = context.operations();
543 assert!(ops.contains("\\(parentheses\\)"));
545 assert!(ops.contains("\\\\backslash\\\\"));
546 }
547
548 #[test]
549 fn test_write_wrapped_newlines_tabs() {
550 let margins = create_test_margins();
551 let mut context = TextFlowContext::new(400.0, 600.0, margins);
552
553 context.write_wrapped("Line1\nLine2\tTabbed").unwrap();
554
555 let ops = context.operations();
556 assert!(ops.contains("\\n") || ops.contains("\\t"));
558 }
559
560 #[test]
561 fn test_write_wrapped_very_long_word() {
562 let margins = create_test_margins();
563 let mut context = TextFlowContext::new(200.0, 600.0, margins); let long_word = "a".repeat(100);
566 context.write_wrapped(&long_word).unwrap();
567
568 let ops = context.operations();
569 assert!(ops.contains("BT\n"));
570 assert!(ops.contains(&long_word));
571 }
572
573 #[test]
574 fn test_write_wrapped_cursor_movement() {
575 let margins = create_test_margins();
576 let mut context = TextFlowContext::new(400.0, 600.0, margins);
577
578 let initial_y = context.cursor_y;
579
580 context.write_wrapped("Line 1").unwrap();
581 let y_after_line1 = context.cursor_y;
582
583 context.write_wrapped("Line 2").unwrap();
584 let y_after_line2 = context.cursor_y;
585
586 assert!(y_after_line1 < initial_y);
588 assert!(y_after_line2 < y_after_line1);
589 }
590
591 #[test]
592 fn test_write_paragraph_spacing() {
593 let margins = create_test_margins();
594 let mut context = TextFlowContext::new(400.0, 600.0, margins);
595
596 let initial_y = context.cursor_y;
597 context.write_paragraph("Paragraph 1").unwrap();
598 let y_after_p1 = context.cursor_y;
599
600 context.write_paragraph("Paragraph 2").unwrap();
601 let y_after_p2 = context.cursor_y;
602
603 let spacing1 = initial_y - y_after_p1;
605 let spacing2 = y_after_p1 - y_after_p2;
606
607 assert!(spacing1 > 0.0);
608 assert!(spacing2 > 0.0);
609 }
610
611 #[test]
612 fn test_multiple_newlines() {
613 let margins = create_test_margins();
614 let mut context = TextFlowContext::new(400.0, 600.0, margins);
615
616 let initial_y = context.cursor_y;
617
618 context.newline();
619 let y1 = context.cursor_y;
620
621 context.newline();
622 let y2 = context.cursor_y;
623
624 context.newline();
625 let y3 = context.cursor_y;
626
627 let spacing1 = initial_y - y1;
629 let spacing2 = y1 - y2;
630 let spacing3 = y2 - y3;
631
632 assert!((spacing1 - spacing2).abs() < 1e-10);
634 assert!((spacing2 - spacing3).abs() < 1e-10);
635 assert!((spacing1 - context.font_size * context.line_height).abs() < 1e-10);
636 }
637
638 #[test]
639 fn test_content_width_different_margins() {
640 let margins = Margins {
641 left: 30.0,
642 right: 70.0,
643 top: 40.0,
644 bottom: 60.0,
645 };
646 let context = TextFlowContext::new(500.0, 700.0, margins);
647
648 let content_width = context.content_width();
649 assert_eq!(content_width, 400.0); }
651
652 #[test]
653 fn test_custom_line_height() {
654 let margins = create_test_margins();
655 let mut context = TextFlowContext::new(400.0, 600.0, margins);
656
657 context.set_line_height(2.0);
658
659 let initial_y = context.cursor_y;
660 context.newline();
661 let y_after = context.cursor_y;
662
663 let spacing = initial_y - y_after;
664 assert_eq!(spacing, context.font_size * 2.0); }
666
667 #[test]
668 fn test_different_fonts() {
669 let margins = create_test_margins();
670 let mut context = TextFlowContext::new(400.0, 600.0, margins);
671
672 let fonts = vec![
673 Font::Helvetica,
674 Font::HelveticaBold,
675 Font::TimesRoman,
676 Font::TimesBold,
677 Font::Courier,
678 Font::CourierBold,
679 ];
680
681 for font in fonts {
682 context.clear();
683 let font_name = font.pdf_name();
684 context.set_font(font, 14.0);
685 context.write_wrapped("Test text").unwrap();
686
687 let ops = context.operations();
688 assert!(ops.contains(&format!("/{font_name} 14 Tf")));
689 }
690 }
691
692 #[test]
693 fn test_font_size_variations() {
694 let margins = create_test_margins();
695 let mut context = TextFlowContext::new(400.0, 600.0, margins);
696
697 let sizes = vec![8.0, 10.0, 12.0, 14.0, 16.0, 24.0, 36.0];
698
699 for size in sizes {
700 context.clear();
701 context.set_font(Font::Helvetica, size);
702 context.write_wrapped("Test").unwrap();
703
704 let ops = context.operations();
705 assert!(ops.contains(&format!("/Helvetica {size} Tf")));
706 }
707 }
708
709 #[test]
710 fn test_at_position_edge_cases() {
711 let margins = create_test_margins();
712 let mut context = TextFlowContext::new(400.0, 600.0, margins);
713
714 context.at(0.0, 0.0);
716 assert_eq!(context.cursor_position(), (0.0, 0.0));
717
718 context.at(-10.0, -20.0);
720 assert_eq!(context.cursor_position(), (-10.0, -20.0));
721
722 context.at(10000.0, 20000.0);
724 assert_eq!(context.cursor_position(), (10000.0, 20000.0));
725 }
726
727 #[test]
728 fn test_write_wrapped_with_narrow_content() {
729 let margins = Margins {
730 left: 190.0,
731 right: 190.0,
732 top: 50.0,
733 bottom: 50.0,
734 };
735 let mut context = TextFlowContext::new(400.0, 600.0, margins);
736
737 context
739 .write_wrapped("This text should wrap a lot")
740 .unwrap();
741
742 let ops = context.operations();
743 let bt_count = ops.matches("BT\n").count();
745 assert!(bt_count > 1);
746 }
747
748 #[test]
749 fn test_justified_text_single_word_line() {
750 let margins = create_test_margins();
751 let mut context = TextFlowContext::new(400.0, 600.0, margins);
752
753 context.set_alignment(TextAlign::Justified);
754 context.write_wrapped("SingleWord").unwrap();
755
756 let ops = context.operations();
757 assert!(!ops.contains(" Tw") || ops.contains("0 Tw"));
759 }
760
761 #[test]
762 fn test_justified_text_last_line() {
763 let margins = create_test_margins();
764 let mut context = TextFlowContext::new(400.0, 600.0, margins);
765
766 context.set_alignment(TextAlign::Justified);
767 context.write_wrapped("This is a test of justified text alignment where the last line should not be justified").unwrap();
769
770 let ops = context.operations();
771 assert!(ops.contains("0 Tw"));
773 }
774
775 #[test]
776 fn test_generate_operations_encoding() {
777 let margins = create_test_margins();
778 let mut context = TextFlowContext::new(400.0, 600.0, margins);
779
780 context.write_wrapped("UTF-8 Text: Ñ").unwrap();
781
782 let ops_bytes = context.generate_operations();
783 let ops_string = String::from_utf8(ops_bytes.clone()).unwrap();
784
785 assert_eq!(ops_bytes, context.operations().as_bytes());
786 assert_eq!(ops_string, context.operations());
787 }
788
789 #[test]
790 fn test_clear_resets_operations_only() {
791 let margins = create_test_margins();
792 let mut context = TextFlowContext::new(400.0, 600.0, margins);
793
794 context.set_font(Font::TimesBold, 18.0);
795 context.set_alignment(TextAlign::Right);
796 context.at(100.0, 200.0);
797 context.write_wrapped("Text").unwrap();
798
799 context.clear();
800
801 assert!(context.operations().is_empty());
803
804 assert_eq!(context.current_font, Font::TimesBold);
806 assert_eq!(context.font_size, 18.0);
807 assert_eq!(context.alignment(), TextAlign::Right);
808 let (x, y) = context.cursor_position();
810 assert_eq!(x, 100.0); assert!(y < 200.0); }
813
814 #[test]
815 fn test_long_text_wrapping() {
816 let margins = create_test_margins();
817 let mut context = TextFlowContext::new(400.0, 600.0, margins);
818
819 let long_text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. \
820 Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \
821 Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.";
822
823 context.write_wrapped(long_text).unwrap();
824
825 let ops = context.operations();
826 let tj_count = ops.matches(") Tj").count();
828 assert!(tj_count > 1);
829 }
830
831 #[test]
832 fn test_empty_operations_initially() {
833 let margins = create_test_margins();
834 let context = TextFlowContext::new(400.0, 600.0, margins);
835
836 assert!(context.operations().is_empty());
837 assert_eq!(context.generate_operations().len(), 0);
838 }
839
840 #[test]
841 fn test_write_paragraph_empty() {
842 let margins = create_test_margins();
843 let mut context = TextFlowContext::new(400.0, 600.0, margins);
844
845 let initial_y = context.cursor_y;
846 context.write_paragraph("").unwrap();
847
848 assert!(context.cursor_y < initial_y);
850 }
851
852 #[test]
853 fn test_extreme_line_height() {
854 let margins = create_test_margins();
855 let mut context = TextFlowContext::new(400.0, 600.0, margins);
856
857 context.set_line_height(0.1);
859 let initial_y = context.cursor_y;
860 context.newline();
861 assert_eq!(context.cursor_y, initial_y - context.font_size * 0.1);
862
863 context.set_line_height(10.0);
865 let initial_y2 = context.cursor_y;
866 context.newline();
867 assert_eq!(context.cursor_y, initial_y2 - context.font_size * 10.0);
868 }
869
870 #[test]
871 fn test_zero_content_width() {
872 let margins = Margins {
873 left: 200.0,
874 right: 200.0,
875 top: 50.0,
876 bottom: 50.0,
877 };
878 let context = TextFlowContext::new(400.0, 600.0, margins);
879
880 assert_eq!(context.content_width(), 0.0);
881 }
882
883 #[test]
884 fn test_cursor_x_reset_on_newline() {
885 let margins = create_test_margins();
886 let mut context = TextFlowContext::new(400.0, 600.0, margins.clone());
887
888 context.at(250.0, 300.0); context.newline();
890
891 assert_eq!(context.cursor_x, margins.left);
893 assert_eq!(
895 context.cursor_y,
896 300.0 - context.font_size * context.line_height
897 );
898 }
899
900 #[test]
903 fn test_available_width_respects_cursor_x() {
904 let margins = create_test_margins(); let mut context = TextFlowContext::new(400.0, 600.0, margins);
907
908 assert_eq!(context.available_width(), 300.0);
910
911 context.at(200.0, 500.0);
913 assert_eq!(context.available_width(), 150.0);
914 }
915
916 #[test]
917 fn test_available_width_clamps_to_zero() {
918 let margins = create_test_margins(); let mut context = TextFlowContext::new(400.0, 600.0, margins);
921
922 context.at(380.0, 500.0);
924 assert_eq!(context.available_width(), 0.0);
925 }
926
927 #[test]
928 fn test_write_wrapped_at_x_limits_available_width() {
929 let margins = create_test_margins();
933 let mut context = TextFlowContext::new(400.0, 600.0, margins);
934
935 context.set_font(Font::Helvetica, 12.0);
936 context.at(250.0, 500.0);
938 context.write_wrapped("Hello World Hello World").unwrap();
939
940 let ops = context.operations();
941 let bt_count = ops.matches("BT\n").count();
943 assert!(
944 bt_count > 1,
945 "Expected wrapping (multiple lines), got {bt_count} BT blocks. ops:\n{ops}"
946 );
947 }
948
949 #[test]
950 fn test_write_wrapped_respects_cursor_x_offset() {
951 let margins = Margins {
953 left: 50.0,
954 right: 50.0,
955 top: 50.0,
956 bottom: 50.0,
957 };
958 let mut context = TextFlowContext::new(600.0, 800.0, margins);
959
960 context.set_font(Font::Helvetica, 12.0);
961 context.at(300.0, 700.0);
962 context
963 .write_wrapped("Hello World Foo Bar Baz Qux")
964 .unwrap();
965
966 let ops = context.operations();
967 for line in ops.lines() {
969 if line.ends_with(" Td") {
970 let parts: Vec<&str> = line.split_whitespace().collect();
971 if parts.len() >= 3 {
972 let x: f64 = parts[0].parse().expect("Td x should be a number");
973 assert!(
974 x >= 300.0 - 1e-6,
975 "Expected Td x >= 300.0 but got {x}. ops:\n{ops}"
976 );
977 }
978 }
979 }
980 }
981}