1use crate::error::PdfError;
7use crate::graphics::{Color, GraphicsContext};
8use crate::text::{Font, TextAlign};
9
10#[derive(Debug, Clone)]
12pub struct ColumnLayout {
13 column_count: usize,
15 column_widths: Vec<f64>,
17 column_gap: f64,
19 total_width: f64,
21 options: ColumnOptions,
23}
24
25#[derive(Debug, Clone)]
27pub struct ColumnOptions {
28 pub font: Font,
30 pub font_size: f64,
32 pub line_height: f64,
34 pub text_color: Color,
36 pub text_align: TextAlign,
38 pub balance_columns: bool,
40 pub show_separators: bool,
42 pub separator_color: Color,
44 pub separator_width: f64,
46}
47
48impl Default for ColumnOptions {
49 fn default() -> Self {
50 Self {
51 font: Font::Helvetica,
52 font_size: 10.0,
53 line_height: 1.2,
54 text_color: Color::black(),
55 text_align: TextAlign::Left,
56 balance_columns: true,
57 show_separators: false,
58 separator_color: Color::gray(0.7),
59 separator_width: 0.5,
60 }
61 }
62}
63
64#[derive(Debug, Clone)]
66pub struct ColumnContent {
67 text: String,
69 formatting: Vec<TextFormat>,
71}
72
73#[derive(Debug, Clone)]
75pub struct TextFormat {
76 #[allow(dead_code)]
78 start: usize,
79 #[allow(dead_code)]
81 end: usize,
82 font: Option<Font>,
84 font_size: Option<f64>,
86 color: Option<Color>,
88 bold: bool,
90 italic: bool,
92}
93
94#[derive(Debug)]
96pub struct ColumnFlowContext {
97 current_column: usize,
99 column_positions: Vec<f64>,
101 column_heights: Vec<f64>,
103 column_contents: Vec<Vec<String>>,
105}
106
107impl ColumnLayout {
108 pub fn new(column_count: usize, total_width: f64, column_gap: f64) -> Self {
110 if column_count == 0 {
111 panic!("Column count must be greater than 0");
112 }
113
114 let available_width = total_width - (column_gap * (column_count - 1) as f64);
115 let column_width = available_width / column_count as f64;
116 let column_widths = vec![column_width; column_count];
117
118 Self {
119 column_count,
120 column_widths,
121 column_gap,
122 total_width,
123 options: ColumnOptions::default(),
124 }
125 }
126
127 pub fn with_custom_widths(column_widths: Vec<f64>, column_gap: f64) -> Self {
129 let column_count = column_widths.len();
130 if column_count == 0 {
131 panic!("Must have at least one column");
132 }
133
134 let content_width: f64 = column_widths.iter().sum();
135 let total_width = content_width + (column_gap * (column_count - 1) as f64);
136
137 Self {
138 column_count,
139 column_widths,
140 column_gap,
141 total_width,
142 options: ColumnOptions::default(),
143 }
144 }
145
146 pub fn set_options(&mut self, options: ColumnOptions) -> &mut Self {
148 self.options = options;
149 self
150 }
151
152 pub fn column_count(&self) -> usize {
154 self.column_count
155 }
156
157 pub fn total_width(&self) -> f64 {
159 self.total_width
160 }
161
162 pub fn column_width(&self, index: usize) -> Option<f64> {
164 self.column_widths.get(index).copied()
165 }
166
167 pub fn column_x_position(&self, index: usize) -> f64 {
169 let mut x = 0.0;
170 for i in 0..index.min(self.column_count) {
171 x += self.column_widths[i] + self.column_gap;
172 }
173 x
174 }
175
176 pub fn create_flow_context(&self, start_y: f64, column_height: f64) -> ColumnFlowContext {
178 ColumnFlowContext {
179 current_column: 0,
180 column_positions: vec![start_y; self.column_count],
181 column_heights: vec![column_height; self.column_count],
182 column_contents: vec![Vec::new(); self.column_count],
183 }
184 }
185
186 pub fn render(
188 &self,
189 graphics: &mut GraphicsContext,
190 content: &ColumnContent,
191 start_x: f64,
192 start_y: f64,
193 column_height: f64,
194 ) -> Result<(), PdfError> {
195 let mut flow_context = self.create_flow_context(start_y, column_height);
197
198 let words = self.split_text_into_words(&content.text);
200
201 self.flow_text_across_columns(&words, &mut flow_context)?;
203
204 for (col_index, column_content) in flow_context.column_contents.iter().enumerate() {
206 let column_x = start_x + self.column_x_position(col_index);
207 self.render_column(graphics, column_content, column_x, start_y)?;
208 }
209
210 if self.options.show_separators {
212 self.draw_separators(graphics, start_x, start_y, column_height)?;
213 }
214
215 Ok(())
216 }
217
218 fn split_text_into_words(&self, text: &str) -> Vec<String> {
220 text.split_whitespace()
221 .map(|word| word.to_string())
222 .collect()
223 }
224
225 fn flow_text_across_columns(
227 &self,
228 words: &[String],
229 flow_context: &mut ColumnFlowContext,
230 ) -> Result<(), PdfError> {
231 let mut current_line = String::new();
232 let line_height = self.options.font_size * self.options.line_height;
233
234 for word in words {
235 let test_line = if current_line.is_empty() {
237 word.clone()
238 } else {
239 format!("{current_line} {word}")
240 };
241
242 let line_width = self.estimate_text_width(&test_line);
243 let column_width = self.column_widths[flow_context.current_column];
244
245 if line_width <= column_width || current_line.is_empty() {
246 current_line = test_line;
248 } else {
249 if !current_line.is_empty() {
251 flow_context.column_contents[flow_context.current_column]
253 .push(current_line.clone());
254 flow_context.column_positions[flow_context.current_column] -= line_height;
255
256 if flow_context.column_positions[flow_context.current_column]
258 < flow_context.column_heights[flow_context.current_column] - line_height
259 {
260 if flow_context.current_column + 1 < self.column_count {
262 flow_context.current_column += 1;
263 }
264 }
265 }
266 current_line = word.clone();
267 }
268 }
269
270 if !current_line.is_empty() {
272 flow_context.column_contents[flow_context.current_column].push(current_line);
273 }
274
275 if self.options.balance_columns {
277 self.balance_column_content(flow_context)?;
278 }
279
280 Ok(())
281 }
282
283 fn estimate_text_width(&self, text: &str) -> f64 {
285 text.len() as f64 * self.options.font_size * 0.6
287 }
288
289 fn balance_column_content(&self, flow_context: &mut ColumnFlowContext) -> Result<(), PdfError> {
291 let mut all_lines = Vec::new();
293 for column in &flow_context.column_contents {
294 all_lines.extend(column.iter().cloned());
295 }
296
297 for column in &mut flow_context.column_contents {
299 column.clear();
300 }
301
302 let lines_per_column = all_lines.len().div_ceil(self.column_count);
304
305 for (line_index, line) in all_lines.into_iter().enumerate() {
306 let column_index = (line_index / lines_per_column).min(self.column_count - 1);
307 flow_context.column_contents[column_index].push(line);
308 }
309
310 Ok(())
311 }
312
313 fn render_column(
315 &self,
316 graphics: &mut GraphicsContext,
317 lines: &[String],
318 column_x: f64,
319 start_y: f64,
320 ) -> Result<(), PdfError> {
321 let line_height = self.options.font_size * self.options.line_height;
322 let mut current_y = start_y;
323
324 graphics.save_state();
325 graphics.set_font(self.options.font.clone(), self.options.font_size);
326 graphics.set_fill_color(self.options.text_color);
327
328 for line in lines {
329 graphics.begin_text();
330
331 match self.options.text_align {
332 TextAlign::Left => {
333 graphics.set_text_position(column_x, current_y);
334 graphics.show_text(line)?;
335 }
336 TextAlign::Center => {
337 let line_width = self.estimate_text_width(line);
338 let column_width = self.column_widths[0]; let text_x = column_x + (column_width - line_width) / 2.0;
340 graphics.set_text_position(text_x, current_y);
341 graphics.show_text(line)?;
342 }
343 TextAlign::Right => {
344 let line_width = self.estimate_text_width(line);
345 let column_width = self.column_widths[0]; let text_x = column_x + column_width - line_width;
347 graphics.set_text_position(text_x, current_y);
348 graphics.show_text(line)?;
349 }
350 TextAlign::Justified => {
351 let column_width = self.column_widths[0]; graphics.set_text_position(column_x, current_y);
353 graphics.show_justified_text(line, column_width)?;
354 }
355 };
356 graphics.end_text();
357
358 current_y -= line_height;
359 }
360
361 graphics.restore_state();
362 Ok(())
363 }
364
365 fn draw_separators(
367 &self,
368 graphics: &mut GraphicsContext,
369 start_x: f64,
370 start_y: f64,
371 column_height: f64,
372 ) -> Result<(), PdfError> {
373 if self.column_count <= 1 {
374 return Ok(());
375 }
376
377 graphics.save_state();
378 graphics.set_stroke_color(self.options.separator_color);
379 graphics.set_line_width(self.options.separator_width);
380
381 for i in 0..self.column_count - 1 {
382 let separator_x = start_x
383 + self.column_x_position(i)
384 + self.column_widths[i]
385 + (self.column_gap / 2.0);
386
387 graphics.move_to(separator_x, start_y);
388 graphics.line_to(separator_x, start_y - column_height);
389 graphics.stroke();
390 }
391
392 graphics.restore_state();
393 Ok(())
394 }
395}
396
397impl ColumnContent {
398 pub fn new(text: impl Into<String>) -> Self {
400 Self {
401 text: text.into(),
402 formatting: Vec::new(),
403 }
404 }
405
406 pub fn add_format(&mut self, format: TextFormat) -> &mut Self {
408 self.formatting.push(format);
409 self
410 }
411
412 pub fn text(&self) -> &str {
414 &self.text
415 }
416}
417
418impl TextFormat {
419 pub fn new(start: usize, end: usize) -> Self {
421 Self {
422 start,
423 end,
424 font: None,
425 font_size: None,
426 color: None,
427 bold: false,
428 italic: false,
429 }
430 }
431
432 pub fn with_font(mut self, font: Font) -> Self {
434 self.font = Some(font);
435 self
436 }
437
438 pub fn with_font_size(mut self, size: f64) -> Self {
440 self.font_size = Some(size);
441 self
442 }
443
444 pub fn with_color(mut self, color: Color) -> Self {
446 self.color = Some(color);
447 self
448 }
449
450 pub fn bold(mut self) -> Self {
452 self.bold = true;
453 self
454 }
455
456 pub fn italic(mut self) -> Self {
458 self.italic = true;
459 self
460 }
461}
462
463#[cfg(test)]
464mod tests {
465 use super::*;
466
467 #[test]
468 fn test_column_layout_creation() {
469 let layout = ColumnLayout::new(3, 600.0, 20.0);
470 assert_eq!(layout.column_count(), 3);
471 assert_eq!(layout.total_width(), 600.0);
472
473 assert!((layout.column_width(0).unwrap() - 186.67).abs() < 0.01);
475 }
476
477 #[test]
478 fn test_custom_column_widths() {
479 let layout = ColumnLayout::with_custom_widths(vec![200.0, 150.0, 250.0], 15.0);
480 assert_eq!(layout.column_count(), 3);
481 assert_eq!(layout.total_width(), 630.0); assert_eq!(layout.column_width(0), Some(200.0));
483 assert_eq!(layout.column_width(1), Some(150.0));
484 assert_eq!(layout.column_width(2), Some(250.0));
485 }
486
487 #[test]
488 fn test_column_x_positions() {
489 let layout = ColumnLayout::with_custom_widths(vec![100.0, 200.0, 150.0], 20.0);
490 assert_eq!(layout.column_x_position(0), 0.0);
491 assert_eq!(layout.column_x_position(1), 120.0); assert_eq!(layout.column_x_position(2), 340.0); }
494
495 #[test]
496 fn test_column_options_default() {
497 let options = ColumnOptions::default();
498 assert_eq!(options.font, Font::Helvetica);
499 assert_eq!(options.font_size, 10.0);
500 assert_eq!(options.line_height, 1.2);
501 assert!(options.balance_columns);
502 assert!(!options.show_separators);
503 }
504
505 #[test]
506 fn test_column_content() {
507 let mut content = ColumnContent::new("Hello world");
508 assert_eq!(content.text(), "Hello world");
509
510 content.add_format(TextFormat::new(0, 5).bold());
511 assert_eq!(content.formatting.len(), 1);
512 assert!(content.formatting[0].bold);
513 }
514
515 #[test]
516 fn test_text_format() {
517 let format = TextFormat::new(0, 10)
518 .with_font(Font::HelveticaBold)
519 .with_font_size(14.0)
520 .with_color(Color::red())
521 .bold()
522 .italic();
523
524 assert_eq!(format.start, 0);
525 assert_eq!(format.end, 10);
526 assert_eq!(format.font, Some(Font::HelveticaBold));
527 assert_eq!(format.font_size, Some(14.0));
528 assert_eq!(format.color, Some(Color::red()));
529 assert!(format.bold);
530 assert!(format.italic);
531 }
532
533 #[test]
534 fn test_flow_context_creation() {
535 let layout = ColumnLayout::new(2, 400.0, 20.0);
536 let context = layout.create_flow_context(100.0, 500.0);
537
538 assert_eq!(context.current_column, 0);
539 assert_eq!(context.column_positions.len(), 2);
540 assert_eq!(context.column_heights.len(), 2);
541 assert_eq!(context.column_contents.len(), 2);
542 assert_eq!(context.column_positions[0], 100.0);
543 assert_eq!(context.column_heights[0], 500.0);
544 }
545
546 #[test]
547 fn test_text_width_estimation() {
548 let layout = ColumnLayout::new(1, 100.0, 0.0);
549 let width = layout.estimate_text_width("Hello");
550 assert_eq!(width, 5.0 * 10.0 * 0.6); }
552
553 #[test]
554 fn test_split_text_into_words() {
555 let layout = ColumnLayout::new(1, 100.0, 0.0);
556 let words = layout.split_text_into_words("Hello world, this is a test");
557 assert_eq!(words, vec!["Hello", "world,", "this", "is", "a", "test"]);
558 }
559
560 #[test]
561 fn test_column_layout_with_options() {
562 let mut layout = ColumnLayout::new(2, 400.0, 20.0);
563 let options = ColumnOptions {
564 font: Font::TimesBold,
565 font_size: 12.0,
566 show_separators: true,
567 ..Default::default()
568 };
569
570 layout.set_options(options);
571 assert_eq!(layout.options.font, Font::TimesBold);
572 assert_eq!(layout.options.font_size, 12.0);
573 assert!(layout.options.show_separators);
574 }
575
576 #[test]
577 #[should_panic(expected = "Column count must be greater than 0")]
578 fn test_zero_columns_panic() {
579 ColumnLayout::new(0, 100.0, 10.0);
580 }
581
582 #[test]
583 #[should_panic(expected = "Must have at least one column")]
584 fn test_empty_custom_widths_panic() {
585 ColumnLayout::with_custom_widths(vec![], 10.0);
586 }
587
588 #[test]
589 fn test_column_width_out_of_bounds() {
590 let layout = ColumnLayout::new(2, 400.0, 20.0);
591 assert_eq!(layout.column_width(5), None);
592 }
593}