diff --git a/core/src/models.rs b/core/src/models.rs index 1a2c82a..eb91bbf 100644 --- a/core/src/models.rs +++ b/core/src/models.rs @@ -247,11 +247,8 @@ pub struct ChartStyle { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ChartElement { - pub id: String, - #[serde(default)] - pub condition: Option, - pub position: PositionMode, - pub size: SizeConstraint, + #[serde(flatten)] + pub base: ElementBase, pub chart_type: ChartType, pub data_source: ArrayBinding, pub category_field: String, @@ -272,6 +269,138 @@ pub struct ChartElement { pub style: ChartStyle, } +// --- Element Base (ortak alanlar) --- + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ElementBase { + pub id: String, + #[serde(default)] + pub condition: Option, + #[serde(default)] + pub position: PositionMode, + #[serde(default)] + pub size: SizeConstraint, +} + +impl ElementBase { + /// Flow pozisyonlu, condition'sız, verilen size ile base oluştur + pub fn flow(id: String, size: SizeConstraint) -> Self { + Self { + id, + condition: None, + position: PositionMode::Flow, + size, + } + } +} + +pub trait HasBase { + fn base(&self) -> &ElementBase; + fn base_mut(&mut self) -> &mut ElementBase; +} + +macro_rules! impl_has_base { + ($($t:ty),+ $(,)?) => { + $(impl HasBase for $t { + fn base(&self) -> &ElementBase { &self.base } + fn base_mut(&mut self) -> &mut ElementBase { &mut self.base } + })+ + }; +} + +impl_has_base!( + ContainerElement, + StaticTextElement, + TextElement, + LineElement, + ImageElement, + PageNumberElement, + BarcodeElement, + RepeatingTableElement, + PageBreakElement, + CurrentDateElement, + ShapeElement, + CheckboxElement, + CalculatedTextElement, + RichTextElement, + ChartElement, +); + +pub trait ElementTypeStr { + fn type_str(&self) -> &'static str; +} + +macro_rules! impl_type_str { + ($($t:ty => $s:literal),+ $(,)?) => { + $(impl ElementTypeStr for $t { + fn type_str(&self) -> &'static str { $s } + })+ + }; +} + +impl_type_str!( + ContainerElement => "container", + StaticTextElement => "static_text", + TextElement => "text", + LineElement => "line", + ImageElement => "image", + PageNumberElement => "page_number", + BarcodeElement => "barcode", + RepeatingTableElement => "repeating_table", + PageBreakElement => "page_break", + CurrentDateElement => "current_date", + ShapeElement => "shape", + CheckboxElement => "checkbox", + CalculatedTextElement => "calculated_text", + RichTextElement => "rich_text", + ChartElement => "chart", +); + +pub trait HasTextStyle { + fn text_style(&self) -> &TextStyle; +} + +macro_rules! impl_has_text_style { + ($($t:ty),+ $(,)?) => { + $(impl HasTextStyle for $t { + fn text_style(&self) -> &TextStyle { &self.style } + })+ + }; +} + +impl_has_text_style!( + StaticTextElement, + TextElement, + PageNumberElement, + CurrentDateElement, + CalculatedTextElement, + RichTextElement, +); + +pub trait HasOptionalBinding { + fn binding(&self) -> Option<&ScalarBinding>; + fn static_value(&self) -> Option<&str>; +} + +impl HasOptionalBinding for ImageElement { + fn binding(&self) -> Option<&ScalarBinding> { + self.binding.as_ref() + } + fn static_value(&self) -> Option<&str> { + self.src.as_deref() + } +} + +impl HasOptionalBinding for BarcodeElement { + fn binding(&self) -> Option<&ScalarBinding> { + self.binding.as_ref() + } + fn static_value(&self) -> Option<&str> { + self.value.as_deref() + } +} + // --- Element tipleri --- #[derive(Debug, Clone, Default, Serialize, Deserialize)] @@ -316,91 +445,59 @@ pub enum TemplateElement { } impl TemplateElement { - pub fn id(&self) -> &str { + fn inner_base(&self) -> &ElementBase { match self { - Self::Container(e) => &e.id, - Self::StaticText(e) => &e.id, - Self::Text(e) => &e.id, - Self::Line(e) => &e.id, - Self::RepeatingTable(e) => &e.id, - Self::Image(e) => &e.id, - Self::PageNumber(e) => &e.id, - Self::Barcode(e) => &e.id, - Self::PageBreak(e) => &e.id, - Self::CurrentDate(e) => &e.id, - Self::Shape(e) => &e.id, - Self::Checkbox(e) => &e.id, - Self::CalculatedText(e) => &e.id, - Self::RichText(e) => &e.id, - Self::Chart(e) => &e.id, + Self::Container(e) => e.base(), + Self::StaticText(e) => e.base(), + Self::Text(e) => e.base(), + Self::Line(e) => e.base(), + Self::RepeatingTable(e) => e.base(), + Self::Image(e) => e.base(), + Self::PageNumber(e) => e.base(), + Self::Barcode(e) => e.base(), + Self::PageBreak(e) => e.base(), + Self::CurrentDate(e) => e.base(), + Self::Shape(e) => e.base(), + Self::Checkbox(e) => e.base(), + Self::CalculatedText(e) => e.base(), + Self::RichText(e) => e.base(), + Self::Chart(e) => e.base(), } } + pub fn id(&self) -> &str { + &self.inner_base().id + } + pub fn position(&self) -> &PositionMode { - match self { - Self::Container(e) => &e.position, - Self::StaticText(e) => &e.position, - Self::Text(e) => &e.position, - Self::Line(e) => &e.position, - Self::RepeatingTable(e) => &e.position, - Self::Image(e) => &e.position, - Self::PageNumber(e) => &e.position, - Self::Barcode(e) => &e.position, - Self::PageBreak(_) => &PositionMode::Flow, - Self::CurrentDate(e) => &e.position, - Self::Shape(e) => &e.position, - Self::Checkbox(e) => &e.position, - Self::CalculatedText(e) => &e.position, - Self::RichText(e) => &e.position, - Self::Chart(e) => &e.position, - } + &self.inner_base().position } pub fn condition(&self) -> Option<&Condition> { - match self { - Self::Container(e) => e.condition.as_ref(), - Self::StaticText(e) => e.condition.as_ref(), - Self::Text(e) => e.condition.as_ref(), - Self::Line(e) => e.condition.as_ref(), - Self::RepeatingTable(e) => e.condition.as_ref(), - Self::Image(e) => e.condition.as_ref(), - Self::PageNumber(e) => e.condition.as_ref(), - Self::Barcode(e) => e.condition.as_ref(), - Self::PageBreak(e) => e.condition.as_ref(), - Self::CurrentDate(e) => e.condition.as_ref(), - Self::Shape(e) => e.condition.as_ref(), - Self::Checkbox(e) => e.condition.as_ref(), - Self::CalculatedText(e) => e.condition.as_ref(), - Self::RichText(e) => e.condition.as_ref(), - Self::Chart(e) => e.condition.as_ref(), - } + self.inner_base().condition.as_ref() } pub fn size(&self) -> &SizeConstraint { - static DEFAULT_SIZE: SizeConstraint = SizeConstraint { - width: SizeValue::Auto, - height: SizeValue::Auto, - min_width: None, - min_height: None, - max_width: None, - max_height: None, - }; + &self.inner_base().size + } + + pub fn type_str(&self) -> &'static str { match self { - Self::Container(e) => &e.size, - Self::StaticText(e) => &e.size, - Self::Text(e) => &e.size, - Self::Line(e) => &e.size, - Self::RepeatingTable(e) => &e.size, - Self::Image(e) => &e.size, - Self::PageNumber(e) => &e.size, - Self::Barcode(e) => &e.size, - Self::PageBreak(_) => &DEFAULT_SIZE, - Self::CurrentDate(e) => &e.size, - Self::Shape(e) => &e.size, - Self::Checkbox(e) => &e.size, - Self::CalculatedText(e) => &e.size, - Self::RichText(e) => &e.size, - Self::Chart(e) => &e.size, + Self::Container(e) => e.type_str(), + Self::StaticText(e) => e.type_str(), + Self::Text(e) => e.type_str(), + Self::Line(e) => e.type_str(), + Self::RepeatingTable(e) => e.type_str(), + Self::Image(e) => e.type_str(), + Self::PageNumber(e) => e.type_str(), + Self::Barcode(e) => e.type_str(), + Self::PageBreak(e) => e.type_str(), + Self::CurrentDate(e) => e.type_str(), + Self::Shape(e) => e.type_str(), + Self::Checkbox(e) => e.type_str(), + Self::CalculatedText(e) => e.type_str(), + Self::RichText(e) => e.type_str(), + Self::Chart(e) => e.type_str(), } } } @@ -408,11 +505,8 @@ impl TemplateElement { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RichTextElement { - pub id: String, - #[serde(default)] - pub condition: Option, - pub position: PositionMode, - pub size: SizeConstraint, + #[serde(flatten)] + pub base: ElementBase, #[serde(default)] pub style: TextStyle, // varsayilan stil (span'lar override edebilir) pub content: Vec, @@ -421,13 +515,8 @@ pub struct RichTextElement { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ContainerElement { - pub id: String, - #[serde(default)] - pub condition: Option, - #[serde(default)] - pub position: PositionMode, - #[serde(default)] - pub size: SizeConstraint, + #[serde(flatten)] + pub base: ElementBase, #[serde(default = "default_column")] pub direction: String, #[serde(default)] @@ -463,11 +552,8 @@ fn default_start() -> String { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct StaticTextElement { - pub id: String, - #[serde(default)] - pub condition: Option, - pub position: PositionMode, - pub size: SizeConstraint, + #[serde(flatten)] + pub base: ElementBase, pub style: TextStyle, pub content: String, } @@ -475,11 +561,8 @@ pub struct StaticTextElement { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct TextElement { - pub id: String, - #[serde(default)] - pub condition: Option, - pub position: PositionMode, - pub size: SizeConstraint, + #[serde(flatten)] + pub base: ElementBase, pub style: TextStyle, pub content: Option, pub binding: ScalarBinding, @@ -488,22 +571,16 @@ pub struct TextElement { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct LineElement { - pub id: String, - #[serde(default)] - pub condition: Option, - pub position: PositionMode, - pub size: SizeConstraint, + #[serde(flatten)] + pub base: ElementBase, pub style: LineStyle, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ImageElement { - pub id: String, - #[serde(default)] - pub condition: Option, - pub position: PositionMode, - pub size: SizeConstraint, + #[serde(flatten)] + pub base: ElementBase, pub src: Option, pub binding: Option, pub style: ImageStyle, @@ -512,11 +589,8 @@ pub struct ImageElement { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PageNumberElement { - pub id: String, - #[serde(default)] - pub condition: Option, - pub position: PositionMode, - pub size: SizeConstraint, + #[serde(flatten)] + pub base: ElementBase, pub style: TextStyle, pub format: Option, } @@ -524,11 +598,8 @@ pub struct PageNumberElement { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct BarcodeElement { - pub id: String, - #[serde(default)] - pub condition: Option, - pub position: PositionMode, - pub size: SizeConstraint, + #[serde(flatten)] + pub base: ElementBase, pub format: String, // qr, ean13, ean8, code128, code39 pub value: Option, pub binding: Option, @@ -538,11 +609,8 @@ pub struct BarcodeElement { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RepeatingTableElement { - pub id: String, - #[serde(default)] - pub condition: Option, - pub position: PositionMode, - pub size: SizeConstraint, + #[serde(flatten)] + pub base: ElementBase, pub data_source: ArrayBinding, pub columns: Vec, pub style: TableStyle, @@ -557,19 +625,15 @@ fn default_true() -> Option { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PageBreakElement { - pub id: String, - #[serde(default)] - pub condition: Option, + #[serde(flatten)] + pub base: ElementBase, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CurrentDateElement { - pub id: String, - #[serde(default)] - pub condition: Option, - pub position: PositionMode, - pub size: SizeConstraint, + #[serde(flatten)] + pub base: ElementBase, pub style: TextStyle, pub format: Option, } @@ -577,11 +641,8 @@ pub struct CurrentDateElement { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ShapeElement { - pub id: String, - #[serde(default)] - pub condition: Option, - pub position: PositionMode, - pub size: SizeConstraint, + #[serde(flatten)] + pub base: ElementBase, pub shape_type: String, // rectangle, ellipse, rounded_rectangle pub style: ContainerStyle, } @@ -598,11 +659,8 @@ pub struct CheckboxStyle { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CheckboxElement { - pub id: String, - #[serde(default)] - pub condition: Option, - pub position: PositionMode, - pub size: SizeConstraint, + #[serde(flatten)] + pub base: ElementBase, pub checked: Option, // statik değer pub binding: Option, // dinamik boolean binding pub style: CheckboxStyle, @@ -611,11 +669,8 @@ pub struct CheckboxElement { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CalculatedTextElement { - pub id: String, - #[serde(default)] - pub condition: Option, - pub position: PositionMode, - pub size: SizeConstraint, + #[serde(flatten)] + pub base: ElementBase, pub style: TextStyle, pub expression: String, pub format: Option, diff --git a/frontend/src/core/types.ts b/frontend/src/core/types.ts index 251d92a..e628f07 100644 --- a/frontend/src/core/types.ts +++ b/frontend/src/core/types.ts @@ -116,10 +116,19 @@ export interface BarcodeStyle { includeText?: boolean // barkod altına değer yazılsın mı (QR hariç) } +// --- Condition (koşullu gösterim) --- + +export interface Condition { + path: string + operator: string + value?: unknown +} + // --- Element tipleri --- interface BaseElement { id: string + condition?: Condition position: PositionMode size: SizeConstraint } diff --git a/frontend/tests/visual/editor.spec.ts-snapshots/editor-canvas-editor-darwin.png b/frontend/tests/visual/editor.spec.ts-snapshots/editor-canvas-editor-darwin.png index 4ddbabe..b8da67f 100644 Binary files a/frontend/tests/visual/editor.spec.ts-snapshots/editor-canvas-editor-darwin.png and b/frontend/tests/visual/editor.spec.ts-snapshots/editor-canvas-editor-darwin.png differ diff --git a/frontend/tests/visual/editor.spec.ts-snapshots/editor-full-editor-darwin.png b/frontend/tests/visual/editor.spec.ts-snapshots/editor-full-editor-darwin.png index ec5721a..3262a2b 100644 Binary files a/frontend/tests/visual/editor.spec.ts-snapshots/editor-full-editor-darwin.png and b/frontend/tests/visual/editor.spec.ts-snapshots/editor-full-editor-darwin.png differ diff --git a/layout-engine/src/data_resolve.rs b/layout-engine/src/data_resolve.rs index 806bd6e..848390c 100644 --- a/layout-engine/src/data_resolve.rs +++ b/layout-engine/src/data_resolve.rs @@ -2,6 +2,9 @@ use dreport_core::models::*; use serde_json::Value; use std::collections::HashMap; +// Re-export HasOptionalBinding for convenience +pub use dreport_core::models::HasOptionalBinding; + /// Şu anki tarihi verilen format string'ine göre formatla. /// Desteklenen tokenlar: YYYY, MM, DD, HH, mm, ss /// WASM'da js_sys::Date, native'de SystemTime kullanır. @@ -226,6 +229,15 @@ fn json_values_eq(a: &Value, b: &Value) -> bool { } } +/// Çözümle optional binding: binding varsa data'dan, yoksa static value'dan +fn resolve_optional_binding(el: &impl HasOptionalBinding, data: &Value) -> String { + if let Some(binding) = el.binding() { + value_to_string(resolve_path(data, &binding.path)) + } else { + el.static_value().unwrap_or_default().to_string() + } +} + fn resolve_element(el: &TemplateElement, data: &Value, resolved: &mut ResolvedData, format_config: &dreport_core::models::FormatConfig) { // Koşul kontrolü: condition varsa ve sağlanmıyorsa, hidden olarak işaretle ve çık if let Some(condition) = el.condition() && !evaluate_condition(condition, data) { @@ -235,7 +247,7 @@ fn resolve_element(el: &TemplateElement, data: &Value, resolved: &mut ResolvedDa match el { TemplateElement::StaticText(e) => { - resolved.texts.insert(e.id.clone(), e.content.clone()); + resolved.texts.insert(e.base.id.clone(), e.content.clone()); } TemplateElement::Text(e) => { let bound_value = value_to_string(resolve_path(data, &e.binding.path)); @@ -243,7 +255,7 @@ fn resolve_element(el: &TemplateElement, data: &Value, resolved: &mut ResolvedDa Some(prefix) if !prefix.is_empty() => format!("{}{}", prefix, bound_value), _ => bound_value, }; - resolved.texts.insert(e.id.clone(), text); + resolved.texts.insert(e.base.id.clone(), text); } TemplateElement::PageNumber(e) => { // Format string'i sakla — sayfa bölme sonrası gerçek değerlerle çözülecek @@ -254,28 +266,18 @@ fn resolve_element(el: &TemplateElement, data: &Value, resolved: &mut ResolvedDa .to_string(); resolved .page_number_formats - .insert(e.id.clone(), fmt.clone()); + .insert(e.base.id.clone(), fmt.clone()); // Placeholder koy (tek sayfalık fallback) resolved.texts.insert( - e.id.clone(), + e.base.id.clone(), fmt.replace("{current}", "1").replace("{total}", "1"), ); } TemplateElement::Barcode(e) => { - let value = if let Some(binding) = &e.binding { - value_to_string(resolve_path(data, &binding.path)) - } else { - e.value.clone().unwrap_or_default() - }; - resolved.barcodes.insert(e.id.clone(), value); + resolved.barcodes.insert(e.base.id.clone(), resolve_optional_binding(e, data)); } TemplateElement::Image(e) => { - let src = if let Some(binding) = &e.binding { - value_to_string(resolve_path(data, &binding.path)) - } else { - e.src.clone().unwrap_or_default() - }; - resolved.images.insert(e.id.clone(), src); + resolved.images.insert(e.base.id.clone(), resolve_optional_binding(e, data)); } TemplateElement::RepeatingTable(e) => { let array = resolve_path(data, &e.data_source.path); @@ -302,7 +304,7 @@ fn resolve_element(el: &TemplateElement, data: &Value, resolved: &mut ResolvedDa } _ => vec![], }; - resolved.tables.insert(e.id.clone(), ResolvedTable { rows }); + resolved.tables.insert(e.base.id.clone(), ResolvedTable { rows }); } TemplateElement::Container(e) => { for child in &e.children { @@ -312,7 +314,7 @@ fn resolve_element(el: &TemplateElement, data: &Value, resolved: &mut ResolvedDa TemplateElement::CurrentDate(e) => { let fmt = e.format.as_deref().unwrap_or("DD.MM.YYYY"); let text = format_current_date(fmt); - resolved.texts.insert(e.id.clone(), text); + resolved.texts.insert(e.base.id.clone(), text); } TemplateElement::Checkbox(e) => { let checked = if let Some(binding) = &e.binding { @@ -327,7 +329,7 @@ fn resolve_element(el: &TemplateElement, data: &Value, resolved: &mut ResolvedDa e.checked.unwrap_or(false) }; // Store as "true"/"false" string in texts map - resolved.texts.insert(e.id.clone(), checked.to_string()); + resolved.texts.insert(e.base.id.clone(), checked.to_string()); } TemplateElement::CalculatedText(e) => { let result = crate::expr_eval::evaluate_expression(&e.expression, data); @@ -338,7 +340,7 @@ fn resolve_element(el: &TemplateElement, data: &Value, resolved: &mut ResolvedDa } else { formatted }; - resolved.texts.insert(e.id.clone(), text); + resolved.texts.insert(e.base.id.clone(), text); } TemplateElement::RichText(e) => { let spans: Vec = e @@ -371,7 +373,7 @@ fn resolve_element(el: &TemplateElement, data: &Value, resolved: &mut ResolvedDa } }) .collect(); - resolved.rich_texts.insert(e.id.clone(), spans); + resolved.rich_texts.insert(e.base.id.clone(), spans); } TemplateElement::Chart(e) => { let array = resolve_path(data, &e.data_source.path); @@ -389,7 +391,7 @@ fn resolve_element(el: &TemplateElement, data: &Value, resolved: &mut ResolvedDa group_mode: e.group_mode.clone(), }, }; - resolved.charts.insert(e.id.clone(), chart_data); + resolved.charts.insert(e.base.id.clone(), chart_data); } TemplateElement::Line(_) => {} TemplateElement::Shape(_) => {} @@ -542,10 +544,7 @@ mod tests { format_config: None, locale: None, root: ContainerElement { - id: "root".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint::default(), + base: ElementBase::flow("root".to_string(), SizeConstraint::default()), direction: "column".to_string(), gap: 0.0, padding: Padding::default(), @@ -554,10 +553,7 @@ mod tests { style: ContainerStyle::default(), break_inside: "auto".to_string(), children: vec![TemplateElement::Text(TextElement { - id: "el_name".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint::default(), + base: ElementBase::flow("el_name".to_string(), SizeConstraint::default()), style: TextStyle::default(), content: None, binding: ScalarBinding { @@ -593,10 +589,7 @@ mod tests { format_config: None, locale: None, root: ContainerElement { - id: "root".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint::default(), + base: ElementBase::flow("root".to_string(), SizeConstraint::default()), direction: "column".to_string(), gap: 0.0, padding: Padding::default(), @@ -605,10 +598,7 @@ mod tests { style: ContainerStyle::default(), break_inside: "auto".to_string(), children: vec![TemplateElement::Text(TextElement { - id: "el_no".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint::default(), + base: ElementBase::flow("el_no".to_string(), SizeConstraint::default()), style: TextStyle::default(), content: Some("Fatura No: ".to_string()), binding: ScalarBinding { @@ -641,10 +631,7 @@ mod tests { format_config: None, locale: None, root: ContainerElement { - id: "root".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint::default(), + base: ElementBase::flow("root".to_string(), SizeConstraint::default()), direction: "column".to_string(), gap: 0.0, padding: Padding::default(), @@ -653,10 +640,7 @@ mod tests { style: ContainerStyle::default(), break_inside: "auto".to_string(), children: vec![TemplateElement::StaticText(StaticTextElement { - id: "title".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint::default(), + base: ElementBase::flow("title".to_string(), SizeConstraint::default()), style: TextStyle::default(), content: "FATURA".to_string(), })], @@ -682,10 +666,7 @@ mod tests { format_config: None, locale: None, root: ContainerElement { - id: "root".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint::default(), + base: ElementBase::flow("root".to_string(), SizeConstraint::default()), direction: "column".to_string(), gap: 0.0, padding: Padding::default(), @@ -694,10 +675,7 @@ mod tests { style: ContainerStyle::default(), break_inside: "auto".to_string(), children: vec![TemplateElement::RepeatingTable(RepeatingTableElement { - id: "tbl".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint::default(), + base: ElementBase::flow("tbl".to_string(), SizeConstraint::default()), data_source: ArrayBinding { path: "kalemler".to_string(), }, @@ -754,10 +732,7 @@ mod tests { format_config: None, locale: None, root: ContainerElement { - id: "root".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint::default(), + base: ElementBase::flow("root".to_string(), SizeConstraint::default()), direction: "column".to_string(), gap: 0.0, padding: Padding::default(), @@ -766,10 +741,7 @@ mod tests { style: ContainerStyle::default(), break_inside: "auto".to_string(), children: vec![TemplateElement::RepeatingTable(RepeatingTableElement { - id: "tbl".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint::default(), + base: ElementBase::flow("tbl".to_string(), SizeConstraint::default()), data_source: ArrayBinding { path: "items".to_string(), }, @@ -808,10 +780,7 @@ mod tests { format_config: None, locale: None, root: ContainerElement { - id: "root".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint::default(), + base: ElementBase::flow("root".to_string(), SizeConstraint::default()), direction: "column".to_string(), gap: 0.0, padding: Padding::default(), @@ -820,10 +789,7 @@ mod tests { style: ContainerStyle::default(), break_inside: "auto".to_string(), children: vec![TemplateElement::Text(TextElement { - id: "el_missing".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint::default(), + base: ElementBase::flow("el_missing".to_string(), SizeConstraint::default()), style: TextStyle::default(), content: None, binding: ScalarBinding { diff --git a/layout-engine/src/lib.rs b/layout-engine/src/lib.rs index 0f2729a..72616ed 100644 --- a/layout-engine/src/lib.rs +++ b/layout-engine/src/lib.rs @@ -223,6 +223,75 @@ pub struct ResolvedStyle { pub barcode_include_text: Option, } +// --- From<&XStyle> for ResolvedStyle --- + +impl From<&dreport_core::models::TextStyle> for ResolvedStyle { + fn from(s: &dreport_core::models::TextStyle) -> Self { + Self { + font_size: s.font_size, + font_weight: s.font_weight.clone(), + font_style: s.font_style.clone(), + font_family: s.font_family.clone(), + color: s.color.clone(), + text_align: s.align.clone(), + ..Default::default() + } + } +} + +impl From<&dreport_core::models::ContainerStyle> for ResolvedStyle { + fn from(s: &dreport_core::models::ContainerStyle) -> Self { + Self { + background_color: s.background_color.clone(), + border_color: s.border_color.clone(), + border_width: s.border_width, + border_radius: s.border_radius, + border_style: s.border_style.clone(), + ..Default::default() + } + } +} + +impl From<&dreport_core::models::LineStyle> for ResolvedStyle { + fn from(s: &dreport_core::models::LineStyle) -> Self { + Self { + stroke_color: s.stroke_color.clone(), + stroke_width: s.stroke_width, + ..Default::default() + } + } +} + +impl From<&dreport_core::models::ImageStyle> for ResolvedStyle { + fn from(s: &dreport_core::models::ImageStyle) -> Self { + Self { + object_fit: s.object_fit.clone(), + ..Default::default() + } + } +} + +impl From<&dreport_core::models::BarcodeStyle> for ResolvedStyle { + fn from(s: &dreport_core::models::BarcodeStyle) -> Self { + Self { + barcode_color: s.color.clone(), + barcode_include_text: s.include_text, + ..Default::default() + } + } +} + +impl From<&dreport_core::models::CheckboxStyle> for ResolvedStyle { + fn from(s: &dreport_core::models::CheckboxStyle) -> Self { + Self { + color: s.check_color.clone(), + border_color: s.border_color.clone(), + border_width: s.border_width, + ..Default::default() + } + } +} + /// Ana layout hesaplama fonksiyonu. /// Template + data + font verileri alır, her element için pozisyon döner. pub fn compute_layout( diff --git a/layout-engine/src/pdf_render.rs b/layout-engine/src/pdf_render.rs index 3aac829..a3061f3 100644 --- a/layout-engine/src/pdf_render.rs +++ b/layout-engine/src/pdf_render.rs @@ -1603,17 +1603,14 @@ mod tests { format_config: None, locale: None, root: ContainerElement { - id: "root".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint { + base: ElementBase::flow("root".to_string(), SizeConstraint { width: SizeValue::Auto, height: SizeValue::Auto, min_width: None, min_height: None, max_width: None, max_height: None, - }, + }), direction: "column".to_string(), gap: 5.0, padding: Padding { @@ -1628,17 +1625,14 @@ mod tests { break_inside: "auto".to_string(), children: vec![ TemplateElement::StaticText(StaticTextElement { - id: "title".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint { + base: ElementBase::flow("title".to_string(), 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: Some(18.0), font_weight: Some("bold".to_string()), @@ -1647,34 +1641,28 @@ mod tests { content: "FATURA".to_string(), }), TemplateElement::Line(LineElement { - id: "line1".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint { + base: ElementBase::flow("line1".to_string(), 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: Some("#000000".to_string()), stroke_width: Some(0.5), }, }), TemplateElement::Text(TextElement { - id: "firma".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint { + base: ElementBase::flow("firma".to_string(), 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: Some(11.0), ..Default::default() diff --git a/layout-engine/src/sizing.rs b/layout-engine/src/sizing.rs index d2576a4..8e00dca 100644 --- a/layout-engine/src/sizing.rs +++ b/layout-engine/src/sizing.rs @@ -138,7 +138,7 @@ pub fn container_to_style(el: &ContainerElement, parent_direction: Option<&str>) }; // Pozisyon moduna göre - match &el.position { + match &el.base.position { PositionMode::Absolute { x, y } => { style.position = Position::Absolute; style.inset = Rect { @@ -152,7 +152,7 @@ pub fn container_to_style(el: &ContainerElement, parent_direction: Option<&str>) } // Boyut - apply_size_to_style(&mut style, &el.size, parent_direction); + apply_size_to_style(&mut style, &el.base.size, parent_direction); // Container border if let Some(bw) = el.style.border_width { @@ -197,7 +197,7 @@ pub fn leaf_style( #[cfg(test)] mod tests { use super::*; - use dreport_core::models::{ContainerStyle, Padding}; + use dreport_core::models::{ContainerStyle, ElementBase, Padding}; #[test] fn test_mm_to_pt_conversion() { @@ -327,10 +327,7 @@ mod tests { #[test] fn test_container_to_style_direction() { let el = ContainerElement { - id: "test".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint::default(), + base: ElementBase::flow("test".to_string(), SizeConstraint::default()), direction: "row".to_string(), gap: 5.0, padding: Padding { @@ -354,10 +351,12 @@ mod tests { #[test] fn test_container_to_style_absolute() { let el = ContainerElement { - id: "test".to_string(), - condition: None, - position: PositionMode::Absolute { x: 20.0, y: 30.0 }, - size: SizeConstraint::default(), + base: ElementBase { + id: "test".to_string(), + condition: None, + position: PositionMode::Absolute { x: 20.0, y: 30.0 }, + size: SizeConstraint::default(), + }, direction: "column".to_string(), gap: 0.0, padding: Padding::default(), diff --git a/layout-engine/src/table_layout.rs b/layout-engine/src/table_layout.rs index b2d2f74..c8ad25f 100644 --- a/layout-engine/src/table_layout.rs +++ b/layout-engine/src/table_layout.rs @@ -186,7 +186,7 @@ pub fn expand_table_cached( ) -> ContainerElement { let rows = resolved .tables - .get(&table.id) + .get(&table.base.id) .map(|t| t.rows.as_slice()) .unwrap_or(&[]); let key = table_cache_key(table, rows, available_width_mm); @@ -211,7 +211,7 @@ pub fn expand_table( measurer: &mut TextMeasurer, available_width_mm: f64, ) -> ContainerElement { - let resolved_table = resolved.tables.get(&table.id); + let resolved_table = resolved.tables.get(&table.base.id); let rows = resolved_table.map(|t| t.rows.as_slice()).unwrap_or(&[]); // Auto sütunlar için içerik bazlı genişlik hesapla @@ -232,17 +232,14 @@ pub fn expand_table( .enumerate() .map(|(i, col)| { let text = TemplateElement::StaticText(StaticTextElement { - id: format!("{}_hdr_{}", table.id, i), - condition: None, - 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, - }, + base: ElementBase::flow( + format!("{}_hdr_{}", table.base.id, i), + SizeConstraint { + width: SizeValue::Fr { value: 1.0 }, + height: SizeValue::Auto, + ..Default::default() + }, + ), style: TextStyle { font_size: table.style.header_font_size.or(table.style.font_size), font_weight: Some("bold".to_string()), @@ -254,25 +251,13 @@ pub fn expand_table( content: col.title.clone(), }); TemplateElement::Container(ContainerElement { - id: format!("{}_hdr_{}_wrap", table.id, i), - condition: None, - 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, - }, + base: ElementBase::flow( + format!("{}_hdr_{}_wrap", table.base.id, i), + SizeConstraint { width: effective_widths[i].clone(), ..Default::default() }, + ), 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, - }, + 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(), @@ -283,31 +268,16 @@ pub fn expand_table( .collect(); children.push(TemplateElement::Container(ContainerElement { - id: format!("{}_header", table.id), - condition: None, - 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, - }, + base: ElementBase::flow( + format!("{}_header", table.base.id), + SizeConstraint { width: SizeValue::Fr { value: 1.0 }, ..Default::default() }, + ), direction: "row".to_string(), gap: 0.0, - padding: Padding { - top: 0.0, - right: 0.0, - bottom: 0.0, - left: 0.0, - }, + padding: Padding::default(), align: "stretch".to_string(), justify: "start".to_string(), - style: ContainerStyle { - background_color: table.style.header_bg.clone(), - ..Default::default() - }, + style: ContainerStyle { background_color: table.style.header_bg.clone(), ..Default::default() }, children: header_cells, break_inside: "auto".to_string(), })); @@ -315,17 +285,10 @@ pub fn expand_table( // 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), - condition: None, - 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, - }, + base: ElementBase::flow( + format!("{}_header_line", table.base.id), + SizeConstraint { width: SizeValue::Fr { value: 1.0 }, ..Default::default() }, + ), style: LineStyle { stroke_color: table.style.border_color.clone(), stroke_width: table.style.border_width, @@ -343,17 +306,10 @@ pub fn expand_table( 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), - condition: None, - 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, - }, + base: ElementBase::flow( + format!("{}_r{}c{}", table.base.id, row_idx, col_idx), + SizeConstraint { width: SizeValue::Fr { value: 1.0 }, ..Default::default() }, + ), style: TextStyle { font_size: table.style.font_size, font_weight: None, @@ -365,25 +321,13 @@ pub fn expand_table( content: text_content, }); TemplateElement::Container(ContainerElement { - id: format!("{}_r{}c{}_wrap", table.id, row_idx, col_idx), - condition: None, - 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, - }, + base: ElementBase::flow( + format!("{}_r{}c{}_wrap", table.base.id, row_idx, col_idx), + SizeConstraint { width: effective_widths[col_idx].clone(), ..Default::default() }, + ), 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, - }, + 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(), @@ -401,31 +345,16 @@ pub fn expand_table( }; children.push(TemplateElement::Container(ContainerElement { - id: format!("{}_row_{}", table.id, row_idx), - condition: None, - 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, - }, + base: ElementBase::flow( + format!("{}_row_{}", table.base.id, row_idx), + SizeConstraint { width: SizeValue::Fr { value: 1.0 }, ..Default::default() }, + ), direction: "row".to_string(), gap: 0.0, - padding: Padding { - top: 0.0, - right: 0.0, - bottom: 0.0, - left: 0.0, - }, + padding: Padding::default(), align: "stretch".to_string(), justify: "start".to_string(), - style: ContainerStyle { - background_color: bg, - ..Default::default() - }, + style: ContainerStyle { background_color: bg, ..Default::default() }, children: cells, break_inside: "auto".to_string(), })); @@ -433,18 +362,15 @@ pub fn expand_table( // Wrapper container (column direction, tüm tablo) ContainerElement { - id: table.id.clone(), - condition: None, - position: table.position.clone(), - size: table.size.clone(), + base: ElementBase { + id: table.base.id.clone(), + condition: None, + position: table.base.position.clone(), + size: table.base.size.clone(), + }, direction: "column".to_string(), gap: 0.0, - padding: Padding { - top: 0.0, - right: 0.0, - bottom: 0.0, - left: 0.0, - }, + padding: Padding::default(), align: "stretch".to_string(), justify: "start".to_string(), style: ContainerStyle { @@ -478,14 +404,10 @@ mod tests { .collect(); RepeatingTableElement { - id: "tbl".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint { - width: SizeValue::Fr { value: 1.0 }, - height: SizeValue::Auto, - ..Default::default() - }, + base: ElementBase::flow( + "tbl".to_string(), + SizeConstraint { width: SizeValue::Fr { value: 1.0 }, ..Default::default() }, + ), data_source: ArrayBinding { path: "items".to_string(), }, @@ -554,7 +476,7 @@ mod tests { let container = expand_table(&table, &resolved, &mut measurer, 180.0); // Wrapper container properties - assert_eq!(container.id, "tbl"); + assert_eq!(container.base.id, "tbl"); assert_eq!(container.direction, "column"); // Children: header row + 2 data rows (no border_color so no separator line) @@ -563,7 +485,7 @@ mod tests { // First child is header row container match &container.children[0] { TemplateElement::Container(c) => { - assert_eq!(c.id, "tbl_header"); + assert_eq!(c.base.id, "tbl_header"); assert_eq!(c.direction, "row"); assert_eq!(c.children.len(), 2); // 2 columns // Check header cell text (inside wrapper container) @@ -577,7 +499,7 @@ mod tests { 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.base.id, format!("tbl_row_{}", row_idx)); assert_eq!(c.direction, "row"); assert_eq!(c.children.len(), 2); } @@ -666,7 +588,7 @@ mod tests { // Second child should be a Line match &container.children[1] { TemplateElement::Line(l) => { - assert_eq!(l.id, "tbl_header_line"); + assert_eq!(l.base.id, "tbl_header_line"); } _ => panic!("Expected Line separator after header"), } @@ -738,14 +660,10 @@ mod tests { ]; let table = RepeatingTableElement { - id: "tbl".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint { - width: SizeValue::Fr { value: 1.0 }, - height: SizeValue::Auto, - ..Default::default() - }, + base: ElementBase::flow( + "tbl".to_string(), + SizeConstraint { width: SizeValue::Fr { value: 1.0 }, ..Default::default() }, + ), data_source: ArrayBinding { path: "items".to_string(), }, @@ -769,14 +687,14 @@ mod tests { match &container.children[0] { TemplateElement::Container(c) => { let w0 = match &c.children[0] { - TemplateElement::Container(wrap) => match &wrap.size.width { + TemplateElement::Container(wrap) => match &wrap.base.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 { + TemplateElement::Container(wrap) => match &wrap.base.size.width { SizeValue::Fixed { value } => *value, _ => panic!("Expected Fixed width for auto column wrapper"), }, @@ -811,7 +729,7 @@ mod tests { // Second call — same inputs — cache hit let result2 = expand_table_cached(&table, &resolved, &mut measurer, 180.0, &mut cache); assert_eq!(cache.len(), 1); // no new entry - assert_eq!(result1.id, result2.id); + assert_eq!(result1.base.id, result2.base.id); assert_eq!(result1.children.len(), result2.children.len()); } diff --git a/layout-engine/src/tree.rs b/layout-engine/src/tree.rs index 0a0e848..6907b5c 100644 --- a/layout-engine/src/tree.rs +++ b/layout-engine/src/tree.rs @@ -185,7 +185,7 @@ fn collect_break_modes(root: &ContainerElement) -> HashMap { fn collect_break_modes_recursive(el: &TemplateElement, modes: &mut HashMap) { if let TemplateElement::Container(c) = el { - modes.insert(c.id.clone(), c.break_inside.clone()); + modes.insert(c.base.id.clone(), c.break_inside.clone()); for child in &c.children { collect_break_modes_recursive(child, modes); } @@ -208,7 +208,7 @@ fn collect_no_repeat_recursive(el: &TemplateElement, set: &mut std::collections: } TemplateElement::RepeatingTable(t) => { if t.repeat_header == Some(false) { - set.insert(t.id.clone()); + set.insert(t.base.id.clone()); } } _ => {} @@ -233,7 +233,7 @@ fn build_container( // Child'lar için kullanılabilir genişliği hesapla // Container'ın kendi padding ve border'ını çıkar let border_w = el.style.border_width.unwrap_or(0.0); - let container_own_width = match &el.size.width { + let container_own_width = match &el.base.size.width { SizeValue::Fixed { value } => *value, _ => page_width_mm, // Fr veya Auto ise parent'ın genişliğini kullan }; @@ -268,17 +268,10 @@ fn build_container( node_map.insert( node, NodeInfo { - element_id: el.id.clone(), - element_type: "container".to_string(), + element_id: el.base.id.clone(), + element_type: el.type_str().to_string(), content: None, - style: ResolvedStyle { - background_color: el.style.background_color.clone(), - border_color: el.style.border_color.clone(), - border_width: el.style.border_width, - border_radius: el.style.border_radius, - border_style: el.style.border_style.clone(), - ..Default::default() - }, + style: (&el.style).into(), children_ids, }, ); @@ -309,88 +302,63 @@ fn build_element( page_width_mm, table_cache, ), - TemplateElement::StaticText(e) => build_text_leaf( + TemplateElement::StaticText(e) => build_resolved_text_leaf( + &e.base, + e.type_str(), + &e.style, taffy, node_map, - &e.id, - "static_text", - resolved - .texts - .get(&e.id) - .map(|s| s.as_str()) - .unwrap_or(&e.content), - &e.style, - &e.size, - &e.position, + resolved, parent_direction, + &e.content, + ), + TemplateElement::Text(e) => build_resolved_text_leaf( + &e.base, + e.type_str(), + &e.style, + taffy, + node_map, + resolved, + parent_direction, + "", + ), + TemplateElement::PageNumber(e) => build_resolved_text_leaf( + &e.base, + e.type_str(), + &e.style, + taffy, + node_map, + resolved, + parent_direction, + "1 / 1", + ), + TemplateElement::CurrentDate(e) => build_resolved_text_leaf( + &e.base, + e.type_str(), + &e.style, + taffy, + node_map, + resolved, + parent_direction, + "", + ), + TemplateElement::CalculatedText(e) => build_resolved_text_leaf( + &e.base, + e.type_str(), + &e.style, + taffy, + node_map, + resolved, + parent_direction, + "", ), - TemplateElement::Text(e) => { - let text = resolved.texts.get(&e.id).map(|s| s.as_str()).unwrap_or(""); - build_text_leaf( - taffy, - node_map, - &e.id, - "text", - text, - &e.style, - &e.size, - &e.position, - parent_direction, - ) - } - TemplateElement::PageNumber(e) => { - let text = resolved - .texts - .get(&e.id) - .map(|s| s.as_str()) - .unwrap_or("1 / 1"); - build_text_leaf( - taffy, - node_map, - &e.id, - "page_number", - text, - &e.style, - &e.size, - &e.position, - parent_direction, - ) - } - TemplateElement::CurrentDate(e) => { - let text = resolved.texts.get(&e.id).map(|s| s.as_str()).unwrap_or(""); - build_text_leaf( - taffy, - node_map, - &e.id, - "current_date", - text, - &e.style, - &e.size, - &e.position, - parent_direction, - ) - } - TemplateElement::CalculatedText(e) => { - let text = resolved.texts.get(&e.id).map(|s| s.as_str()).unwrap_or(""); - build_text_leaf( - taffy, - node_map, - &e.id, - "calculated_text", - text, - &e.style, - &e.size, - &e.position, - parent_direction, - ) - } TemplateElement::Line(e) => { let stroke_w = e.style.stroke_width.unwrap_or(0.5); - let style = sizing::leaf_style(&e.size, &e.position, parent_direction); + let style = sizing::leaf_style(&e.base.size, &e.base.position, parent_direction); // Line: genişlik parent'tan, yükseklik stroke width let mut leaf_style = style; - if matches!(e.size.height, SizeValue::Auto) { + if matches!(e.base.size.height, SizeValue::Auto) { leaf_style.size.height = Dimension::length(mm_to_pt(stroke_w)); } @@ -398,13 +366,13 @@ fn build_element( node_map.insert( node, NodeInfo { - element_id: e.id.clone(), - element_type: "line".to_string(), + element_id: e.base.id.clone(), + element_type: e.type_str().to_string(), content: Some(ResolvedContent::Line), - style: ResolvedStyle { - stroke_color: e.style.stroke_color.clone(), - stroke_width: Some(stroke_w), - ..Default::default() + style: { + let mut s: ResolvedStyle = (&e.style).into(); + s.stroke_width = Some(stroke_w); + s }, children_ids: vec![], }, @@ -412,37 +380,34 @@ fn build_element( Ok(node) } TemplateElement::Image(e) => { - let style = sizing::leaf_style(&e.size, &e.position, parent_direction); - let src = resolved.images.get(&e.id).cloned().unwrap_or_default(); + let style = sizing::leaf_style(&e.base.size, &e.base.position, parent_direction); + let src = resolved.images.get(&e.base.id).cloned().unwrap_or_default(); let node = taffy.new_leaf(style)?; node_map.insert( node, NodeInfo { - element_id: e.id.clone(), - element_type: "image".to_string(), + element_id: e.base.id.clone(), + element_type: e.type_str().to_string(), content: Some(ResolvedContent::Image { src }), - style: ResolvedStyle { - object_fit: e.style.object_fit.clone(), - ..Default::default() - }, + style: (&e.style).into(), children_ids: vec![], }, ); Ok(node) } TemplateElement::Barcode(e) => { - let mut style = sizing::leaf_style(&e.size, &e.position, parent_direction); - let value = resolved.barcodes.get(&e.id).cloned().unwrap_or_default(); + let mut style = sizing::leaf_style(&e.base.size, &e.base.position, parent_direction); + let value = resolved.barcodes.get(&e.base.id).cloned().unwrap_or_default(); // Barcode leaf'e minimum boyut ver (MeasureFunc yok, Auto=0 olur) let is_qr = e.format == "qr"; let default_h = if is_qr { 20.0 } else { 15.0 }; // mm let default_w = if is_qr { 20.0 } else { 40.0 }; // mm - if matches!(e.size.height, SizeValue::Auto) { + if matches!(e.base.size.height, SizeValue::Auto) { style.min_size.height = Dimension::length(mm_to_pt(default_h)); } - if matches!(e.size.width, SizeValue::Auto) { + if matches!(e.base.size.width, SizeValue::Auto) { style.min_size.width = Dimension::length(mm_to_pt(default_w)); } @@ -450,17 +415,13 @@ fn build_element( node_map.insert( node, NodeInfo { - element_id: e.id.clone(), - element_type: "barcode".to_string(), + element_id: e.base.id.clone(), + element_type: e.type_str().to_string(), content: Some(ResolvedContent::Barcode { format: e.format.clone(), value, }), - style: ResolvedStyle { - barcode_color: e.style.color.clone(), - barcode_include_text: e.style.include_text, - ..Default::default() - }, + style: (&e.style).into(), children_ids: vec![], }, ); @@ -497,23 +458,17 @@ fn build_element( ) } TemplateElement::Shape(e) => { - let style = sizing::leaf_style(&e.size, &e.position, parent_direction); + let style = sizing::leaf_style(&e.base.size, &e.base.position, parent_direction); let node = taffy.new_leaf(style)?; node_map.insert( node, NodeInfo { - element_id: e.id.clone(), - element_type: "shape".to_string(), + element_id: e.base.id.clone(), + element_type: e.type_str().to_string(), content: Some(ResolvedContent::Shape { shape_type: e.shape_type.clone(), }), - style: ResolvedStyle { - background_color: e.style.background_color.clone(), - border_color: e.style.border_color.clone(), - border_width: e.style.border_width, - border_radius: e.style.border_radius, - ..Default::default() - }, + style: (&e.style).into(), children_ids: vec![], }, ); @@ -522,19 +477,19 @@ fn build_element( TemplateElement::Checkbox(e) => { let checked_str = resolved .texts - .get(&e.id) + .get(&e.base.id) .map(|s| s.as_str()) .unwrap_or("false"); let checked = checked_str == "true"; let box_size_mm = e.style.size.unwrap_or(4.0); - let style = sizing::leaf_style(&e.size, &e.position, parent_direction); + let style = sizing::leaf_style(&e.base.size, &e.base.position, parent_direction); // Auto size → square based on style.size let mut leaf_style = style; - if matches!(e.size.width, SizeValue::Auto) { + if matches!(e.base.size.width, SizeValue::Auto) { leaf_style.size.width = Dimension::length(mm_to_pt(box_size_mm)); } - if matches!(e.size.height, SizeValue::Auto) { + if matches!(e.base.size.height, SizeValue::Auto) { leaf_style.size.height = Dimension::length(mm_to_pt(box_size_mm)); } @@ -542,22 +497,17 @@ fn build_element( node_map.insert( node, NodeInfo { - element_id: e.id.clone(), - element_type: "checkbox".to_string(), + element_id: e.base.id.clone(), + element_type: e.type_str().to_string(), content: Some(ResolvedContent::Checkbox { checked }), - style: ResolvedStyle { - color: e.style.check_color.clone(), - border_color: e.style.border_color.clone(), - border_width: e.style.border_width, - ..Default::default() - }, + style: (&e.style).into(), children_ids: vec![], }, ); Ok(node) } TemplateElement::RichText(e) => { - let spans = resolved.rich_texts.get(&e.id).cloned().unwrap_or_default(); + let spans = resolved.rich_texts.get(&e.base.id).cloned().unwrap_or_default(); let rich_span_measures: Vec = spans .iter() .map(|s| crate::text_measure::RichSpanMeasure { @@ -573,7 +523,7 @@ fn build_element( .map(|s| s.font_size_pt) .fold(11.0f32, f32::max); - let style = sizing::leaf_style(&e.size, &e.position, parent_direction); + let style = sizing::leaf_style(&e.base.size, &e.base.position, parent_direction); let context = MeasureContext { text: String::new(), @@ -600,39 +550,32 @@ fn build_element( node_map.insert( node, NodeInfo { - element_id: e.id.clone(), - element_type: "rich_text".to_string(), + element_id: e.base.id.clone(), + element_type: e.type_str().to_string(), content: Some(ResolvedContent::RichText { spans: resolved_spans, }), - style: ResolvedStyle { - font_size: e.style.font_size, - font_weight: e.style.font_weight.clone(), - font_family: e.style.font_family.clone(), - color: e.style.color.clone(), - text_align: e.style.align.clone(), - ..Default::default() - }, + style: (&e.style).into(), children_ids: vec![], }, ); Ok(node) } TemplateElement::Chart(e) => { - let mut style = sizing::leaf_style(&e.size, &e.position, parent_direction); + let mut style = sizing::leaf_style(&e.base.size, &e.base.position, parent_direction); // Default minimum boyut — Auto ise chart cok kucuk olmasin - if matches!(e.size.width, SizeValue::Auto) { + if matches!(e.base.size.width, SizeValue::Auto) { style.min_size.width = Dimension::length(mm_to_pt(80.0)); } - if matches!(e.size.height, SizeValue::Auto) { + if matches!(e.base.size.height, SizeValue::Auto) { style.min_size.height = Dimension::length(mm_to_pt(60.0)); } let node = taffy.new_leaf(style)?; node_map.insert( node, NodeInfo { - element_id: e.id.clone(), - element_type: "chart".to_string(), + element_id: e.base.id.clone(), + element_type: e.type_str().to_string(), content: None, // SVG collect_layout'ta uretilecek style: ResolvedStyle::default(), children_ids: vec![], @@ -653,8 +596,8 @@ fn build_element( node_map.insert( node, NodeInfo { - element_id: e.id.clone(), - element_type: "page_break".to_string(), + element_id: e.base.id.clone(), + element_type: e.type_str().to_string(), content: None, style: ResolvedStyle::default(), children_ids: vec![], @@ -669,7 +612,7 @@ fn build_element( fn register_expanded_texts(el: &TemplateElement, resolved: &mut ResolvedData) { match el { TemplateElement::StaticText(e) => { - resolved.texts.insert(e.id.clone(), e.content.clone()); + resolved.texts.insert(e.base.id.clone(), e.content.clone()); } TemplateElement::Container(e) => { for child in &e.children { @@ -680,6 +623,35 @@ fn register_expanded_texts(el: &TemplateElement, resolved: &mut ResolvedData) { } } +/// Generic text leaf builder — HasTextStyle trait ile text-benzeri elementleri tek yerde build eder +fn build_resolved_text_leaf( + el_base: &ElementBase, + el_type_str: &str, + text_style: &TextStyle, + taffy: &mut TaffyTree, + node_map: &mut HashMap, + resolved: &ResolvedData, + parent_direction: Option<&str>, + fallback_text: &str, +) -> Result { + let text = resolved + .texts + .get(&el_base.id) + .map(|s| s.as_str()) + .unwrap_or(fallback_text); + build_text_leaf( + taffy, + node_map, + &el_base.id, + el_type_str, + text, + text_style, + &el_base.size, + &el_base.position, + parent_direction, + ) +} + /// Text leaf node oluştur (static_text, text, page_number için ortak) #[allow(clippy::too_many_arguments)] fn build_text_leaf( @@ -714,15 +686,7 @@ fn build_text_leaf( content: Some(ResolvedContent::Text { value: text.to_string(), }), - style: ResolvedStyle { - font_size: text_style.font_size, - font_weight: text_style.font_weight.clone(), - font_style: text_style.font_style.clone(), - font_family: text_style.font_family.clone(), - color: text_style.color.clone(), - text_align: text_style.align.clone(), - ..Default::default() - }, + style: text_style.into(), children_ids: vec![], }, ); @@ -902,17 +866,14 @@ mod tests { format_config: None, locale: None, root: ContainerElement { - id: "root".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint { + base: ElementBase::flow("root".to_string(), SizeConstraint { width: SizeValue::Auto, height: SizeValue::Auto, min_width: None, min_height: None, max_width: None, max_height: None, - }, + }), direction: "column".to_string(), gap: 5.0, padding: Padding { @@ -927,17 +888,14 @@ mod tests { break_inside: "auto".to_string(), children: vec![ TemplateElement::StaticText(StaticTextElement { - id: "title".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint { + base: ElementBase::flow("title".to_string(), 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: Some(18.0), font_weight: Some("bold".to_string()), @@ -946,34 +904,28 @@ mod tests { content: "FATURA".to_string(), }), TemplateElement::Line(LineElement { - id: "line1".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint { + base: ElementBase::flow("line1".to_string(), 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: Some("#000000".to_string()), stroke_width: Some(0.5), }, }), TemplateElement::StaticText(StaticTextElement { - id: "body".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint { + base: ElementBase::flow("body".to_string(), 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: Some(11.0), ..Default::default() @@ -1051,17 +1003,14 @@ mod tests { format_config: None, locale: None, root: ContainerElement { - id: "root".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint { + base: ElementBase::flow("root".to_string(), SizeConstraint { width: SizeValue::Auto, height: SizeValue::Auto, min_width: None, min_height: None, max_width: None, max_height: None, - }, + }), direction: "column".to_string(), gap: 0.0, padding: Padding { @@ -1075,17 +1024,14 @@ mod tests { style: ContainerStyle::default(), break_inside: "auto".to_string(), children: vec![TemplateElement::Container(ContainerElement { - id: "row".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint { + base: ElementBase::flow("row".to_string(), 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: 5.0, padding: Padding { @@ -1100,17 +1046,14 @@ mod tests { break_inside: "auto".to_string(), children: vec![ TemplateElement::StaticText(StaticTextElement { - id: "left".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint { + base: ElementBase::flow("left".to_string(), 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: Some(11.0), ..Default::default() @@ -1118,17 +1061,14 @@ mod tests { content: "Sol".to_string(), }), TemplateElement::StaticText(StaticTextElement { - id: "right".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint { + base: ElementBase::flow("right".to_string(), 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: Some(11.0), ..Default::default() @@ -1184,17 +1124,14 @@ mod tests { format_config: None, locale: None, root: ContainerElement { - id: "root".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint { + base: ElementBase::flow("root".to_string(), SizeConstraint { width: SizeValue::Auto, height: SizeValue::Auto, min_width: None, min_height: None, max_width: None, max_height: None, - }, + }), direction: "column".to_string(), gap: 0.0, padding: Padding { @@ -1208,16 +1145,18 @@ mod tests { style: ContainerStyle::default(), break_inside: "auto".to_string(), children: vec![TemplateElement::StaticText(StaticTextElement { - id: "abs_text".to_string(), - condition: None, - position: PositionMode::Absolute { x: 50.0, y: 80.0 }, - size: SizeConstraint { - width: SizeValue::Fixed { value: 100.0 }, - height: SizeValue::Auto, - min_width: None, - min_height: None, - max_width: None, - max_height: None, + base: ElementBase { + id: "abs_text".to_string(), + condition: None, + position: PositionMode::Absolute { x: 50.0, y: 80.0 }, + size: SizeConstraint { + width: SizeValue::Fixed { value: 100.0 }, + height: SizeValue::Auto, + min_width: None, + min_height: None, + max_width: None, + max_height: None, + }, }, style: TextStyle { font_size: Some(14.0), @@ -1276,10 +1215,7 @@ mod tests { format_config: None, locale: None, root: ContainerElement { - id: "root".to_string(), - condition: None, - position: PositionMode::Flow, - size: sz_auto.clone(), + base: ElementBase::flow("root".to_string(), sz_auto.clone()), direction: "column".to_string(), gap: 5.0, padding: Padding { @@ -1295,10 +1231,7 @@ mod tests { children: vec![ // Header row TemplateElement::Container(ContainerElement { - id: "c_header".to_string(), - condition: None, - position: PositionMode::Flow, - size: sz_fr_auto.clone(), + base: ElementBase::flow("c_header".to_string(), sz_fr_auto.clone()), direction: "row".to_string(), gap: 5.0, padding: p0.clone(), @@ -1309,10 +1242,7 @@ mod tests { children: vec![ // Sol: firma bilgileri TemplateElement::Container(ContainerElement { - id: "c_firma".to_string(), - condition: None, - position: PositionMode::Flow, - size: sz_fr_auto.clone(), + base: ElementBase::flow("c_firma".to_string(), sz_fr_auto.clone()), direction: "column".to_string(), gap: 1.0, padding: p0.clone(), @@ -1322,10 +1252,7 @@ mod tests { break_inside: "auto".to_string(), children: vec![ TemplateElement::StaticText(StaticTextElement { - id: "el_firma_unvan".to_string(), - condition: None, - position: PositionMode::Flow, - size: sz_auto.clone(), + base: ElementBase::flow("el_firma_unvan".to_string(), sz_auto.clone()), style: TextStyle { font_size: Some(14.0), font_weight: Some("bold".to_string()), @@ -1334,10 +1261,7 @@ mod tests { content: "Teknova Yazılım ve Danışmanlık A.Ş.".to_string(), }), TemplateElement::StaticText(StaticTextElement { - id: "el_firma_adres".to_string(), - condition: None, - position: PositionMode::Flow, - size: sz_auto.clone(), + base: ElementBase::flow("el_firma_adres".to_string(), sz_auto.clone()), style: TextStyle { font_size: Some(9.0), ..Default::default() @@ -1346,10 +1270,7 @@ mod tests { .to_string(), }), TemplateElement::StaticText(StaticTextElement { - id: "el_firma_il".to_string(), - condition: None, - position: PositionMode::Flow, - size: sz_auto.clone(), + base: ElementBase::flow("el_firma_il".to_string(), sz_auto.clone()), style: TextStyle { font_size: Some(9.0), ..Default::default() @@ -1357,10 +1278,7 @@ mod tests { content: "Istanbul".to_string(), }), TemplateElement::StaticText(StaticTextElement { - id: "el_firma_tel".to_string(), - condition: None, - position: PositionMode::Flow, - size: sz_auto.clone(), + base: ElementBase::flow("el_firma_tel".to_string(), sz_auto.clone()), style: TextStyle { font_size: Some(9.0), ..Default::default() @@ -1368,10 +1286,7 @@ mod tests { content: "Tel: +90 212 555 0042".to_string(), }), TemplateElement::StaticText(StaticTextElement { - id: "el_firma_vd".to_string(), - condition: None, - position: PositionMode::Flow, - size: sz_auto.clone(), + base: ElementBase::flow("el_firma_vd".to_string(), sz_auto.clone()), style: TextStyle { font_size: Some(9.0), ..Default::default() @@ -1379,10 +1294,7 @@ mod tests { content: "VD: Levent VD".to_string(), }), TemplateElement::StaticText(StaticTextElement { - id: "el_firma_vn".to_string(), - condition: None, - position: PositionMode::Flow, - size: sz_auto.clone(), + base: ElementBase::flow("el_firma_vn".to_string(), sz_auto.clone()), style: TextStyle { font_size: Some(9.0), ..Default::default() @@ -1393,10 +1305,7 @@ mod tests { }), // Sağ: fatura başlığı TemplateElement::Container(ContainerElement { - id: "c_fatura_baslik".to_string(), - condition: None, - position: PositionMode::Flow, - size: sz_auto.clone(), + base: ElementBase::flow("c_fatura_baslik".to_string(), sz_auto.clone()), direction: "column".to_string(), gap: 2.0, padding: p0.clone(), @@ -1406,10 +1315,7 @@ mod tests { break_inside: "auto".to_string(), children: vec![ TemplateElement::StaticText(StaticTextElement { - id: "el_fatura_baslik".to_string(), - condition: None, - position: PositionMode::Flow, - size: sz_auto.clone(), + base: ElementBase::flow("el_fatura_baslik".to_string(), sz_auto.clone()), style: TextStyle { font_size: Some(18.0), font_weight: Some("bold".to_string()), @@ -1418,10 +1324,7 @@ mod tests { content: "FATURA".to_string(), }), TemplateElement::StaticText(StaticTextElement { - id: "el_fatura_no".to_string(), - condition: None, - position: PositionMode::Flow, - size: sz_auto.clone(), + base: ElementBase::flow("el_fatura_no".to_string(), sz_auto.clone()), style: TextStyle { font_size: Some(10.0), ..Default::default() @@ -1429,10 +1332,7 @@ mod tests { content: "No: FTR-2026-001547".to_string(), }), TemplateElement::StaticText(StaticTextElement { - id: "el_fatura_tarih".to_string(), - condition: None, - position: PositionMode::Flow, - size: sz_auto.clone(), + base: ElementBase::flow("el_fatura_tarih".to_string(), sz_auto.clone()), style: TextStyle { font_size: Some(10.0), ..Default::default() @@ -1440,10 +1340,7 @@ mod tests { content: "Tarih: 2026-03-29".to_string(), }), TemplateElement::StaticText(StaticTextElement { - id: "el_fatura_vade".to_string(), - condition: None, - position: PositionMode::Flow, - size: sz_auto.clone(), + base: ElementBase::flow("el_fatura_vade".to_string(), sz_auto.clone()), style: TextStyle { font_size: Some(10.0), ..Default::default() diff --git a/layout-engine/tests/improvements_test.rs b/layout-engine/tests/improvements_test.rs index 6927edd..736a5b2 100644 --- a/layout-engine/tests/improvements_test.rs +++ b/layout-engine/tests/improvements_test.rs @@ -27,12 +27,9 @@ fn base_template() -> Template { format_config: None, locale: None, root: ContainerElement { - id: "root".to_string(), - position: PositionMode::Flow, - size: SizeConstraint::default(), + base: ElementBase::flow("root".to_string(), SizeConstraint::default()), direction: "column".to_string(), gap: 5.0, - condition: None, padding: Padding { top: 15.0, right: 15.0, @@ -57,14 +54,11 @@ fn test_1_2_text_wrapping_layout_height() { // Dar bir container'da uzun metin → yükseklik tek satırdan fazla olmalı let mut tpl = base_template(); tpl.root.children.push(TemplateElement::StaticText(StaticTextElement { - id: "long_text".to_string(), - position: PositionMode::Flow, - condition: None, - size: SizeConstraint { + base: ElementBase::flow("long_text".to_string(), SizeConstraint { width: SizeValue::Fixed { value: 40.0 }, // 40mm genişlik — kısa height: SizeValue::Auto, ..Default::default() - }, + }), style: TextStyle { font_size: Some(12.0), ..Default::default() @@ -94,14 +88,11 @@ fn test_1_2_text_wrapping_pdf_renders() { // PDF render sırasında text wrapping çalışmalı — crash olmamalı let mut tpl = base_template(); tpl.root.children.push(TemplateElement::StaticText(StaticTextElement { - id: "wrap_pdf".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint { + base: ElementBase::flow("wrap_pdf".to_string(), SizeConstraint { width: SizeValue::Fixed { value: 50.0 }, height: SizeValue::Auto, ..Default::default() - }, + }), style: TextStyle { font_size: Some(11.0), ..Default::default() @@ -125,14 +116,11 @@ fn test_1_2_text_wrapping_pdf_renders() { fn test_1_3_image_object_fit_in_layout() { let mut tpl = base_template(); tpl.root.children.push(TemplateElement::Image(ImageElement { - id: "img_contain".to_string(), - position: PositionMode::Flow, - condition: None, - size: SizeConstraint { + base: ElementBase::flow("img_contain".to_string(), SizeConstraint { width: SizeValue::Fixed { value: 40.0 }, height: SizeValue::Fixed { value: 30.0 }, ..Default::default() - }, + }), src: Some("data:image/png;base64,iVBORw0KGgo=".to_string()), binding: None, style: ImageStyle { @@ -167,14 +155,11 @@ fn test_1_4_italic_font_in_pdf() { tpl.root .children .push(TemplateElement::StaticText(StaticTextElement { - id: "italic_text".to_string(), - position: PositionMode::Flow, - condition: None, - size: SizeConstraint { + base: ElementBase::flow("italic_text".to_string(), SizeConstraint { width: SizeValue::Fr { value: 1.0 }, height: SizeValue::Auto, ..Default::default() - }, + }), style: TextStyle { font_size: Some(12.0), font_style: Some("italic".to_string()), @@ -205,14 +190,11 @@ fn test_1_4_bold_italic_font_in_pdf() { tpl.root .children .push(TemplateElement::StaticText(StaticTextElement { - id: "bold_italic".to_string(), - position: PositionMode::Flow, - condition: None, - size: SizeConstraint { + base: ElementBase::flow("bold_italic".to_string(), SizeConstraint { width: SizeValue::Fr { value: 1.0 }, height: SizeValue::Auto, ..Default::default() - }, + }), style: TextStyle { font_size: Some(14.0), font_weight: Some("bold".to_string()), @@ -239,14 +221,11 @@ fn test_2_1_repeat_header_false_no_repeat_on_second_page() { tpl.root .children .push(TemplateElement::RepeatingTable(RepeatingTableElement { - id: "tbl_no_repeat".to_string(), - position: PositionMode::Flow, - condition: None, - size: SizeConstraint { + base: ElementBase::flow("tbl_no_repeat".to_string(), SizeConstraint { width: SizeValue::Fr { value: 1.0 }, height: SizeValue::Auto, ..Default::default() - }, + }), data_source: ArrayBinding { path: "items".to_string(), }, @@ -305,14 +284,11 @@ fn test_2_1_repeat_header_true_repeats_on_second_page() { tpl.root .children .push(TemplateElement::RepeatingTable(RepeatingTableElement { - id: "tbl_repeat".to_string(), - position: PositionMode::Flow, - condition: None, - size: SizeConstraint { + base: ElementBase::flow("tbl_repeat".to_string(), SizeConstraint { width: SizeValue::Fr { value: 1.0 }, height: SizeValue::Auto, ..Default::default() - }, + }), data_source: ArrayBinding { path: "items".to_string(), }, @@ -389,14 +365,11 @@ fn test_2_2_table_column_format_currency() { tpl.root .children .push(TemplateElement::RepeatingTable(RepeatingTableElement { - id: "tbl_fmt".to_string(), - position: PositionMode::Flow, - condition: None, - size: SizeConstraint { + base: ElementBase::flow("tbl_fmt".to_string(), SizeConstraint { width: SizeValue::Fr { value: 1.0 }, height: SizeValue::Auto, ..Default::default() - }, + }), data_source: ArrayBinding { path: "items".to_string(), }, @@ -459,14 +432,11 @@ fn test_2_2_table_column_format_currency() { fn test_2_3_rounded_rectangle_renders() { let mut tpl = base_template(); tpl.root.children.push(TemplateElement::Shape(ShapeElement { - id: "rounded_shape".to_string(), - position: PositionMode::Flow, - condition: None, - size: SizeConstraint { + base: ElementBase::flow("rounded_shape".to_string(), SizeConstraint { width: SizeValue::Fixed { value: 50.0 }, height: SizeValue::Fixed { value: 30.0 }, ..Default::default() - }, + }), shape_type: "rounded_rectangle".to_string(), style: ContainerStyle { background_color: Some("#3b82f6".to_string()), @@ -505,14 +475,11 @@ fn test_2_3_container_border_radius_renders() { tpl.root .children .push(TemplateElement::StaticText(StaticTextElement { - id: "text_in_rounded".to_string(), - position: PositionMode::Flow, - condition: None, - size: SizeConstraint { + base: ElementBase::flow("text_in_rounded".to_string(), SizeConstraint { width: SizeValue::Fr { value: 1.0 }, height: SizeValue::Auto, ..Default::default() - }, + }), style: TextStyle { font_size: Some(12.0), ..Default::default() @@ -604,14 +571,11 @@ fn test_2_7_format_config_in_template() { fn test_ellipse_shape_renders() { let mut tpl = base_template(); tpl.root.children.push(TemplateElement::Shape(ShapeElement { - id: "ellipse".to_string(), - position: PositionMode::Flow, - condition: None, - size: SizeConstraint { + base: ElementBase::flow("ellipse".to_string(), SizeConstraint { width: SizeValue::Fixed { value: 40.0 }, height: SizeValue::Fixed { value: 20.0 }, ..Default::default() - }, + }), shape_type: "ellipse".to_string(), style: ContainerStyle { background_color: Some("#ff6600".to_string()), @@ -635,22 +599,21 @@ fn test_ellipse_shape_renders() { fn test_7_1_condition_gt_hides_element() { let mut tpl = base_template(); tpl.root.children.push(TemplateElement::StaticText(StaticTextElement { - id: "always_visible".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint::default(), + base: ElementBase::flow("always_visible".to_string(), SizeConstraint::default()), style: TextStyle { font_size: Some(10.0), ..Default::default() }, content: "Visible".to_string(), })); tpl.root.children.push(TemplateElement::Text(TextElement { - id: "conditional_text".to_string(), - condition: Some(Condition { - path: "toplamlar.iskonto".to_string(), - operator: "gt".to_string(), - value: Some(serde_json::json!(0)), - }), - position: PositionMode::Flow, - size: SizeConstraint::default(), + base: ElementBase { + id: "conditional_text".to_string(), + condition: Some(Condition { + path: "toplamlar.iskonto".to_string(), + operator: "gt".to_string(), + value: Some(serde_json::json!(0)), + }), + position: PositionMode::Flow, + size: SizeConstraint::default(), + }, style: TextStyle { font_size: Some(10.0), ..Default::default() }, content: None, binding: ScalarBinding { path: "toplamlar.iskonto".to_string() }, @@ -676,14 +639,16 @@ fn test_7_1_condition_gt_hides_element() { fn test_7_1_condition_gt_shows_element() { let mut tpl = base_template(); tpl.root.children.push(TemplateElement::Text(TextElement { - id: "conditional_text".to_string(), - condition: Some(Condition { - path: "toplamlar.iskonto".to_string(), - operator: "gt".to_string(), - value: Some(serde_json::json!(0)), - }), - position: PositionMode::Flow, - size: SizeConstraint::default(), + base: ElementBase { + id: "conditional_text".to_string(), + condition: Some(Condition { + path: "toplamlar.iskonto".to_string(), + operator: "gt".to_string(), + value: Some(serde_json::json!(0)), + }), + position: PositionMode::Flow, + size: SizeConstraint::default(), + }, style: TextStyle { font_size: Some(10.0), ..Default::default() }, content: None, binding: ScalarBinding { path: "toplamlar.iskonto".to_string() }, @@ -705,14 +670,16 @@ fn test_7_1_condition_gt_shows_element() { fn test_7_1_condition_eq_operator() { let mut tpl = base_template(); tpl.root.children.push(TemplateElement::StaticText(StaticTextElement { - id: "status_text".to_string(), - condition: Some(Condition { - path: "durum".to_string(), - operator: "eq".to_string(), - value: Some(serde_json::json!("aktif")), - }), - position: PositionMode::Flow, - size: SizeConstraint::default(), + base: ElementBase { + id: "status_text".to_string(), + condition: Some(Condition { + path: "durum".to_string(), + operator: "eq".to_string(), + value: Some(serde_json::json!("aktif")), + }), + position: PositionMode::Flow, + size: SizeConstraint::default(), + }, style: TextStyle { font_size: Some(10.0), ..Default::default() }, content: "Aktif".to_string(), })); @@ -732,14 +699,16 @@ fn test_7_1_condition_eq_operator() { fn test_7_1_condition_empty_not_empty() { let mut tpl = base_template(); tpl.root.children.push(TemplateElement::StaticText(StaticTextElement { - id: "show_if_exists".to_string(), - condition: Some(Condition { - path: "note".to_string(), - operator: "not_empty".to_string(), - value: None, - }), - position: PositionMode::Flow, - size: SizeConstraint::default(), + base: ElementBase { + id: "show_if_exists".to_string(), + condition: Some(Condition { + path: "note".to_string(), + operator: "not_empty".to_string(), + value: None, + }), + position: PositionMode::Flow, + size: SizeConstraint::default(), + }, style: TextStyle { font_size: Some(10.0), ..Default::default() }, content: "Has note".to_string(), })); @@ -763,14 +732,16 @@ fn test_7_1_condition_empty_not_empty() { fn test_7_1_condition_on_container_hides_children() { let mut tpl = base_template(); tpl.root.children.push(TemplateElement::Container(ContainerElement { - id: "cond_container".to_string(), - condition: Some(Condition { - path: "show".to_string(), - operator: "eq".to_string(), - value: Some(serde_json::json!(true)), - }), - position: PositionMode::Flow, - size: SizeConstraint::default(), + base: ElementBase { + id: "cond_container".to_string(), + condition: Some(Condition { + path: "show".to_string(), + operator: "eq".to_string(), + value: Some(serde_json::json!(true)), + }), + position: PositionMode::Flow, + size: SizeConstraint::default(), + }, direction: "column".to_string(), gap: 0.0, padding: Padding::default(), @@ -779,10 +750,7 @@ fn test_7_1_condition_on_container_hides_children() { style: ContainerStyle::default(), break_inside: "auto".to_string(), children: vec![TemplateElement::StaticText(StaticTextElement { - id: "child_text".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint::default(), + base: ElementBase::flow("child_text".to_string(), SizeConstraint::default()), style: TextStyle { font_size: Some(10.0), ..Default::default() }, content: "Child".to_string(), })], @@ -854,10 +822,7 @@ fn test_7_5_effective_format_config_priority() { header: None, footer: None, root: ContainerElement { - id: "root".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint::default(), + base: ElementBase::flow("root".to_string(), SizeConstraint::default()), direction: "column".to_string(), gap: 0.0, padding: Padding::default(), @@ -889,10 +854,7 @@ fn test_7_5_effective_format_config_locale_fallback() { header: None, footer: None, root: ContainerElement { - id: "root".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint::default(), + base: ElementBase::flow("root".to_string(), SizeConstraint::default()), direction: "column".to_string(), gap: 0.0, padding: Padding::default(), @@ -915,14 +877,11 @@ fn test_7_5_locale_affects_table_currency_format() { let mut tpl = base_template(); tpl.locale = Some("en-US".to_string()); tpl.root.children.push(TemplateElement::RepeatingTable(RepeatingTableElement { - id: "tbl_locale".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint { + base: ElementBase::flow("tbl_locale".to_string(), SizeConstraint { width: SizeValue::Fr { value: 1.0 }, height: SizeValue::Auto, ..Default::default() - }, + }), data_source: ArrayBinding { path: "items".to_string() }, columns: vec![ TableColumn { diff --git a/layout-engine/tests/layout_integration.rs b/layout-engine/tests/layout_integration.rs index c750d9a..2612877 100644 --- a/layout-engine/tests/layout_integration.rs +++ b/layout-engine/tests/layout_integration.rs @@ -20,10 +20,7 @@ fn simple_template() -> Template { format_config: None, locale: None, root: ContainerElement { - id: "root".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint::default(), + base: ElementBase::flow("root".to_string(), SizeConstraint::default()), direction: "column".to_string(), gap: 5.0, padding: Padding { @@ -37,14 +34,14 @@ fn simple_template() -> Template { style: ContainerStyle::default(), break_inside: "auto".to_string(), children: vec![TemplateElement::StaticText(StaticTextElement { - id: "title".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint { - width: SizeValue::Fr { value: 1.0 }, - height: SizeValue::Auto, - ..Default::default() - }, + base: ElementBase::flow( + "title".to_string(), + SizeConstraint { + width: SizeValue::Fr { value: 1.0 }, + height: SizeValue::Auto, + ..Default::default() + }, + ), style: TextStyle { font_size: Some(14.0), font_weight: Some("bold".to_string()), @@ -166,10 +163,7 @@ fn test_compute_layout_with_data_binding() { format_config: None, locale: None, root: ContainerElement { - id: "root".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint::default(), + base: ElementBase::flow("root".to_string(), SizeConstraint::default()), direction: "column".to_string(), gap: 0.0, padding: Padding { @@ -183,14 +177,14 @@ fn test_compute_layout_with_data_binding() { style: ContainerStyle::default(), break_inside: "auto".to_string(), children: vec![TemplateElement::Text(TextElement { - id: "bound_text".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint { - width: SizeValue::Fr { value: 1.0 }, - height: SizeValue::Auto, - ..Default::default() - }, + base: ElementBase::flow( + "bound_text".to_string(), + SizeConstraint { + width: SizeValue::Fr { value: 1.0 }, + height: SizeValue::Auto, + ..Default::default() + }, + ), style: TextStyle { font_size: Some(12.0), ..Default::default() @@ -235,10 +229,7 @@ fn test_compute_layout_multiple_children_ordering() { format_config: None, locale: None, root: ContainerElement { - id: "root".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint::default(), + base: ElementBase::flow("root".to_string(), SizeConstraint::default()), direction: "column".to_string(), gap: 5.0, padding: Padding { @@ -253,14 +244,14 @@ fn test_compute_layout_multiple_children_ordering() { break_inside: "auto".to_string(), children: vec![ TemplateElement::StaticText(StaticTextElement { - id: "first".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint { - width: SizeValue::Fr { value: 1.0 }, - height: SizeValue::Auto, - ..Default::default() - }, + base: ElementBase::flow( + "first".to_string(), + SizeConstraint { + width: SizeValue::Fr { value: 1.0 }, + height: SizeValue::Auto, + ..Default::default() + }, + ), style: TextStyle { font_size: Some(12.0), ..Default::default() @@ -268,14 +259,14 @@ fn test_compute_layout_multiple_children_ordering() { content: "First".to_string(), }), TemplateElement::StaticText(StaticTextElement { - id: "second".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint { - width: SizeValue::Fr { value: 1.0 }, - height: SizeValue::Auto, - ..Default::default() - }, + base: ElementBase::flow( + "second".to_string(), + SizeConstraint { + width: SizeValue::Fr { value: 1.0 }, + height: SizeValue::Auto, + ..Default::default() + }, + ), style: TextStyle { font_size: Some(12.0), ..Default::default() diff --git a/layout-engine/tests/pdf_render_test.rs b/layout-engine/tests/pdf_render_test.rs index 940db25..7a9cb59 100644 --- a/layout-engine/tests/pdf_render_test.rs +++ b/layout-engine/tests/pdf_render_test.rs @@ -23,10 +23,7 @@ fn simple_template() -> Template { format_config: None, locale: None, root: ContainerElement { - id: "root".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint::default(), + base: ElementBase::flow("root".to_string(), SizeConstraint::default()), direction: "column".to_string(), gap: 5.0, padding: Padding { @@ -40,14 +37,11 @@ fn simple_template() -> Template { style: ContainerStyle::default(), break_inside: "auto".to_string(), children: vec![TemplateElement::StaticText(StaticTextElement { - id: "title".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint { + base: ElementBase::flow("title".to_string(), SizeConstraint { width: SizeValue::Fr { value: 1.0 }, height: SizeValue::Auto, ..Default::default() - }, + }), style: TextStyle { font_size: Some(18.0), font_weight: Some("bold".to_string()), @@ -94,10 +88,7 @@ fn test_render_pdf_with_multiple_elements() { format_config: None, locale: None, root: ContainerElement { - id: "root".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint::default(), + base: ElementBase::flow("root".to_string(), SizeConstraint::default()), direction: "column".to_string(), gap: 5.0, padding: Padding { @@ -112,14 +103,11 @@ fn test_render_pdf_with_multiple_elements() { break_inside: "auto".to_string(), children: vec![ TemplateElement::StaticText(StaticTextElement { - id: "header".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint { + base: ElementBase::flow("header".to_string(), SizeConstraint { width: SizeValue::Fr { value: 1.0 }, height: SizeValue::Auto, ..Default::default() - }, + }), style: TextStyle { font_size: Some(16.0), font_weight: Some("bold".to_string()), @@ -128,28 +116,22 @@ fn test_render_pdf_with_multiple_elements() { content: "FATURA".to_string(), }), TemplateElement::Line(LineElement { - id: "sep".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint { + base: ElementBase::flow("sep".to_string(), SizeConstraint { width: SizeValue::Fr { value: 1.0 }, height: SizeValue::Auto, ..Default::default() - }, + }), style: LineStyle { stroke_color: Some("#000000".to_string()), stroke_width: Some(0.5), }, }), TemplateElement::StaticText(StaticTextElement { - id: "body".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint { + base: ElementBase::flow("body".to_string(), SizeConstraint { width: SizeValue::Fr { value: 1.0 }, height: SizeValue::Auto, ..Default::default() - }, + }), style: TextStyle { font_size: Some(11.0), ..Default::default() @@ -192,10 +174,7 @@ fn test_render_pdf_with_container_styles() { format_config: None, locale: None, root: ContainerElement { - id: "root".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint::default(), + base: ElementBase::flow("root".to_string(), SizeConstraint::default()), direction: "column".to_string(), gap: 0.0, padding: Padding { @@ -214,14 +193,11 @@ fn test_render_pdf_with_container_styles() { }, break_inside: "auto".to_string(), children: vec![TemplateElement::StaticText(StaticTextElement { - id: "text".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint { + base: ElementBase::flow("text".to_string(), SizeConstraint { width: SizeValue::Fr { value: 1.0 }, height: SizeValue::Auto, ..Default::default() - }, + }), style: TextStyle { font_size: Some(12.0), color: Some("#ff0000".to_string()), @@ -257,10 +233,7 @@ fn test_page_break_produces_multiple_pages() { format_config: None, locale: None, root: ContainerElement { - id: "root".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint::default(), + base: ElementBase::flow("root".to_string(), SizeConstraint::default()), direction: "column".to_string(), gap: 5.0, padding: Padding { @@ -275,14 +248,11 @@ fn test_page_break_produces_multiple_pages() { break_inside: "auto".to_string(), children: vec![ TemplateElement::StaticText(StaticTextElement { - id: "t1".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint { + base: ElementBase::flow("t1".to_string(), SizeConstraint { width: SizeValue::Fr { value: 1.0 }, height: SizeValue::Auto, ..Default::default() - }, + }), style: TextStyle { font_size: Some(18.0), ..Default::default() @@ -290,18 +260,14 @@ fn test_page_break_produces_multiple_pages() { content: "Page 1 content".to_string(), }), TemplateElement::PageBreak(PageBreakElement { - id: "pb1".to_string(), - condition: None, + base: ElementBase::flow("pb1".to_string(), SizeConstraint::default()), }), TemplateElement::StaticText(StaticTextElement { - id: "t2".to_string(), - condition: None, - position: PositionMode::Flow, - size: SizeConstraint { + base: ElementBase::flow("t2".to_string(), SizeConstraint { width: SizeValue::Fr { value: 1.0 }, height: SizeValue::Auto, ..Default::default() - }, + }), style: TextStyle { font_size: Some(18.0), ..Default::default()