1use crate::error::PdfError;
7use crate::graphics::{Color, GraphicsContext};
8use crate::text::{measure_text, Font, TextAlign};
9
10#[derive(Debug, Clone)]
12pub struct Table {
13 rows: Vec<TableRow>,
15 column_widths: Vec<f64>,
17 position: (f64, f64),
19 options: TableOptions,
21}
22
23#[derive(Debug, Clone)]
25pub struct TableOptions {
26 pub border_width: f64,
28 pub border_color: Color,
30 pub cell_padding: f64,
32 pub row_height: f64,
34 pub font: Font,
36 pub font_size: f64,
38 pub text_color: Color,
40 pub header_style: Option<HeaderStyle>,
42}
43
44#[derive(Debug, Clone)]
46pub struct HeaderStyle {
47 pub background_color: Color,
49 pub text_color: Color,
51 pub font: Font,
53 pub bold: bool,
55}
56
57#[derive(Debug, Clone)]
59pub struct TableRow {
60 cells: Vec<TableCell>,
62 is_header: bool,
64}
65
66#[derive(Debug, Clone)]
68pub struct TableCell {
69 content: String,
71 align: TextAlign,
73 colspan: usize,
75}
76
77impl Default for TableOptions {
78 fn default() -> Self {
79 Self {
80 border_width: 1.0,
81 border_color: Color::black(),
82 cell_padding: 5.0,
83 row_height: 0.0, font: Font::Helvetica,
85 font_size: 10.0,
86 text_color: Color::black(),
87 header_style: None,
88 }
89 }
90}
91
92impl Table {
93 pub fn new(column_widths: Vec<f64>) -> Self {
95 Self {
96 rows: Vec::new(),
97 column_widths,
98 position: (0.0, 0.0),
99 options: TableOptions::default(),
100 }
101 }
102
103 pub fn with_equal_columns(num_columns: usize, total_width: f64) -> Self {
105 let column_width = total_width / num_columns as f64;
106 let column_widths = vec![column_width; num_columns];
107 Self::new(column_widths)
108 }
109
110 pub fn set_position(&mut self, x: f64, y: f64) -> &mut Self {
112 self.position = (x, y);
113 self
114 }
115
116 pub fn set_options(&mut self, options: TableOptions) -> &mut Self {
118 self.options = options;
119 self
120 }
121
122 pub fn add_header_row(&mut self, cells: Vec<String>) -> Result<&mut Self, PdfError> {
124 if cells.len() != self.column_widths.len() {
125 return Err(PdfError::InvalidStructure(
126 "Header cells count doesn't match column count".to_string(),
127 ));
128 }
129
130 let row_cells: Vec<TableCell> = cells
131 .into_iter()
132 .map(|content| TableCell {
133 content,
134 align: TextAlign::Center,
135 colspan: 1,
136 })
137 .collect();
138
139 self.rows.push(TableRow {
140 cells: row_cells,
141 is_header: true,
142 });
143
144 Ok(self)
145 }
146
147 pub fn add_row(&mut self, cells: Vec<String>) -> Result<&mut Self, PdfError> {
149 self.add_row_with_alignment(cells, TextAlign::Left)
150 }
151
152 pub fn add_row_with_alignment(
154 &mut self,
155 cells: Vec<String>,
156 align: TextAlign,
157 ) -> Result<&mut Self, PdfError> {
158 if cells.len() != self.column_widths.len() {
159 return Err(PdfError::InvalidStructure(
160 "Row cells count doesn't match column count".to_string(),
161 ));
162 }
163
164 let row_cells: Vec<TableCell> = cells
165 .into_iter()
166 .map(|content| TableCell {
167 content,
168 align,
169 colspan: 1,
170 })
171 .collect();
172
173 self.rows.push(TableRow {
174 cells: row_cells,
175 is_header: false,
176 });
177
178 Ok(self)
179 }
180
181 pub fn add_custom_row(&mut self, cells: Vec<TableCell>) -> Result<&mut Self, PdfError> {
183 let total_colspan: usize = cells.iter().map(|c| c.colspan).sum();
185 if total_colspan != self.column_widths.len() {
186 return Err(PdfError::InvalidStructure(
187 "Total colspan doesn't match column count".to_string(),
188 ));
189 }
190
191 self.rows.push(TableRow {
192 cells,
193 is_header: false,
194 });
195
196 Ok(self)
197 }
198
199 fn calculate_row_height(&self, _row: &TableRow) -> f64 {
201 if self.options.row_height > 0.0 {
202 self.options.row_height
203 } else {
204 self.options.font_size + (self.options.cell_padding * 2.0)
206 }
207 }
208
209 pub fn get_height(&self) -> f64 {
211 self.rows
212 .iter()
213 .map(|row| self.calculate_row_height(row))
214 .sum()
215 }
216
217 pub fn get_width(&self) -> f64 {
219 self.column_widths.iter().sum()
220 }
221
222 pub fn render(&self, graphics: &mut GraphicsContext) -> Result<(), PdfError> {
224 let (start_x, start_y) = self.position;
225 let mut current_y = start_y;
226
227 for row in self.rows.iter() {
229 let row_height = self.calculate_row_height(row);
230 let mut current_x = start_x;
231
232 let use_header_style = row.is_header && self.options.header_style.is_some();
234 let header_style = self.options.header_style.as_ref();
235
236 let mut col_index = 0;
238 for cell in &row.cells {
239 let mut cell_width = 0.0;
241 for i in 0..cell.colspan {
242 if col_index + i < self.column_widths.len() {
243 cell_width += self.column_widths[col_index + i];
244 }
245 }
246
247 if use_header_style {
249 if let Some(style) = header_style {
250 graphics.save_state();
251 graphics.set_fill_color(style.background_color);
252 graphics.rectangle(current_x, current_y, cell_width, row_height);
253 graphics.fill();
254 graphics.restore_state();
255 }
256 }
257
258 graphics.save_state();
260 graphics.set_stroke_color(self.options.border_color);
261 graphics.set_line_width(self.options.border_width);
262 graphics.rectangle(current_x, current_y, cell_width, row_height);
263 graphics.stroke();
264 graphics.restore_state();
265
266 let text_x = current_x + self.options.cell_padding;
268 let text_y =
269 current_y + row_height - self.options.cell_padding - self.options.font_size;
270 let text_width = cell_width - (2.0 * self.options.cell_padding);
271
272 graphics.save_state();
273
274 if use_header_style {
276 if let Some(style) = header_style {
277 let font = if style.bold {
278 match style.font {
279 Font::Helvetica => Font::HelveticaBold,
280 Font::TimesRoman => Font::TimesBold,
281 Font::Courier => Font::CourierBold,
282 _ => style.font.clone(),
283 }
284 } else {
285 style.font.clone()
286 };
287 graphics.set_font(font, self.options.font_size);
288 graphics.set_fill_color(style.text_color);
289 }
290 } else {
291 graphics.set_font(self.options.font.clone(), self.options.font_size);
292 graphics.set_fill_color(self.options.text_color);
293 }
294
295 match cell.align {
297 TextAlign::Left => {
298 graphics.begin_text();
299 graphics.set_text_position(text_x, text_y);
300 graphics.show_text(&cell.content)?;
301 graphics.end_text();
302 }
303 TextAlign::Center => {
304 let font_to_measure = if use_header_style {
306 if let Some(style) = header_style {
307 if style.bold {
308 match style.font {
309 Font::Helvetica => Font::HelveticaBold,
310 Font::TimesRoman => Font::TimesBold,
311 Font::Courier => Font::CourierBold,
312 _ => style.font.clone(),
313 }
314 } else {
315 style.font.clone()
316 }
317 } else {
318 self.options.font.clone()
319 }
320 } else {
321 self.options.font.clone()
322 };
323
324 let text_width_measured =
325 measure_text(&cell.content, font_to_measure, self.options.font_size);
326 let centered_x = text_x + (text_width - text_width_measured) / 2.0;
327 graphics.begin_text();
328 graphics.set_text_position(centered_x, text_y);
329 graphics.show_text(&cell.content)?;
330 graphics.end_text();
331 }
332 TextAlign::Right => {
333 let font_to_measure = if use_header_style {
335 if let Some(style) = header_style {
336 if style.bold {
337 match style.font {
338 Font::Helvetica => Font::HelveticaBold,
339 Font::TimesRoman => Font::TimesBold,
340 Font::Courier => Font::CourierBold,
341 _ => style.font.clone(),
342 }
343 } else {
344 style.font.clone()
345 }
346 } else {
347 self.options.font.clone()
348 }
349 } else {
350 self.options.font.clone()
351 };
352
353 let text_width_measured =
354 measure_text(&cell.content, font_to_measure, self.options.font_size);
355 let right_x = text_x + text_width - text_width_measured;
356 graphics.begin_text();
357 graphics.set_text_position(right_x, text_y);
358 graphics.show_text(&cell.content)?;
359 graphics.end_text();
360 }
361 TextAlign::Justified => {
362 graphics.begin_text();
364 graphics.set_text_position(text_x, text_y);
365 graphics.show_text(&cell.content)?;
366 graphics.end_text();
367 }
368 }
369
370 graphics.restore_state();
371
372 current_x += cell_width;
373 col_index += cell.colspan;
374 }
375
376 current_y += row_height;
377 }
378
379 Ok(())
380 }
381}
382
383impl TableRow {
384 #[allow(dead_code)]
386 pub fn new(cells: Vec<TableCell>) -> Self {
387 Self {
388 cells,
389 is_header: false,
390 }
391 }
392
393 #[allow(dead_code)]
395 pub fn header(cells: Vec<TableCell>) -> Self {
396 Self {
397 cells,
398 is_header: true,
399 }
400 }
401}
402
403impl TableCell {
404 pub fn new(content: String) -> Self {
406 Self {
407 content,
408 align: TextAlign::Left,
409 colspan: 1,
410 }
411 }
412
413 pub fn with_align(content: String, align: TextAlign) -> Self {
415 Self {
416 content,
417 align,
418 colspan: 1,
419 }
420 }
421
422 pub fn with_colspan(content: String, colspan: usize) -> Self {
424 Self {
425 content,
426 align: TextAlign::Left,
427 colspan,
428 }
429 }
430
431 pub fn set_align(&mut self, align: TextAlign) -> &mut Self {
433 self.align = align;
434 self
435 }
436
437 pub fn set_colspan(&mut self, colspan: usize) -> &mut Self {
439 self.colspan = colspan;
440 self
441 }
442}
443
444#[cfg(test)]
445mod tests {
446 use super::*;
447
448 #[test]
449 fn test_table_creation() {
450 let table = Table::new(vec![100.0, 150.0, 200.0]);
451 assert_eq!(table.column_widths.len(), 3);
452 assert_eq!(table.rows.len(), 0);
453 }
454
455 #[test]
456 fn test_table_equal_columns() {
457 let table = Table::with_equal_columns(4, 400.0);
458 assert_eq!(table.column_widths.len(), 4);
459 assert_eq!(table.column_widths[0], 100.0);
460 assert_eq!(table.get_width(), 400.0);
461 }
462
463 #[test]
464 fn test_add_header_row() {
465 let mut table = Table::new(vec![100.0, 100.0, 100.0]);
466 let result = table.add_header_row(vec![
467 "Name".to_string(),
468 "Age".to_string(),
469 "City".to_string(),
470 ]);
471 assert!(result.is_ok());
472 assert_eq!(table.rows.len(), 1);
473 assert!(table.rows[0].is_header);
474 }
475
476 #[test]
477 fn test_add_row_mismatch() {
478 let mut table = Table::new(vec![100.0, 100.0]);
479 let result = table.add_row(vec![
480 "John".to_string(),
481 "25".to_string(),
482 "NYC".to_string(),
483 ]);
484 assert!(result.is_err());
485 }
486
487 #[test]
488 fn test_table_cell_creation() {
489 let cell = TableCell::new("Test".to_string());
490 assert_eq!(cell.content, "Test");
491 assert_eq!(cell.align, TextAlign::Left);
492 assert_eq!(cell.colspan, 1);
493 }
494
495 #[test]
496 fn test_table_cell_with_colspan() {
497 let cell = TableCell::with_colspan("Merged".to_string(), 3);
498 assert_eq!(cell.content, "Merged");
499 assert_eq!(cell.colspan, 3);
500 }
501
502 #[test]
503 fn test_custom_row_colspan_validation() {
504 let mut table = Table::new(vec![100.0, 100.0, 100.0]);
505 let cells = vec![
506 TableCell::new("Normal".to_string()),
507 TableCell::with_colspan("Merged".to_string(), 2),
508 ];
509 let result = table.add_custom_row(cells);
510 assert!(result.is_ok());
511 assert_eq!(table.rows.len(), 1);
512 }
513
514 #[test]
515 fn test_custom_row_invalid_colspan() {
516 let mut table = Table::new(vec![100.0, 100.0, 100.0]);
517 let cells = vec![
518 TableCell::new("Normal".to_string()),
519 TableCell::with_colspan("Merged".to_string(), 3), ];
521 let result = table.add_custom_row(cells);
522 assert!(result.is_err());
523 }
524
525 #[test]
526 fn test_table_options_default() {
527 let options = TableOptions::default();
528 assert_eq!(options.border_width, 1.0);
529 assert_eq!(options.border_color, Color::black());
530 assert_eq!(options.cell_padding, 5.0);
531 assert_eq!(options.font_size, 10.0);
532 }
533
534 #[test]
535 fn test_header_style() {
536 let style = HeaderStyle {
537 background_color: Color::gray(0.9),
538 text_color: Color::black(),
539 font: Font::HelveticaBold,
540 bold: true,
541 };
542 assert_eq!(style.background_color, Color::gray(0.9));
543 assert!(style.bold);
544 }
545
546 #[test]
547 fn test_table_dimensions() {
548 let mut table = Table::new(vec![100.0, 150.0, 200.0]);
549 table.options.row_height = 20.0;
550
551 table
552 .add_row(vec!["A".to_string(), "B".to_string(), "C".to_string()])
553 .unwrap();
554 table
555 .add_row(vec!["D".to_string(), "E".to_string(), "F".to_string()])
556 .unwrap();
557
558 assert_eq!(table.get_width(), 450.0);
559 assert_eq!(table.get_height(), 40.0);
560 }
561
562 #[test]
563 fn test_table_position() {
564 let mut table = Table::new(vec![100.0]);
565 table.set_position(50.0, 100.0);
566 assert_eq!(table.position, (50.0, 100.0));
567 }
568
569 #[test]
570 fn test_row_with_alignment() {
571 let mut table = Table::new(vec![100.0, 100.0]);
572 let result = table.add_row_with_alignment(
573 vec!["Left".to_string(), "Right".to_string()],
574 TextAlign::Right,
575 );
576 assert!(result.is_ok());
577 assert_eq!(table.rows[0].cells[0].align, TextAlign::Right);
578 }
579
580 #[test]
581 fn test_table_cell_setters() {
582 let mut cell = TableCell::new("Test".to_string());
583 cell.set_align(TextAlign::Center).set_colspan(2);
584 assert_eq!(cell.align, TextAlign::Center);
585 assert_eq!(cell.colspan, 2);
586 }
587
588 #[test]
589 fn test_auto_row_height() {
590 let table = Table::new(vec![100.0]);
591 let row = TableRow::new(vec![TableCell::new("Test".to_string())]);
592 let height = table.calculate_row_height(&row);
593 assert_eq!(height, 20.0); }
595
596 #[test]
597 fn test_fixed_row_height() {
598 let mut table = Table::new(vec![100.0]);
599 table.options.row_height = 30.0;
600 let row = TableRow::new(vec![TableCell::new("Test".to_string())]);
601 let height = table.calculate_row_height(&row);
602 assert_eq!(height, 30.0);
603 }
604}