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 let text_x = match self.options.text_align {
332 TextAlign::Left => column_x,
333 TextAlign::Center => {
334 let line_width = self.estimate_text_width(line);
335 let column_width = self.column_widths[0]; column_x + (column_width - line_width) / 2.0
337 }
338 TextAlign::Right => {
339 let line_width = self.estimate_text_width(line);
340 let column_width = self.column_widths[0]; column_x + column_width - line_width
342 }
343 TextAlign::Justified => column_x, };
345
346 graphics.set_text_position(text_x, current_y);
347 graphics.show_text(line)?;
348 graphics.end_text();
349
350 current_y -= line_height;
351 }
352
353 graphics.restore_state();
354 Ok(())
355 }
356
357 fn draw_separators(
359 &self,
360 graphics: &mut GraphicsContext,
361 start_x: f64,
362 start_y: f64,
363 column_height: f64,
364 ) -> Result<(), PdfError> {
365 if self.column_count <= 1 {
366 return Ok(());
367 }
368
369 graphics.save_state();
370 graphics.set_stroke_color(self.options.separator_color);
371 graphics.set_line_width(self.options.separator_width);
372
373 for i in 0..self.column_count - 1 {
374 let separator_x = start_x
375 + self.column_x_position(i)
376 + self.column_widths[i]
377 + (self.column_gap / 2.0);
378
379 graphics.move_to(separator_x, start_y);
380 graphics.line_to(separator_x, start_y - column_height);
381 graphics.stroke();
382 }
383
384 graphics.restore_state();
385 Ok(())
386 }
387}
388
389impl ColumnContent {
390 pub fn new(text: impl Into<String>) -> Self {
392 Self {
393 text: text.into(),
394 formatting: Vec::new(),
395 }
396 }
397
398 pub fn add_format(&mut self, format: TextFormat) -> &mut Self {
400 self.formatting.push(format);
401 self
402 }
403
404 pub fn text(&self) -> &str {
406 &self.text
407 }
408}
409
410impl TextFormat {
411 pub fn new(start: usize, end: usize) -> Self {
413 Self {
414 start,
415 end,
416 font: None,
417 font_size: None,
418 color: None,
419 bold: false,
420 italic: false,
421 }
422 }
423
424 pub fn with_font(mut self, font: Font) -> Self {
426 self.font = Some(font);
427 self
428 }
429
430 pub fn with_font_size(mut self, size: f64) -> Self {
432 self.font_size = Some(size);
433 self
434 }
435
436 pub fn with_color(mut self, color: Color) -> Self {
438 self.color = Some(color);
439 self
440 }
441
442 pub fn bold(mut self) -> Self {
444 self.bold = true;
445 self
446 }
447
448 pub fn italic(mut self) -> Self {
450 self.italic = true;
451 self
452 }
453}
454
455#[cfg(test)]
456mod tests {
457 use super::*;
458
459 #[test]
460 fn test_column_layout_creation() {
461 let layout = ColumnLayout::new(3, 600.0, 20.0);
462 assert_eq!(layout.column_count(), 3);
463 assert_eq!(layout.total_width(), 600.0);
464
465 assert!((layout.column_width(0).unwrap() - 186.67).abs() < 0.01);
467 }
468
469 #[test]
470 fn test_custom_column_widths() {
471 let layout = ColumnLayout::with_custom_widths(vec![200.0, 150.0, 250.0], 15.0);
472 assert_eq!(layout.column_count(), 3);
473 assert_eq!(layout.total_width(), 630.0); assert_eq!(layout.column_width(0), Some(200.0));
475 assert_eq!(layout.column_width(1), Some(150.0));
476 assert_eq!(layout.column_width(2), Some(250.0));
477 }
478
479 #[test]
480 fn test_column_x_positions() {
481 let layout = ColumnLayout::with_custom_widths(vec![100.0, 200.0, 150.0], 20.0);
482 assert_eq!(layout.column_x_position(0), 0.0);
483 assert_eq!(layout.column_x_position(1), 120.0); assert_eq!(layout.column_x_position(2), 340.0); }
486
487 #[test]
488 fn test_column_options_default() {
489 let options = ColumnOptions::default();
490 assert_eq!(options.font, Font::Helvetica);
491 assert_eq!(options.font_size, 10.0);
492 assert_eq!(options.line_height, 1.2);
493 assert!(options.balance_columns);
494 assert!(!options.show_separators);
495 }
496
497 #[test]
498 fn test_column_content() {
499 let mut content = ColumnContent::new("Hello world");
500 assert_eq!(content.text(), "Hello world");
501
502 content.add_format(TextFormat::new(0, 5).bold());
503 assert_eq!(content.formatting.len(), 1);
504 assert!(content.formatting[0].bold);
505 }
506
507 #[test]
508 fn test_text_format() {
509 let format = TextFormat::new(0, 10)
510 .with_font(Font::HelveticaBold)
511 .with_font_size(14.0)
512 .with_color(Color::red())
513 .bold()
514 .italic();
515
516 assert_eq!(format.start, 0);
517 assert_eq!(format.end, 10);
518 assert_eq!(format.font, Some(Font::HelveticaBold));
519 assert_eq!(format.font_size, Some(14.0));
520 assert_eq!(format.color, Some(Color::red()));
521 assert!(format.bold);
522 assert!(format.italic);
523 }
524
525 #[test]
526 fn test_flow_context_creation() {
527 let layout = ColumnLayout::new(2, 400.0, 20.0);
528 let context = layout.create_flow_context(100.0, 500.0);
529
530 assert_eq!(context.current_column, 0);
531 assert_eq!(context.column_positions.len(), 2);
532 assert_eq!(context.column_heights.len(), 2);
533 assert_eq!(context.column_contents.len(), 2);
534 assert_eq!(context.column_positions[0], 100.0);
535 assert_eq!(context.column_heights[0], 500.0);
536 }
537
538 #[test]
539 fn test_text_width_estimation() {
540 let layout = ColumnLayout::new(1, 100.0, 0.0);
541 let width = layout.estimate_text_width("Hello");
542 assert_eq!(width, 5.0 * 10.0 * 0.6); }
544
545 #[test]
546 fn test_split_text_into_words() {
547 let layout = ColumnLayout::new(1, 100.0, 0.0);
548 let words = layout.split_text_into_words("Hello world, this is a test");
549 assert_eq!(words, vec!["Hello", "world,", "this", "is", "a", "test"]);
550 }
551
552 #[test]
553 fn test_column_layout_with_options() {
554 let mut layout = ColumnLayout::new(2, 400.0, 20.0);
555 let options = ColumnOptions {
556 font: Font::TimesBold,
557 font_size: 12.0,
558 show_separators: true,
559 ..Default::default()
560 };
561
562 layout.set_options(options);
563 assert_eq!(layout.options.font, Font::TimesBold);
564 assert_eq!(layout.options.font_size, 12.0);
565 assert!(layout.options.show_separators);
566 }
567
568 #[test]
569 #[should_panic(expected = "Column count must be greater than 0")]
570 fn test_zero_columns_panic() {
571 ColumnLayout::new(0, 100.0, 10.0);
572 }
573
574 #[test]
575 #[should_panic(expected = "Must have at least one column")]
576 fn test_empty_custom_widths_panic() {
577 ColumnLayout::with_custom_widths(vec![], 10.0);
578 }
579
580 #[test]
581 fn test_column_width_out_of_bounds() {
582 let layout = ColumnLayout::new(2, 400.0, 20.0);
583 assert_eq!(layout.column_width(5), None);
584 }
585}