use dreport_core::models::*; use crate::data_resolve::ResolvedData; use crate::text_measure::TextMeasurer; /// Her auto sütun için header + tüm data satırlarındaki en geniş text'i ölç, /// doğal genişliklerini Fixed olarak ata. /// Fr sütunları olduğu gibi bırak (kalan alanı taffy dağıtır). /// Sadece auto sütunlar varsa (fr/fixed yoksa) kalan alanı oransal dağıt. fn compute_auto_column_widths( table: &RepeatingTableElement, rows: &[Vec], measurer: &mut TextMeasurer, available_width_mm: f64, ) -> Vec { let num_cols = table.columns.len(); let font_size = table.style.font_size.unwrap_or(10.0); let header_font_size = table.style.header_font_size.unwrap_or(font_size); let cell_pad_h = table.style.cell_padding_h.unwrap_or(2.0); let header_pad_h = table.style.header_padding_h.unwrap_or(cell_pad_h); // Ölçüme dahil edilecek max yatay padding (header ve cell'den büyük olanı) let max_pad_h = cell_pad_h.max(header_pad_h); // Hangi sütunlar auto? let is_auto: Vec = table.columns.iter().map(|c| matches!(c.width, SizeValue::Auto)).collect(); // Hiç auto yoksa olduğu gibi dön if !is_auto.iter().any(|&a| a) { return table.columns.iter().map(|c| c.width.clone()).collect(); } // Fr sütun var mı? let has_fr = table.columns.iter().any(|c| matches!(c.width, SizeValue::Fr { .. })); // Her auto sütun için max içerik genişliğini ölç (mm cinsinden) let mut max_widths_mm = vec![0.0_f64; num_cols]; for (col_idx, col) in table.columns.iter().enumerate() { if !is_auto[col_idx] { continue; } // Header text ölçümü (font_size zaten pt cinsinden) let (header_w_pt, _) = measurer.measure( &col.title, None, header_font_size as f32, Some("bold"), None, ); let header_w_mm = header_w_pt as f64 / (72.0 / 25.4); max_widths_mm[col_idx] = header_w_mm; // Data row text ölçümü for row in rows { let text = row.get(col_idx).map(|s| s.as_str()).unwrap_or(""); if text.is_empty() { continue; } let (w_pt, _) = measurer.measure( text, None, font_size as f32, None, None, ); let w_mm = w_pt as f64 / (72.0 / 25.4); if w_mm > max_widths_mm[col_idx] { max_widths_mm[col_idx] = w_mm; } } // Yatay padding ekle (sol + sağ) max_widths_mm[col_idx] += max_pad_h * 2.0; } // Fixed sütunların kapladığı alanı hesapla let mut fixed_total_mm = 0.0_f64; for (col_idx, col) in table.columns.iter().enumerate() { if !is_auto[col_idx] { if let SizeValue::Fixed { value } = &col.width { fixed_total_mm += value; } } } // Auto sütunların toplam doğal genişliği let auto_natural_total: f64 = max_widths_mm.iter().sum(); let remaining_mm = available_width_mm - fixed_total_mm; // Sonuç genişlikleri let mut result: Vec = Vec::with_capacity(num_cols); if has_fr { // Fr sütunlar var — auto sütunlara doğal genişliklerini ver, // kalan alanı Fr sütunlarına bırak (taffy flex ile dağıtır). // Fr sütunları için minimum alan ayır (en az padding kadar) let fr_count = table.columns.iter() .filter(|c| matches!(c.width, SizeValue::Fr { .. })) .count(); let fr_min_space = fr_count as f64 * max_pad_h * 2.0; let auto_budget = (remaining_mm - fr_min_space).max(0.0); for (col_idx, col) in table.columns.iter().enumerate() { if !is_auto[col_idx] { result.push(col.width.clone()); } else if auto_natural_total <= auto_budget { // Sığıyor — doğal genişliği kullan result.push(SizeValue::Fixed { value: max_widths_mm[col_idx] }); } else if auto_budget > 0.0 && auto_natural_total > 0.0 { // Sığmıyor — budget'a oransal küçült let ratio = max_widths_mm[col_idx] / auto_natural_total; let width_mm = auto_budget * ratio; result.push(SizeValue::Fixed { value: width_mm }); } else { result.push(SizeValue::Fixed { value: max_widths_mm[col_idx] }); } } } else { // Fr sütun yok — kalan alanı auto sütunlar arasında oransal dağıt for (col_idx, col) in table.columns.iter().enumerate() { if !is_auto[col_idx] { result.push(col.width.clone()); } else if auto_natural_total > 0.0 { let ratio = max_widths_mm[col_idx] / auto_natural_total; let width_mm = remaining_mm * ratio; result.push(SizeValue::Fixed { value: width_mm }); } else { // Tüm auto sütunlar boş — eşit dağıt let auto_count = is_auto.iter().filter(|&&a| a).count(); let width_mm = remaining_mm / auto_count as f64; result.push(SizeValue::Fixed { value: width_mm }); } } } result } /// RepeatingTable element'ini bir container ağacına expand eder. /// Tablo → column container (header row + data rows) /// Her row → row container (cell'ler → static_text) /// /// Bu sayede tablo, normal container layout'u ile hesaplanır. pub fn expand_table( table: &RepeatingTableElement, resolved: &ResolvedData, measurer: &mut TextMeasurer, available_width_mm: f64, ) -> ContainerElement { let resolved_table = resolved.tables.get(&table.id); let rows = resolved_table .map(|t| t.rows.as_slice()) .unwrap_or(&[]); // Auto sütunlar için içerik bazlı genişlik hesapla let effective_widths = compute_auto_column_widths(table, rows, measurer, available_width_mm); // Padding değerleri (mm) let cell_pad_h = table.style.cell_padding_h.unwrap_or(2.0); let cell_pad_v = table.style.cell_padding_v.unwrap_or(1.0); let header_pad_h = table.style.header_padding_h.unwrap_or(cell_pad_h); let header_pad_v = table.style.header_padding_v.unwrap_or(cell_pad_v); let mut children: Vec = Vec::new(); // Header row — her hücre padding container'ı içinde let header_cells: Vec = table .columns .iter() .enumerate() .map(|(i, col)| { let text = TemplateElement::StaticText(StaticTextElement { id: format!("{}_hdr_{}", table.id, i), position: PositionMode::Flow, size: SizeConstraint { width: SizeValue::Fr { value: 1.0 }, height: SizeValue::Auto, min_width: None, min_height: None, max_width: None, max_height: None, }, style: TextStyle { font_size: table.style.header_font_size.or(table.style.font_size), font_weight: Some("bold".to_string()), font_family: None, color: table.style.header_color.clone(), align: Some(col.align.clone()), }, content: col.title.clone(), }); TemplateElement::Container(ContainerElement { id: format!("{}_hdr_{}_wrap", table.id, i), position: PositionMode::Flow, size: SizeConstraint { width: effective_widths[i].clone(), height: SizeValue::Auto, min_width: None, min_height: None, max_width: None, max_height: None, }, direction: "column".to_string(), gap: 0.0, padding: Padding { top: header_pad_v, right: header_pad_h, bottom: header_pad_v, left: header_pad_h, }, align: "stretch".to_string(), justify: "start".to_string(), style: ContainerStyle::default(), children: vec![text], break_inside: "auto".to_string(), }) }) .collect(); children.push(TemplateElement::Container(ContainerElement { id: format!("{}_header", table.id), position: PositionMode::Flow, size: SizeConstraint { width: SizeValue::Fr { value: 1.0 }, height: SizeValue::Auto, min_width: None, min_height: None, max_width: None, max_height: None, }, direction: "row".to_string(), gap: 0.0, padding: Padding { top: 0.0, right: 0.0, bottom: 0.0, left: 0.0, }, align: "stretch".to_string(), justify: "start".to_string(), style: ContainerStyle { background_color: table.style.header_bg.clone(), ..Default::default() }, children: header_cells, break_inside: "auto".to_string(), })); // Header altına ayırıcı çizgi if table.style.border_color.is_some() { children.push(TemplateElement::Line(LineElement { id: format!("{}_header_line", table.id), position: PositionMode::Flow, size: SizeConstraint { width: SizeValue::Fr { value: 1.0 }, height: SizeValue::Auto, min_width: None, min_height: None, max_width: None, max_height: None, }, style: LineStyle { stroke_color: table.style.border_color.clone(), stroke_width: table.style.border_width, }, })); } // Data rows — her hücre padding container'ı içinde for (row_idx, row_data) in rows.iter().enumerate() { let cells: Vec = table .columns .iter() .enumerate() .map(|(col_idx, col)| { let text_content = row_data .get(col_idx) .cloned() .unwrap_or_default(); let text = TemplateElement::StaticText(StaticTextElement { id: format!("{}_r{}c{}", table.id, row_idx, col_idx), position: PositionMode::Flow, size: SizeConstraint { width: SizeValue::Fr { value: 1.0 }, height: SizeValue::Auto, min_width: None, min_height: None, max_width: None, max_height: None, }, style: TextStyle { font_size: table.style.font_size, font_weight: None, font_family: None, color: None, align: Some(col.align.clone()), }, content: text_content, }); TemplateElement::Container(ContainerElement { id: format!("{}_r{}c{}_wrap", table.id, row_idx, col_idx), position: PositionMode::Flow, size: SizeConstraint { width: effective_widths[col_idx].clone(), height: SizeValue::Auto, min_width: None, min_height: None, max_width: None, max_height: None, }, direction: "column".to_string(), gap: 0.0, padding: Padding { top: cell_pad_v, right: cell_pad_h, bottom: cell_pad_v, left: cell_pad_h, }, align: "stretch".to_string(), justify: "start".to_string(), style: ContainerStyle::default(), children: vec![text], break_inside: "auto".to_string(), }) }) .collect(); // row_idx 0-based: çift index (0,2,4) renksiz, tek index (1,3,5) zebra rengi let bg = if row_idx % 2 == 1 { table.style.zebra_odd.clone() } else { table.style.zebra_even.clone() }; children.push(TemplateElement::Container(ContainerElement { id: format!("{}_row_{}", table.id, row_idx), position: PositionMode::Flow, size: SizeConstraint { width: SizeValue::Fr { value: 1.0 }, height: SizeValue::Auto, min_width: None, min_height: None, max_width: None, max_height: None, }, direction: "row".to_string(), gap: 0.0, padding: Padding { top: 0.0, right: 0.0, bottom: 0.0, left: 0.0, }, align: "stretch".to_string(), justify: "start".to_string(), style: ContainerStyle { background_color: bg, ..Default::default() }, children: cells, break_inside: "auto".to_string(), })); } // Wrapper container (column direction, tüm tablo) ContainerElement { id: table.id.clone(), position: table.position.clone(), size: table.size.clone(), direction: "column".to_string(), gap: 0.0, padding: Padding { top: 0.0, right: 0.0, bottom: 0.0, left: 0.0, }, align: "stretch".to_string(), justify: "start".to_string(), style: ContainerStyle { border_color: table.style.border_color.clone(), border_width: table.style.border_width, ..Default::default() }, children, break_inside: "auto".to_string(), } } #[cfg(test)] mod tests { use super::*; use crate::data_resolve::{ResolvedData, ResolvedTable}; use crate::text_measure::TextMeasurer; use crate::FontData; use std::collections::HashMap; fn make_table(num_columns: usize) -> RepeatingTableElement { let columns: Vec = (0..num_columns) .map(|i| TableColumn { id: format!("col_{}", i), field: format!("field_{}", i), title: format!("Column {}", i), width: SizeValue::Fr { value: 1.0 }, align: "left".to_string(), format: None, }) .collect(); RepeatingTableElement { id: "tbl".to_string(), position: PositionMode::Flow, size: SizeConstraint { width: SizeValue::Fr { value: 1.0 }, height: SizeValue::Auto, ..Default::default() }, data_source: ArrayBinding { path: "items".to_string() }, columns, style: TableStyle::default(), repeat_header: Some(true), } } fn make_resolved(table_id: &str, rows: Vec>) -> ResolvedData { let mut tables = HashMap::new(); tables.insert(table_id.to_string(), ResolvedTable { rows }); ResolvedData { texts: HashMap::new(), tables, barcodes: HashMap::new(), images: HashMap::new(), page_number_formats: HashMap::new(), rich_texts: HashMap::new(), charts: HashMap::new(), } } fn make_measurer() -> TextMeasurer { // Font dosyasını yükle let font_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) .parent() .unwrap() .join("backend/fonts/NotoSans-Regular.ttf"); let font_bytes = std::fs::read(&font_path).expect("Font file not found"); let font_data = vec![FontData { family: "Noto Sans".to_string(), data: font_bytes, }]; TextMeasurer::new(&font_data) } /// Hücre wrapper container'ından içindeki StaticText'i çıkar fn unwrap_cell_text(cell: &TemplateElement) -> &StaticTextElement { match cell { TemplateElement::Container(c) => { assert_eq!(c.children.len(), 1, "Cell wrapper should have exactly 1 child"); match &c.children[0] { TemplateElement::StaticText(t) => t, _ => panic!("Expected StaticText inside cell wrapper"), } } _ => panic!("Expected Container wrapper for cell"), } } #[test] fn test_expand_table_structure() { let table = make_table(2); let resolved = make_resolved("tbl", vec![ vec!["A".to_string(), "1".to_string()], vec!["B".to_string(), "2".to_string()], ]); let mut measurer = make_measurer(); let container = expand_table(&table, &resolved, &mut measurer, 180.0); // Wrapper container properties assert_eq!(container.id, "tbl"); assert_eq!(container.direction, "column"); // Children: header row + 2 data rows (no border_color so no separator line) assert_eq!(container.children.len(), 3); // First child is header row container match &container.children[0] { TemplateElement::Container(c) => { assert_eq!(c.id, "tbl_header"); assert_eq!(c.direction, "row"); assert_eq!(c.children.len(), 2); // 2 columns // Check header cell text (inside wrapper container) let text = unwrap_cell_text(&c.children[0]); assert_eq!(text.content, "Column 0"); } _ => panic!("Expected Container for header row"), } // Data rows for (row_idx, child) in container.children[1..].iter().enumerate() { match child { TemplateElement::Container(c) => { assert_eq!(c.id, format!("tbl_row_{}", row_idx)); assert_eq!(c.direction, "row"); assert_eq!(c.children.len(), 2); } _ => panic!("Expected Container for data row"), } } } #[test] fn test_expand_table_empty_data() { let table = make_table(3); let resolved = make_resolved("tbl", vec![]); let mut measurer = make_measurer(); let container = expand_table(&table, &resolved, &mut measurer, 180.0); // Only header row, no data rows assert_eq!(container.children.len(), 1); // Header should still have all 3 columns match &container.children[0] { TemplateElement::Container(c) => { assert_eq!(c.children.len(), 3); } _ => panic!("Expected Container for header row"), } } #[test] fn test_expand_table_column_count() { let table = make_table(4); let resolved = make_resolved("tbl", vec![ vec!["a".into(), "b".into(), "c".into(), "d".into()], ]); let mut measurer = make_measurer(); let container = expand_table(&table, &resolved, &mut measurer, 180.0); // header + 1 data row assert_eq!(container.children.len(), 2); // Both header and data row should have 4 cells match &container.children[0] { TemplateElement::Container(c) => assert_eq!(c.children.len(), 4), _ => panic!("Expected Container"), } match &container.children[1] { TemplateElement::Container(c) => assert_eq!(c.children.len(), 4), _ => panic!("Expected Container"), } } #[test] fn test_expand_table_data_cell_content() { let table = make_table(2); let resolved = make_resolved("tbl", vec![ vec!["Hello".to_string(), "42".to_string()], ]); let mut measurer = make_measurer(); let container = expand_table(&table, &resolved, &mut measurer, 180.0); // Data row cells should contain the resolved text (inside wrapper containers) match &container.children[1] { TemplateElement::Container(c) => { let t0 = unwrap_cell_text(&c.children[0]); assert_eq!(t0.content, "Hello"); let t1 = unwrap_cell_text(&c.children[1]); assert_eq!(t1.content, "42"); } _ => panic!("Expected Container"), } } #[test] fn test_expand_table_with_border_adds_separator() { let mut table = make_table(2); table.style.border_color = Some("#000000".to_string()); let resolved = make_resolved("tbl", vec![ vec!["A".to_string(), "1".to_string()], ]); let mut measurer = make_measurer(); let container = expand_table(&table, &resolved, &mut measurer, 180.0); // header + separator line + 1 data row = 3 assert_eq!(container.children.len(), 3); // Second child should be a Line match &container.children[1] { TemplateElement::Line(l) => { assert_eq!(l.id, "tbl_header_line"); } _ => panic!("Expected Line separator after header"), } } #[test] fn test_expand_table_zebra_stripes() { let mut table = make_table(1); table.style.zebra_odd = Some("#f0f0f0".to_string()); table.style.zebra_even = Some("#ffffff".to_string()); let resolved = make_resolved("tbl", vec![ vec!["row0".into()], vec!["row1".into()], vec!["row2".into()], ]); let mut measurer = make_measurer(); let container = expand_table(&table, &resolved, &mut measurer, 180.0); // header + 3 data rows assert_eq!(container.children.len(), 4); // row_0 (even index) => zebra_even (no stripe) match &container.children[1] { TemplateElement::Container(c) => { assert_eq!(c.style.background_color, Some("#ffffff".to_string())); } _ => panic!("Expected Container"), } // row_1 (odd index) => zebra_odd (striped) match &container.children[2] { TemplateElement::Container(c) => { assert_eq!(c.style.background_color, Some("#f0f0f0".to_string())); } _ => panic!("Expected Container"), } // row_2 (even index) => zebra_even (no stripe) match &container.children[3] { TemplateElement::Container(c) => { assert_eq!(c.style.background_color, Some("#ffffff".to_string())); } _ => panic!("Expected Container"), } } #[test] fn test_auto_columns_get_content_based_widths() { // Auto sütunlu tablo: genişlikler içeriğe göre hesaplanmalı let columns = vec![ TableColumn { id: "col_0".into(), field: "short".into(), title: "No".into(), width: SizeValue::Auto, align: "right".into(), format: None, }, TableColumn { id: "col_1".into(), field: "long".into(), title: "Urun / Hizmet Adi".into(), width: SizeValue::Auto, align: "left".into(), format: None, }, ]; let table = RepeatingTableElement { id: "tbl".to_string(), position: PositionMode::Flow, size: SizeConstraint { width: SizeValue::Fr { value: 1.0 }, height: SizeValue::Auto, ..Default::default() }, data_source: ArrayBinding { path: "items".to_string() }, columns, style: TableStyle::default(), repeat_header: Some(true), }; let resolved = make_resolved("tbl", vec![ vec!["1".into(), "Web Uygulama Gelistirme".into()], vec!["2".into(), "SSL Sertifikasi".into()], ]); let mut measurer = make_measurer(); let container = expand_table(&table, &resolved, &mut measurer, 180.0); // Header row'daki ilk hücre wrapper (kısa: "No") ikinciden (uzun: "Urun / Hizmet Adi") dar olmalı match &container.children[0] { TemplateElement::Container(c) => { let w0 = match &c.children[0] { TemplateElement::Container(wrap) => match &wrap.size.width { SizeValue::Fixed { value } => *value, _ => panic!("Expected Fixed width for auto column wrapper"), }, _ => panic!("Expected Container wrapper"), }; let w1 = match &c.children[1] { TemplateElement::Container(wrap) => match &wrap.size.width { SizeValue::Fixed { value } => *value, _ => panic!("Expected Fixed width for auto column wrapper"), }, _ => panic!("Expected Container wrapper"), }; assert!(w1 > w0, "Long column ({w1}mm) should be wider than short column ({w0}mm)"); // Her iki sütun toplamı available_width'e eşit olmalı let total = w0 + w1; assert!((total - 180.0).abs() < 0.1, "Total width ({total}mm) should equal available width (180mm)"); } _ => panic!("Expected Container"), } } }