This commit is contained in:
2026-04-09 00:36:23 +03:00
parent 4fda0e7d98
commit aa27228d08
5 changed files with 459 additions and 570 deletions

View File

@@ -29,14 +29,14 @@ pub fn render_svg(data: &ResolvedChartData, width_mm: f64, height_mm: f64) -> St
// Title // Title
if let Some(ref title) = cl.title { if let Some(ref title) = cl.title {
let anchor = match title.align.as_str() { let anchor = match title.align.as_str() {
"left" => "start", "left" => SvgAnchor::Start,
"right" => "end", "right" => SvgAnchor::End,
_ => "middle", _ => SvgAnchor::Middle,
}; };
write!( write!(
svg, svg,
r##"<text x="{:.2}" y="{:.2}" font-size="{:.1}" fill="{}" text-anchor="{}" font-weight="bold">{}</text>"##, r##"<text x="{:.2}" y="{:.2}" font-size="{:.1}" fill="{}" text-anchor="{}" font-weight="bold">{}</text>"##,
title.x, title.y, title.font_size, title.color, anchor, escape_xml(&title.text) title.x, title.y, title.font_size, title.color, anchor.as_str(), escape_xml(&title.text)
) )
.unwrap(); .unwrap();
} }
@@ -56,14 +56,7 @@ pub fn render_svg(data: &ResolvedChartData, width_mm: f64, height_mm: f64) -> St
let has_axis = !matches!(data.chart_type, dreport_core::models::ChartType::Pie); let has_axis = !matches!(data.chart_type, dreport_core::models::ChartType::Pie);
if has_axis && let Some(ref axis) = data.axis { if has_axis && let Some(ref axis) = data.axis {
if let Some(ref x_label) = axis.x_label { if let Some(ref x_label) = axis.x_label {
let x = cl.plot_x + cl.plot_w / 2.0; svg_text(&mut svg, cl.plot_x + cl.plot_w / 2.0, height_mm - 2.0, 2.8, "#666", SvgAnchor::Middle, x_label);
let y = height_mm - 2.0;
write!(
svg,
r##"<text x="{:.2}" y="{:.2}" font-size="2.8" fill="#666" text-anchor="middle">{}</text>"##,
x, y, escape_xml(x_label)
)
.unwrap();
} }
if let Some(ref y_label) = axis.y_label { if let Some(ref y_label) = axis.y_label {
let x = 3.0; let x = 3.0;
@@ -101,24 +94,8 @@ fn render_bar(svg: &mut String, data: &ResolvedChartData, cl: &ChartLayout) {
) )
.unwrap(); .unwrap();
if bl.show_labels { if bl.show_labels && (!bl.stacked || bar.value > 0.0) {
if bl.stacked { svg_text(svg, bar.label_x, bar.label_y, bl.label_font, &bl.label_color, SvgAnchor::Middle, &format_value(bar.value));
if bar.value > 0.0 {
write!(
svg,
r##"<text x="{:.2}" y="{:.2}" font-size="{:.1}" fill="{}" text-anchor="middle">{}</text>"##,
bar.label_x, bar.label_y, bl.label_font, bl.label_color, format_value(bar.value)
)
.unwrap();
}
} else {
write!(
svg,
r##"<text x="{:.2}" y="{:.2}" font-size="{:.1}" fill="{}" text-anchor="middle">{}</text>"##,
bar.label_x, bar.label_y, bl.label_font, bl.label_color, format_value(bar.value)
)
.unwrap();
}
} }
} }
@@ -162,12 +139,7 @@ fn render_line(svg: &mut String, data: &ResolvedChartData, cl: &ChartLayout) {
} }
if ll.show_labels { if ll.show_labels {
write!( svg_text(svg, pt.x, pt.y - 1.5, ll.label_font, &ll.label_color, SvgAnchor::Middle, &format_value(pt.value));
svg,
r##"<text x="{:.2}" y="{:.2}" font-size="{:.1}" fill="{}" text-anchor="middle">{}</text>"##,
pt.x, pt.y - 1.5, ll.label_font, ll.label_color, format_value(pt.value)
)
.unwrap();
} }
} }
@@ -239,18 +211,11 @@ fn render_pie(svg: &mut String, data: &ResolvedChartData, cl: &ChartLayout) {
.unwrap(); .unwrap();
} }
// Percentage label inside slice
if pl.show_labels { if pl.show_labels {
write!( let pct = format!("{}%", (slice.fraction * 100.0).round());
svg, svg_text_central(svg, slice.label_x, slice.label_y, pl.label_font, &pl.label_color, SvgAnchor::Middle, &pct);
r##"<text x="{:.2}" y="{:.2}" font-size="{:.1}" fill="{}" text-anchor="middle" dominant-baseline="central">{}%</text>"##,
slice.label_x, slice.label_y, pl.label_font, pl.label_color,
(slice.fraction * 100.0).round()
)
.unwrap();
} }
// Category name label outside slice with leader line
if pl.show_cat_labels && !slice.cat_label_text.is_empty() { if pl.show_cat_labels && !slice.cat_label_text.is_empty() {
write!( write!(
svg, svg,
@@ -259,18 +224,8 @@ fn render_pie(svg: &mut String, data: &ResolvedChartData, cl: &ChartLayout) {
slice.leader_end_x, slice.leader_end_y slice.leader_end_x, slice.leader_end_y
) )
.unwrap(); .unwrap();
let anchor = if slice.cat_label_anchor_end { SvgAnchor::End } else { SvgAnchor::Start };
let anchor = if slice.cat_label_anchor_end { svg_text_central(svg, slice.cat_label_x, slice.cat_label_y, 2.5, "#555", anchor, &slice.cat_label_text);
"end"
} else {
"start"
};
write!(
svg,
r##"<text x="{:.2}" y="{:.2}" font-size="2.5" fill="#555" text-anchor="{}" dominant-baseline="central">{}</text>"##,
slice.cat_label_x, slice.cat_label_y, anchor, escape_xml(&slice.cat_label_text)
)
.unwrap();
} }
} }
} }
@@ -292,15 +247,7 @@ fn render_legend(
item.swatch_x, item.swatch_y, color item.swatch_x, item.swatch_y, color
) )
.unwrap(); .unwrap();
write!( svg_text(svg, item.text_x, item.text_y, legend.font_size, "#666", SvgAnchor::Start, &item.name);
svg,
r##"<text x="{:.2}" y="{:.2}" font-size="{:.1}" fill="#666">{}</text>"##,
item.text_x,
item.text_y,
legend.font_size,
escape_xml(&item.name)
)
.unwrap();
} }
} }
@@ -310,12 +257,7 @@ fn render_legend(
fn render_y_axis_svg(svg: &mut String, y_axis: &chart_layout::YAxisLayout) { fn render_y_axis_svg(svg: &mut String, y_axis: &chart_layout::YAxisLayout) {
for tick in &y_axis.ticks { for tick in &y_axis.ticks {
write!( svg_text(svg, y_axis.axis_x - 1.5, tick.y + 0.8, 2.3, "#666", SvgAnchor::End, &tick.label);
svg,
r##"<text x="{:.2}" y="{:.2}" font-size="2.3" fill="#666" text-anchor="end">{}</text>"##,
y_axis.axis_x - 1.5, tick.y + 0.8, tick.label
)
.unwrap();
if y_axis.show_grid { if y_axis.show_grid {
write!( write!(
@@ -340,6 +282,7 @@ fn render_x_labels_svg(svg: &mut String, x_labels: &chart_layout::XLabelLayout)
let angle = x_labels.rotate_angle; let angle = x_labels.rotate_angle;
for label in &x_labels.labels { for label in &x_labels.labels {
if angle > 0.0 { if angle > 0.0 {
// Döndürülmüş etiket — transform gerektiğinden helper kullanamıyoruz
write!( write!(
svg, svg,
r##"<text x="{:.2}" y="{:.2}" font-size="2.2" fill="#666" text-anchor="end" transform="rotate(-{:.1},{:.2},{:.2})">{}</text>"##, r##"<text x="{:.2}" y="{:.2}" font-size="2.2" fill="#666" text-anchor="end" transform="rotate(-{:.1},{:.2},{:.2})">{}</text>"##,
@@ -347,12 +290,7 @@ fn render_x_labels_svg(svg: &mut String, x_labels: &chart_layout::XLabelLayout)
) )
.unwrap(); .unwrap();
} else { } else {
write!( svg_text(svg, label.x, label.y, 2.5, "#666", SvgAnchor::Middle, &label.text);
svg,
r##"<text x="{:.2}" y="{:.2}" font-size="2.5" fill="#666" text-anchor="middle">{}</text>"##,
label.x, label.y, escape_xml(&label.text)
)
.unwrap();
} }
} }
} }
@@ -364,6 +302,61 @@ fn escape_xml(s: &str) -> String {
.replace('"', "&quot;") .replace('"', "&quot;")
} }
/// SVG text hizalama modu
enum SvgAnchor {
Start,
Middle,
End,
}
impl SvgAnchor {
fn as_str(&self) -> &str {
match self {
SvgAnchor::Start => "start",
SvgAnchor::Middle => "middle",
SvgAnchor::End => "end",
}
}
}
/// Tekrarlayan SVG text element yazımını soyutlar.
fn svg_text(
svg: &mut String,
x: f64,
y: f64,
font_size: f64,
fill: &str,
anchor: SvgAnchor,
text: &str,
) {
write!(
svg,
r##"<text x="{x:.2}" y="{y:.2}" font-size="{font_size:.1}" fill="{fill}" text-anchor="{anchor}">{text}</text>"##,
anchor = anchor.as_str(),
text = escape_xml(text),
)
.unwrap();
}
/// SVG text with dominant-baseline="central" (pie labels vb.)
fn svg_text_central(
svg: &mut String,
x: f64,
y: f64,
font_size: f64,
fill: &str,
anchor: SvgAnchor,
text: &str,
) {
write!(
svg,
r##"<text x="{x:.2}" y="{y:.2}" font-size="{font_size:.1}" fill="{fill}" text-anchor="{anchor}" dominant-baseline="central">{text}</text>"##,
anchor = anchor.as_str(),
text = escape_xml(text),
)
.unwrap();
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@@ -292,6 +292,58 @@ impl From<&dreport_core::models::CheckboxStyle> for ResolvedStyle {
} }
} }
impl From<&data_resolve::ResolvedChartData> for ChartRenderData {
fn from(cd: &data_resolve::ResolvedChartData) -> Self {
let n_colors = cd.categories.len().max(cd.series.len()).max(1);
let colors: Vec<String> = (0..n_colors)
.map(|i| {
cd.style
.colors
.as_ref()
.and_then(|c| c.get(i).cloned())
.unwrap_or_else(|| {
chart_layout::DEFAULT_COLORS[i % chart_layout::DEFAULT_COLORS.len()]
.to_string()
})
})
.collect();
Self {
chart_type: cd.chart_type.clone(),
categories: cd.categories.clone(),
series: cd
.series
.iter()
.map(|s| ChartSeriesData {
name: s.name.clone(),
values: s.values.clone(),
})
.collect(),
title_text: cd.title.as_ref().map(|t| t.text.clone()),
title_font_size: cd.title.as_ref().and_then(|t| t.font_size),
title_color: cd.title.as_ref().and_then(|t| t.color.clone()),
title_align: cd.title.as_ref().and_then(|t| t.align.clone()),
colors,
show_labels: cd.labels.as_ref().is_some_and(|l| l.show),
label_font_size: cd.labels.as_ref().and_then(|l| l.font_size),
label_color: cd.labels.as_ref().and_then(|l| l.color.clone()),
show_grid: cd.axis.as_ref().and_then(|a| a.show_grid).unwrap_or(true),
grid_color: cd.axis.as_ref().and_then(|a| a.grid_color.clone()),
bar_gap: cd.style.bar_gap,
stacked: matches!(cd.group_mode, Some(dreport_core::models::GroupMode::Stacked)),
inner_radius: cd.style.inner_radius,
show_points: cd.style.show_points,
line_width: cd.style.line_width,
background_color: cd.style.background_color.clone(),
legend_show: cd.legend.as_ref().is_some_and(|l| l.show),
legend_position: cd.legend.as_ref().and_then(|l| l.position.clone()),
legend_font_size: cd.legend.as_ref().and_then(|l| l.font_size),
x_label: cd.axis.as_ref().and_then(|a| a.x_label.clone()),
y_label: cd.axis.as_ref().and_then(|a| a.y_label.clone()),
}
}
}
/// Ana layout hesaplama fonksiyonu. /// Ana layout hesaplama fonksiyonu.
/// Template + data + font verileri alır, her element için pozisyon döner. /// Template + data + font verileri alır, her element için pozisyon döner.
pub fn compute_layout( pub fn compute_layout(

View File

@@ -21,11 +21,6 @@ fn mm(v: f64) -> f32 {
v as f32 * MM_TO_PT v as f32 * MM_TO_PT
} }
/// f64 mm degerini f32 pt'ye cevir (chart render icin)
fn pt(mm_val: f64) -> f32 {
mm_val as f32 * MM_TO_PT
}
/// Hex renk (#RRGGBB veya #RGB) → rgb::Color /// Hex renk (#RRGGBB veya #RGB) → rgb::Color
fn parse_color(hex: &str) -> rgb::Color { fn parse_color(hex: &str) -> rgb::Color {
let hex = hex.trim_start_matches('#'); let hex = hex.trim_start_matches('#');
@@ -46,6 +41,18 @@ fn parse_color(hex: &str) -> rgb::Color {
rgb::Color::new(r, g, b) rgb::Color::new(r, g, b)
} }
fn fill_from_color(color: rgb::Color) -> Fill {
Fill {
paint: color.into(),
opacity: NormalizedF32::ONE,
rule: Default::default(),
}
}
// ---------------------------------------------------------------------------
// Path builders
// ---------------------------------------------------------------------------
/// Rounded rectangle path oluştur. radius 0 ise düz dikdörtgen. /// Rounded rectangle path oluştur. radius 0 ise düz dikdörtgen.
fn build_rect_path(x: f32, y: f32, w: f32, h: f32, radius: f32) -> Option<krilla::geom::Path> { fn build_rect_path(x: f32, y: f32, w: f32, h: f32, radius: f32) -> Option<krilla::geom::Path> {
let mut pb = PathBuilder::new(); let mut pb = PathBuilder::new();
@@ -92,11 +99,132 @@ fn build_ellipse_path(x: f32, y: f32, w: f32, h: f32) -> Option<krilla::geom::Pa
pb.finish() pb.finish()
} }
fn fill_from_color(color: rgb::Color) -> Fill { /// Merkez + radius'tan daire path'i oluştur (build_ellipse_path'in kısa hali)
Fill { fn build_circle_path(cx: f32, cy: f32, r: f32) -> Option<krilla::geom::Path> {
build_ellipse_path(cx - r, cy - r, r * 2.0, r * 2.0)
}
// ---------------------------------------------------------------------------
// SurfaceExt — krilla surface üzerinde tekrar eden draw kalıplarını soyutlar
// ---------------------------------------------------------------------------
trait SurfaceExt {
fn draw_filled(&mut self, path: &krilla::geom::Path, color: rgb::Color);
fn draw_stroked(&mut self, path: &krilla::geom::Path, color: rgb::Color, width: f32);
fn draw_filled_stroked(
&mut self,
path: &krilla::geom::Path,
fill: Option<rgb::Color>,
stroke_color: rgb::Color,
stroke_width: f32,
);
}
impl SurfaceExt for krilla::surface::Surface<'_> {
fn draw_filled(&mut self, path: &krilla::geom::Path, color: rgb::Color) {
self.set_fill(Some(fill_from_color(color)));
self.set_stroke(None);
self.draw_path(path);
self.set_fill(None);
}
fn draw_stroked(&mut self, path: &krilla::geom::Path, color: rgb::Color, width: f32) {
self.set_fill(None);
self.set_stroke(Some(Stroke {
paint: color.into(), paint: color.into(),
width,
opacity: NormalizedF32::ONE, opacity: NormalizedF32::ONE,
rule: Default::default(), ..Default::default()
}));
self.draw_path(path);
self.set_stroke(None);
}
fn draw_filled_stroked(
&mut self,
path: &krilla::geom::Path,
fill: Option<rgb::Color>,
stroke_color: rgb::Color,
stroke_width: f32,
) {
self.set_fill(fill.map(fill_from_color));
self.set_stroke(Some(Stroke {
paint: stroke_color.into(),
width: stroke_width,
opacity: NormalizedF32::ONE,
..Default::default()
}));
self.draw_path(path);
self.set_fill(None);
self.set_stroke(None);
}
}
// ---------------------------------------------------------------------------
// draw_box — container ve shape'in ortak fill+border çizim mantığı
// ---------------------------------------------------------------------------
/// Kutu şekli: dikdörtgen, yuvarlatılmış dikdörtgen veya elips.
enum BoxShape {
Rect { radius: f32 },
Ellipse,
}
/// Arka plan + border'ı tek seferde çizer (CSS border-box modeli).
/// Container ve shape render'larının ortak kodu.
fn draw_box(
surface: &mut krilla::surface::Surface<'_>,
x: f32,
y: f32,
w: f32,
h: f32,
bg_color: Option<&str>,
border_color: Option<&str>,
border_width: Option<f64>,
shape: BoxShape,
) {
let has_bg = bg_color.is_some();
let has_border = border_color.is_some() && border_width.unwrap_or(0.0) > 0.0;
if !has_bg && !has_border {
return;
}
let build_path = |bx: f32, by: f32, bw: f32, bh: f32, shape: &BoxShape| -> Option<krilla::geom::Path> {
match shape {
BoxShape::Ellipse => build_ellipse_path(bx, by, bw, bh),
BoxShape::Rect { radius } => build_rect_path(bx, by, bw, bh, *radius),
}
};
if has_border {
let bw = mm(border_width.unwrap_or(0.5));
let bc = parse_color(border_color.unwrap_or("#000000"));
let inset = bw / 2.0;
// Border durumunda radius'u inset kadar küçült
let inset_shape = match shape {
BoxShape::Ellipse => BoxShape::Ellipse,
BoxShape::Rect { radius } => BoxShape::Rect {
radius: (radius - inset).max(0.0),
},
};
let path = build_path(x + inset, y + inset, w - bw, h - bw, &inset_shape);
if let Some(p) = path {
surface.draw_filled_stroked(
&p,
bg_color.map(parse_color),
bc,
bw,
);
}
} else {
let fill = parse_color(bg_color.unwrap_or("#ffffff"));
let path = build_path(x, y, w, h, &shape);
if let Some(p) = path {
surface.draw_filled(&p, fill);
}
} }
} }
@@ -329,80 +457,29 @@ fn render_shape(
style: &ResolvedStyle, style: &ResolvedStyle,
content: &Option<ResolvedContent>, content: &Option<ResolvedContent>,
) { ) {
let has_bg = style.background_color.is_some();
let has_border = style.border_color.is_some() && style.border_width.unwrap_or(0.0) > 0.0;
if !has_bg && !has_border {
return;
}
let shape_type = match content { let shape_type = match content {
Some(ResolvedContent::Shape { shape_type }) => shape_type.as_str(), Some(ResolvedContent::Shape { shape_type }) => shape_type.as_str(),
_ => "rectangle", _ => "rectangle",
}; };
let rect_radius = |s: &ResolvedStyle| -> f32 { let shape = match shape_type {
if shape_type == "rounded_rectangle" { "ellipse" => BoxShape::Ellipse,
s.border_radius.map(mm).unwrap_or(mm(3.0)) "rounded_rectangle" => BoxShape::Rect {
} else { radius: style.border_radius.map(mm).unwrap_or(mm(3.0)),
s.border_radius.map(mm).unwrap_or(0.0) },
} _ => BoxShape::Rect {
radius: style.border_radius.map(mm).unwrap_or(0.0),
},
}; };
if has_border { draw_box(
let border_width = mm(style.border_width.unwrap_or(0.5)); surface,
let border_color = parse_color(style.border_color.as_deref().unwrap_or("#000000")); x, y, w, h,
let inset = border_width / 2.0; style.background_color.as_deref(),
style.border_color.as_deref(),
// Fill + stroke tek path ile — anti-aliasing uyumu style.border_width,
if let Some(ref bg) = style.background_color { shape,
surface.set_fill(Some(fill_from_color(parse_color(bg)))); );
} else {
surface.set_fill(None);
}
surface.set_stroke(Some(Stroke {
paint: border_color.into(),
width: border_width,
opacity: NormalizedF32::ONE,
..Default::default()
}));
let path = match shape_type {
"ellipse" => {
build_ellipse_path(x + inset, y + inset, w - border_width, h - border_width)
}
_ => {
let radius = rect_radius(style);
build_rect_path(
x + inset,
y + inset,
w - border_width,
h - border_width,
(radius - inset).max(0.0),
)
}
};
if let Some(p) = path {
surface.draw_path(&p);
}
} else {
// Sadece fill, border yok
surface.set_fill(Some(fill_from_color(parse_color(
style.background_color.as_deref().unwrap_or("#ffffff"),
))));
surface.set_stroke(None);
let path = match shape_type {
"ellipse" => build_ellipse_path(x, y, w, h),
_ => build_rect_path(x, y, w, h, rect_radius(style)),
};
if let Some(p) = path {
surface.draw_path(&p);
}
}
surface.set_fill(None);
surface.set_stroke(None);
} }
fn render_checkbox( fn render_checkbox(
@@ -418,54 +495,24 @@ fn render_checkbox(
let border_width = mm(style.border_width.unwrap_or(0.3)); let border_width = mm(style.border_width.unwrap_or(0.3));
let inset = border_width / 2.0; let inset = border_width / 2.0;
// Draw box outline (inset for CSS border-box match) if let Some(p) = build_rect_path(x + inset, y + inset, w - border_width, h - border_width, 0.0) {
surface.set_fill(None); surface.draw_stroked(&p, border_color, border_width);
surface.set_stroke(Some(Stroke {
paint: border_color.into(),
width: border_width,
opacity: NormalizedF32::ONE,
..Default::default()
}));
if let Some(p) = build_rect_path(
x + inset,
y + inset,
w - border_width,
h - border_width,
0.0,
) {
surface.draw_path(&p);
} }
// Draw checkmark if checked
if checked { if checked {
let check_color = parse_color(style.color.as_deref().unwrap_or("#000000")); let check_color = parse_color(style.color.as_deref().unwrap_or("#000000"));
let stroke_w = w.min(h) * 0.12; let stroke_w = w.min(h) * 0.12;
surface.set_fill(None);
surface.set_stroke(Some(Stroke {
paint: check_color.into(),
width: stroke_w,
opacity: NormalizedF32::ONE,
..Default::default()
}));
// Checkmark: two lines forming a "✓"
let check_path = { let check_path = {
let mut pb = PathBuilder::new(); let mut pb = PathBuilder::new();
let mx = w * 0.2; pb.move_to(x + w * 0.2, y + h * 0.5);
let my = h * 0.5;
pb.move_to(x + mx, y + my);
pb.line_to(x + w * 0.4, y + h * 0.75); pb.line_to(x + w * 0.4, y + h * 0.75);
pb.line_to(x + w * 0.8, y + h * 0.25); pb.line_to(x + w * 0.8, y + h * 0.25);
pb.finish() pb.finish()
}; };
if let Some(p) = check_path { if let Some(p) = check_path {
surface.draw_path(&p); surface.draw_stroked(&p, check_color, stroke_w);
} }
} }
surface.set_fill(None);
surface.set_stroke(None);
} }
fn render_container_bg( fn render_container_bg(
@@ -476,55 +523,16 @@ fn render_container_bg(
h: f32, h: f32,
style: &ResolvedStyle, style: &ResolvedStyle,
) { ) {
let has_bg = style.background_color.is_some(); draw_box(
let has_border = style.border_color.is_some() && style.border_width.unwrap_or(0.0) > 0.0; surface,
x, y, w, h,
if !has_bg && !has_border { style.background_color.as_deref(),
return; style.border_color.as_deref(),
} style.border_width,
BoxShape::Rect {
let radius = style.border_radius.map(mm).unwrap_or(0.0); radius: style.border_radius.map(mm).unwrap_or(0.0),
},
if has_border { );
let border_width = mm(style.border_width.unwrap_or(0.5));
let border_color = parse_color(style.border_color.as_deref().unwrap_or("#000000"));
let inset = border_width / 2.0;
// CSS border-box: stroke path'i border_width/2 içeri çek.
// Tek draw_path ile hem fill hem stroke çizerek anti-aliasing uyumunu sağla.
if let Some(ref bg) = style.background_color {
surface.set_fill(Some(fill_from_color(parse_color(bg))));
} else {
surface.set_fill(None);
}
surface.set_stroke(Some(Stroke {
paint: border_color.into(),
width: border_width,
opacity: NormalizedF32::ONE,
..Default::default()
}));
if let Some(path) = build_rect_path(
x + inset,
y + inset,
w - border_width,
h - border_width,
(radius - inset).max(0.0),
) {
surface.draw_path(&path);
}
} else {
// Sadece background, border yok
surface.set_fill(Some(fill_from_color(parse_color(
style.background_color.as_deref().unwrap_or("#ffffff"),
))));
surface.set_stroke(None);
if let Some(path) = build_rect_path(x, y, w, h, radius) {
surface.draw_path(&path);
}
}
surface.set_fill(None);
surface.set_stroke(None);
} }
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
@@ -707,32 +715,16 @@ fn render_line(
h: f32, h: f32,
style: &ResolvedStyle, style: &ResolvedStyle,
) { ) {
let stroke_color = style let color = style
.stroke_color .stroke_color
.as_deref() .as_deref()
.map(parse_color) .map(parse_color)
.unwrap_or(rgb::Color::new(0, 0, 0)); .unwrap_or(rgb::Color::new(0, 0, 0));
// Çizgiyi filled rectangle olarak çiz — CSS borderTop ile aynı davranış. // Çizgiyi filled rectangle olarak çiz — CSS borderTop ile aynı davranış.
// Stroke kullanmak sub-pixel anti-aliasing farkları yaratır. if let Some(path) = build_rect_path(x, y, w, h, 0.0) {
surface.set_fill(Some(fill_from_color(stroke_color))); surface.draw_filled(&path, color);
surface.set_stroke(None);
let rect_path = {
let mut pb = PathBuilder::new();
// Eleman yüksekliği layout engine tarafından stroke_width olarak hesaplandı.
// Tüm eleman alanını dolduran ince dikdörtgen çiz.
if let Some(rect) = krilla::geom::Rect::from_xywh(x, y, w, h) {
pb.push_rect(rect);
} }
pb.finish()
};
if let Some(p) = rect_path {
surface.draw_path(&p);
}
surface.set_fill(None);
} }
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
@@ -935,14 +927,14 @@ fn render_chart(
if let Some(f) = font { if let Some(f) = font {
surface.set_fill(Some(fill_from_color(color))); surface.set_fill(Some(fill_from_color(color)));
surface.set_stroke(None); surface.set_stroke(None);
let fs_pt = pt(title.font_size); let fs_pt = mm(title.font_size);
let (tw, _) = measurer.measure(&title.text, None, fs_pt, Some("bold"), None); let (tw, _) = measurer.measure(&title.text, None, fs_pt, Some("bold"), None);
let tx = match title.align.as_str() { let tx = match title.align.as_str() {
"left" => pt(title.x), "left" => mm(title.x),
"right" => pt(title.x) - tw, "right" => mm(title.x) - tw,
_ => pt(title.x) - tw / 2.0, _ => mm(title.x) - tw / 2.0,
}; };
let ty = pt(title.y); let ty = mm(title.y);
surface.draw_text( surface.draw_text(
Point::from_xy(tx, ty), Point::from_xy(tx, ty),
f.clone(), f.clone(),
@@ -966,26 +958,28 @@ fn render_chart(
if bl.stacked { if bl.stacked {
if bar.value > 0.0 { if bar.value > 0.0 {
let label = format_value(bar.value); let label = format_value(bar.value);
chart_text_centered( chart_text(
surface, surface,
bar.label_x, bar.label_x,
bar.label_y, bar.label_y,
&label, &label,
bl.label_font, bl.label_font,
&bl.label_color, &bl.label_color,
ChartTextAlign::Center,
fonts, fonts,
measurer, measurer,
); );
} }
} else { } else {
let label = format_value(bar.value); let label = format_value(bar.value);
chart_text_centered( chart_text(
surface, surface,
bar.label_x, bar.label_x,
bar.label_y, bar.label_y,
&label, &label,
bl.label_font, bl.label_font,
&bl.label_color, &bl.label_color,
ChartTextAlign::Center,
fonts, fonts,
measurer, measurer,
); );
@@ -1015,7 +1009,7 @@ fn render_chart(
surface.set_fill(None); surface.set_fill(None);
surface.set_stroke(Some(Stroke { surface.set_stroke(Some(Stroke {
paint: color.into(), paint: color.into(),
width: pt(ll.line_width), width: mm(ll.line_width),
opacity: NormalizedF32::ONE, opacity: NormalizedF32::ONE,
..Default::default() ..Default::default()
})); }));
@@ -1023,9 +1017,9 @@ fn render_chart(
let mut pb = PathBuilder::new(); let mut pb = PathBuilder::new();
for (i, (lx, ly)) in points.iter().enumerate() { for (i, (lx, ly)) in points.iter().enumerate() {
if i == 0 { if i == 0 {
pb.move_to(pt(*lx), pt(*ly)); pb.move_to(mm(*lx), mm(*ly));
} else { } else {
pb.line_to(pt(*lx), pt(*ly)); pb.line_to(mm(*lx), mm(*ly));
} }
} }
pb.finish() pb.finish()
@@ -1037,24 +1031,8 @@ fn render_chart(
// Points // Points
if ll.show_points { if ll.show_points {
for (lx, ly) in &points { for (lx, ly) in &points {
let r = pt(0.8); if let Some(circle) = build_circle_path(mm(*lx), mm(*ly), mm(0.8)) {
let cx = pt(*lx); surface.draw_filled(&circle, color);
let cy = pt(*ly);
surface.set_fill(Some(fill_from_color(color)));
surface.set_stroke(None);
let circle = {
let mut pb = PathBuilder::new();
let k = r * 0.5522848;
pb.move_to(cx, cy - r);
pb.cubic_to(cx + k, cy - r, cx + r, cy - k, cx + r, cy);
pb.cubic_to(cx + r, cy + k, cx + k, cy + r, cx, cy + r);
pb.cubic_to(cx - k, cy + r, cx - r, cy + k, cx - r, cy);
pb.cubic_to(cx - r, cy - k, cx - k, cy - r, cx, cy - r);
pb.close();
pb.finish()
};
if let Some(p) = circle {
surface.draw_path(&p);
} }
} }
} }
@@ -1063,13 +1041,14 @@ fn render_chart(
if ll.show_labels { if ll.show_labels {
for lp in &series_layout.points { for lp in &series_layout.points {
let label = format_value(lp.value); let label = format_value(lp.value);
chart_text_centered( chart_text(
surface, surface,
lp.x, lp.x,
lp.y - 1.5, lp.y - 1.5,
&label, &label,
ll.label_font, ll.label_font,
&ll.label_color, &ll.label_color,
ChartTextAlign::Center,
fonts, fonts,
measurer, measurer,
); );
@@ -1114,13 +1093,14 @@ fn render_chart(
if pl.show_labels { if pl.show_labels {
let pct = (slice.fraction * 100.0).round(); let pct = (slice.fraction * 100.0).round();
let label = format!("{}%", pct); let label = format!("{}%", pct);
chart_text_centered( chart_text(
surface, surface,
slice.label_x, slice.label_x,
slice.label_y, slice.label_y,
&label, &label,
pl.label_font, pl.label_font,
&pl.label_color, &pl.label_color,
ChartTextAlign::Center,
fonts, fonts,
measurer, measurer,
); );
@@ -1136,25 +1116,19 @@ fn render_chart(
parse_color("#999999"), parse_color("#999999"),
0.5, 0.5,
); );
if slice.cat_label_anchor_end { let align = if slice.cat_label_anchor_end {
chart_text_end( ChartTextAlign::End
surface,
slice.cat_label_x,
slice.cat_label_y,
&slice.cat_label_text,
2.5,
"#555555",
fonts,
measurer,
);
} else { } else {
chart_text_start( ChartTextAlign::Start
};
chart_text(
surface, surface,
slice.cat_label_x, slice.cat_label_x,
slice.cat_label_y, slice.cat_label_y,
&slice.cat_label_text, &slice.cat_label_text,
2.5, 2.5,
"#555555", "#555555",
align,
fonts, fonts,
measurer, measurer,
); );
@@ -1162,7 +1136,6 @@ fn render_chart(
} }
} }
} }
}
// Legend render // Legend render
if cl.legend_show { if cl.legend_show {
@@ -1177,13 +1150,14 @@ fn render_chart(
legend.swatch_size, legend.swatch_size,
color, color,
); );
chart_text_start( chart_text(
surface, surface,
item.text_x, item.text_x,
item.text_y, item.text_y,
&item.name, &item.name,
legend.font_size, legend.font_size,
"#666666", "#666666",
ChartTextAlign::Start,
fonts, fonts,
measurer, measurer,
); );
@@ -1196,14 +1170,14 @@ fn render_chart(
if let Some(ref x_label) = data.x_label { if let Some(ref x_label) = data.x_label {
let lx = cl.plot_x + cl.plot_w / 2.0; let lx = cl.plot_x + cl.plot_w / 2.0;
let ly = base_y_mm + h_mm - 2.0; let ly = base_y_mm + h_mm - 2.0;
chart_text_centered(surface, lx, ly, x_label, 2.8, "#666666", fonts, measurer); chart_text(surface, lx, ly, x_label, 2.8, "#666666", ChartTextAlign::Center, fonts, measurer);
} }
if let Some(ref y_label) = data.y_label { if let Some(ref y_label) = data.y_label {
let lx = base_x_mm + 3.0; let lx = base_x_mm + 3.0;
let ly = cl.plot_y + cl.plot_h / 2.0; let ly = cl.plot_y + cl.plot_h / 2.0;
surface.push_transform(&Transform::from_translate(pt(lx), pt(ly))); surface.push_transform(&Transform::from_translate(mm(lx), mm(ly)));
surface.push_transform(&Transform::from_row(0.0, -1.0, 1.0, 0.0, 0.0, 0.0)); surface.push_transform(&Transform::from_row(0.0, -1.0, 1.0, 0.0, 0.0, 0.0));
chart_text_centered(surface, 0.0, 0.0, y_label, 2.8, "#666666", fonts, measurer); chart_text(surface, 0.0, 0.0, y_label, 2.8, "#666666", ChartTextAlign::Center, fonts, measurer);
surface.pop(); surface.pop();
surface.pop(); surface.pop();
} }
@@ -1219,7 +1193,7 @@ fn chart_rect(
rh: f64, rh: f64,
color: rgb::Color, color: rgb::Color,
) { ) {
let (rx, ry, rw, rh) = (pt(rx), pt(ry), pt(rw), pt(rh)); let (rx, ry, rw, rh) = (mm(rx), mm(ry), mm(rw), mm(rh));
surface.set_fill(Some(fill_from_color(color))); surface.set_fill(Some(fill_from_color(color)));
surface.set_stroke(None); surface.set_stroke(None);
let path = { let path = {
@@ -1243,7 +1217,7 @@ fn chart_line_seg(
color: rgb::Color, color: rgb::Color,
width: f32, width: f32,
) { ) {
let (x1, y1, x2, y2) = (pt(x1), pt(y1), pt(x2), pt(y2)); let (x1, y1, x2, y2) = (mm(x1), mm(y1), mm(x2), mm(y2));
surface.set_fill(None); surface.set_fill(None);
surface.set_stroke(Some(Stroke { surface.set_stroke(Some(Stroke {
paint: color.into(), paint: color.into(),
@@ -1262,93 +1236,52 @@ fn chart_line_seg(
} }
} }
/// Chart icin metin ciz — tek satirlik, centered /// Chart metin hizalama modu
/// font_size_mm: SVG viewBox'taki mm cinsinden boyut, pt'ye cevrilir enum ChartTextAlign {
#[allow(clippy::too_many_arguments)] Start,
fn chart_text_centered( Center,
surface: &mut krilla::surface::Surface<'_>, End,
cx_mm: f64,
cy_mm: f64,
text: &str,
font_size_mm: f64,
color_hex: &str,
fonts: &FontCollection,
measurer: &mut TextMeasurer,
) {
let font = fonts.get(None, None, None);
let Some(f) = font else {
return;
};
let color = parse_color(color_hex);
let fs_pt = pt(font_size_mm);
let (tw, _) = measurer.measure(text, None, fs_pt, None, None);
surface.set_fill(Some(fill_from_color(color)));
surface.set_stroke(None);
surface.draw_text(
Point::from_xy(pt(cx_mm) - tw / 2.0, pt(cy_mm)),
f.clone(),
fs_pt,
text,
false,
TextDirection::Auto,
);
} }
/// Chart icin metin ciz — end-aligned (sag hizali) /// Chart için tek satır metin çiz (mm cinsinden koordinatlar, pt'ye çevrilir)
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn chart_text_end( fn chart_text(
surface: &mut krilla::surface::Surface<'_>,
right_x_mm: f64,
cy_mm: f64,
text: &str,
font_size_mm: f64,
color_hex: &str,
fonts: &FontCollection,
measurer: &mut TextMeasurer,
) {
let font = fonts.get(None, None, None);
let Some(f) = font else {
return;
};
let color = parse_color(color_hex);
let fs_pt = pt(font_size_mm);
let (tw, _) = measurer.measure(text, None, fs_pt, None, None);
surface.set_fill(Some(fill_from_color(color)));
surface.set_stroke(None);
surface.draw_text(
Point::from_xy(pt(right_x_mm) - tw, pt(cy_mm)),
f.clone(),
fs_pt,
text,
false,
TextDirection::Auto,
);
}
/// Chart icin metin ciz — start-aligned (sol hizali)
#[allow(clippy::too_many_arguments)]
fn chart_text_start(
surface: &mut krilla::surface::Surface<'_>, surface: &mut krilla::surface::Surface<'_>,
x_mm: f64, x_mm: f64,
cy_mm: f64, y_mm: f64,
text: &str, text: &str,
font_size_mm: f64, font_size_mm: f64,
color_hex: &str, color_hex: &str,
align: ChartTextAlign,
fonts: &FontCollection, fonts: &FontCollection,
_measurer: &mut TextMeasurer, measurer: &mut TextMeasurer,
) { ) {
let font = fonts.get(None, None, None); let Some(font) = fonts.get(None, None, None) else {
let Some(f) = font else {
return; return;
}; };
let color = parse_color(color_hex); let color = parse_color(color_hex);
let fs_pt = pt(font_size_mm); let fs = mm(font_size_mm);
let px = mm(x_mm);
let py = mm(y_mm);
let draw_x = match align {
ChartTextAlign::Start => px,
ChartTextAlign::Center => {
let (tw, _) = measurer.measure(text, None, fs, None, None);
px - tw / 2.0
}
ChartTextAlign::End => {
let (tw, _) = measurer.measure(text, None, fs, None, None);
px - tw
}
};
surface.set_fill(Some(fill_from_color(color))); surface.set_fill(Some(fill_from_color(color)));
surface.set_stroke(None); surface.set_stroke(None);
surface.draw_text( surface.draw_text(
Point::from_xy(pt(x_mm), pt(cy_mm)), Point::from_xy(draw_x, py),
f.clone(), font.clone(),
fs_pt, fs,
text, text,
false, false,
TextDirection::Auto, TextDirection::Auto,
@@ -1363,13 +1296,14 @@ fn render_chart_y_axis(
measurer: &mut TextMeasurer, measurer: &mut TextMeasurer,
) { ) {
for tick in &y_axis.ticks { for tick in &y_axis.ticks {
chart_text_end( chart_text(
surface, surface,
y_axis.axis_x - 1.5, y_axis.axis_x - 1.5,
tick.y + 0.8, tick.y + 0.8,
&tick.label, &tick.label,
2.3, 2.3,
"#666666", "#666666",
ChartTextAlign::End,
fonts, fonts,
measurer, measurer,
); );
@@ -1409,31 +1343,33 @@ fn render_chart_x_labels(
let angle = x_labels.rotate_angle; let angle = x_labels.rotate_angle;
for label in &x_labels.labels { for label in &x_labels.labels {
if angle > 0.0 { if angle > 0.0 {
surface.push_transform(&Transform::from_translate(pt(label.x), pt(label.y))); surface.push_transform(&Transform::from_translate(mm(label.x), mm(label.y)));
let angle_rad = (angle as f32).to_radians(); let angle_rad = (angle as f32).to_radians();
let c = angle_rad.cos(); let c = angle_rad.cos();
let s = angle_rad.sin(); let s = angle_rad.sin();
surface.push_transform(&Transform::from_row(c, -s, s, c, 0.0, 0.0)); surface.push_transform(&Transform::from_row(c, -s, s, c, 0.0, 0.0));
chart_text_end( chart_text(
surface, surface,
0.0, 0.0,
0.0, 0.0,
&label.text, &label.text,
2.2, 2.2,
"#666666", "#666666",
ChartTextAlign::End,
fonts, fonts,
measurer, measurer,
); );
surface.pop(); surface.pop();
surface.pop(); surface.pop();
} else { } else {
chart_text_centered( chart_text(
surface, surface,
label.x, label.x,
label.y, label.y,
&label.text, &label.text,
2.5, 2.5,
"#666666", "#666666",
ChartTextAlign::Center,
fonts, fonts,
measurer, measurer,
); );
@@ -1452,19 +1388,19 @@ fn build_arc_path(
) -> Option<krilla::geom::Path> { ) -> Option<krilla::geom::Path> {
let mut pb = PathBuilder::new(); let mut pb = PathBuilder::new();
let sx = pt(cx + radius * start.cos()); let sx = mm(cx + radius * start.cos());
let sy = pt(cy + radius * start.sin()); let sy = mm(cy + radius * start.sin());
if inner_r > 0.0 { if inner_r > 0.0 {
pb.move_to(sx, sy); pb.move_to(sx, sy);
approximate_arc(&mut pb, cx, cy, radius, start, end); approximate_arc(&mut pb, cx, cy, radius, start, end);
let ix = pt(cx + inner_r * end.cos()); let ix = mm(cx + inner_r * end.cos());
let iy = pt(cy + inner_r * end.sin()); let iy = mm(cy + inner_r * end.sin());
pb.line_to(ix, iy); pb.line_to(ix, iy);
approximate_arc(&mut pb, cx, cy, inner_r, end, start); approximate_arc(&mut pb, cx, cy, inner_r, end, start);
pb.close(); pb.close();
} else { } else {
pb.move_to(pt(cx), pt(cy)); pb.move_to(mm(cx), mm(cy));
pb.line_to(sx, sy); pb.line_to(sx, sy);
approximate_arc(&mut pb, cx, cy, radius, start, end); approximate_arc(&mut pb, cx, cy, radius, start, end);
pb.close(); pb.close();
@@ -1496,7 +1432,7 @@ fn approximate_arc(pb: &mut PathBuilder, cx: f64, cy: f64, r: f64, start: f64, e
let c2x = p2x + k * r * a2.sin(); let c2x = p2x + k * r * a2.sin();
let c2y = p2y - k * r * a2.cos(); let c2y = p2y - k * r * a2.cos();
pb.cubic_to(pt(c1x), pt(c1y), pt(c2x), pt(c2y), pt(p2x), pt(p2y)); pb.cubic_to(mm(c1x), mm(c1y), mm(c2x), mm(c2y), mm(p2x), mm(p2y));
} }
} }
@@ -1792,7 +1728,7 @@ mod tests {
#[test] #[test]
fn test_pt_conversion() { fn test_pt_conversion() {
let result = pt(25.4); let result = mm(25.4);
assert!((result - 72.0).abs() < 0.01); assert!((result - 72.0).abs() < 0.01);
} }

View File

@@ -279,6 +279,30 @@ fn build_container(
Ok(node) Ok(node)
} }
/// Leaf node oluştur ve node_map'e kaydet (tekrarlayan boilerplate'i ortadan kaldırır).
fn register_leaf(
taffy: &mut TaffyTree<MeasureContext>,
node_map: &mut HashMap<NodeId, NodeInfo>,
style: Style,
id: &str,
element_type: &str,
content: Option<ResolvedContent>,
resolved_style: ResolvedStyle,
) -> Result<NodeId, LayoutError> {
let node = taffy.new_leaf(style)?;
node_map.insert(
node,
NodeInfo {
element_id: id.to_string(),
element_type: element_type.to_string(),
content,
style: resolved_style,
children_ids: vec![],
},
);
Ok(node)
}
/// Herhangi bir element tipini taffy node'a çevir /// Herhangi bir element tipini taffy node'a çevir
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn build_element( fn build_element(
@@ -354,78 +378,45 @@ fn build_element(
), ),
TemplateElement::Line(e) => { TemplateElement::Line(e) => {
let stroke_w = e.style.stroke_width.unwrap_or(0.5); let stroke_w = e.style.stroke_width.unwrap_or(0.5);
let style = sizing::leaf_style(&e.base.size, &e.base.position, parent_direction); let mut 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.base.size.height, SizeValue::Auto) { if matches!(e.base.size.height, SizeValue::Auto) {
leaf_style.size.height = Dimension::length(mm_to_pt(stroke_w)); style.size.height = Dimension::length(mm_to_pt(stroke_w));
} }
let mut rs: ResolvedStyle = (&e.style).into();
let node = taffy.new_leaf(leaf_style)?; rs.stroke_width = Some(stroke_w);
node_map.insert( register_leaf(
node, taffy, node_map, style,
NodeInfo { &e.base.id, e.type_str(),
element_id: e.base.id.clone(), Some(ResolvedContent::Line),
element_type: e.type_str().to_string(), rs,
content: Some(ResolvedContent::Line), )
style: {
let mut s: ResolvedStyle = (&e.style).into();
s.stroke_width = Some(stroke_w);
s
},
children_ids: vec![],
},
);
Ok(node)
} }
TemplateElement::Image(e) => { TemplateElement::Image(e) => {
let style = sizing::leaf_style(&e.base.size, &e.base.position, parent_direction); 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 src = resolved.images.get(&e.base.id).cloned().unwrap_or_default();
register_leaf(
let node = taffy.new_leaf(style)?; taffy, node_map, style,
node_map.insert( &e.base.id, e.type_str(),
node, Some(ResolvedContent::Image { src }),
NodeInfo { (&e.style).into(),
element_id: e.base.id.clone(), )
element_type: e.type_str().to_string(),
content: Some(ResolvedContent::Image { src }),
style: (&e.style).into(),
children_ids: vec![],
},
);
Ok(node)
} }
TemplateElement::Barcode(e) => { TemplateElement::Barcode(e) => {
let mut style = sizing::leaf_style(&e.base.size, &e.base.position, parent_direction); 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(); 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 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.base.size.height, SizeValue::Auto) { if matches!(e.base.size.height, SizeValue::Auto) {
style.min_size.height = Dimension::length(mm_to_pt(default_h)); style.min_size.height = Dimension::length(mm_to_pt(if is_qr { 20.0 } else { 15.0 }));
} }
if matches!(e.base.size.width, SizeValue::Auto) { if matches!(e.base.size.width, SizeValue::Auto) {
style.min_size.width = Dimension::length(mm_to_pt(default_w)); style.min_size.width = Dimension::length(mm_to_pt(if is_qr { 20.0 } else { 40.0 }));
} }
register_leaf(
let node = taffy.new_leaf(style)?; taffy, node_map, style,
node_map.insert( &e.base.id, e.type_str(),
node, Some(ResolvedContent::Barcode { format: e.format.clone(), value }),
NodeInfo { (&e.style).into(),
element_id: e.base.id.clone(), )
element_type: e.type_str().to_string(),
content: Some(ResolvedContent::Barcode {
format: e.format.clone(),
value,
}),
style: (&e.style).into(),
children_ids: vec![],
},
);
Ok(node)
} }
TemplateElement::RepeatingTable(e) => { TemplateElement::RepeatingTable(e) => {
// Tabloyu container ağacına expand et (cache ile) // Tabloyu container ağacına expand et (cache ile)
@@ -459,52 +450,33 @@ fn build_element(
} }
TemplateElement::Shape(e) => { TemplateElement::Shape(e) => {
let style = sizing::leaf_style(&e.base.size, &e.base.position, parent_direction); let style = sizing::leaf_style(&e.base.size, &e.base.position, parent_direction);
let node = taffy.new_leaf(style)?; register_leaf(
node_map.insert( taffy, node_map, style,
node, &e.base.id, e.type_str(),
NodeInfo { Some(ResolvedContent::Shape { shape_type: e.shape_type.clone() }),
element_id: e.base.id.clone(), (&e.style).into(),
element_type: e.type_str().to_string(), )
content: Some(ResolvedContent::Shape {
shape_type: e.shape_type.clone(),
}),
style: (&e.style).into(),
children_ids: vec![],
},
);
Ok(node)
} }
TemplateElement::Checkbox(e) => { TemplateElement::Checkbox(e) => {
let checked_str = resolved let checked = resolved
.texts .texts
.get(&e.base.id) .get(&e.base.id)
.map(|s| s.as_str()) .map(|s| s == "true")
.unwrap_or("false"); .unwrap_or(false);
let checked = checked_str == "true";
let box_size_mm = e.style.size.unwrap_or(4.0); let box_size_mm = e.style.size.unwrap_or(4.0);
let style = sizing::leaf_style(&e.base.size, &e.base.position, parent_direction); let mut 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.base.size.width, SizeValue::Auto) { if matches!(e.base.size.width, SizeValue::Auto) {
leaf_style.size.width = Dimension::length(mm_to_pt(box_size_mm)); style.size.width = Dimension::length(mm_to_pt(box_size_mm));
} }
if matches!(e.base.size.height, SizeValue::Auto) { if matches!(e.base.size.height, SizeValue::Auto) {
leaf_style.size.height = Dimension::length(mm_to_pt(box_size_mm)); style.size.height = Dimension::length(mm_to_pt(box_size_mm));
} }
register_leaf(
let node = taffy.new_leaf(leaf_style)?; taffy, node_map, style,
node_map.insert( &e.base.id, e.type_str(),
node, Some(ResolvedContent::Checkbox { checked }),
NodeInfo { (&e.style).into(),
element_id: e.base.id.clone(), )
element_type: e.type_str().to_string(),
content: Some(ResolvedContent::Checkbox { checked }),
style: (&e.style).into(),
children_ids: vec![],
},
);
Ok(node)
} }
TemplateElement::RichText(e) => { TemplateElement::RichText(e) => {
let spans = resolved.rich_texts.get(&e.base.id).cloned().unwrap_or_default(); let spans = resolved.rich_texts.get(&e.base.id).cloned().unwrap_or_default();
@@ -563,28 +535,20 @@ fn build_element(
} }
TemplateElement::Chart(e) => { TemplateElement::Chart(e) => {
let mut style = sizing::leaf_style(&e.base.size, &e.base.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.base.size.width, SizeValue::Auto) { if matches!(e.base.size.width, SizeValue::Auto) {
style.min_size.width = Dimension::length(mm_to_pt(80.0)); style.min_size.width = Dimension::length(mm_to_pt(80.0));
} }
if matches!(e.base.size.height, SizeValue::Auto) { if matches!(e.base.size.height, SizeValue::Auto) {
style.min_size.height = Dimension::length(mm_to_pt(60.0)); style.min_size.height = Dimension::length(mm_to_pt(60.0));
} }
let node = taffy.new_leaf(style)?; register_leaf(
node_map.insert( taffy, node_map, style,
node, &e.base.id, e.type_str(),
NodeInfo { None, // SVG collect_layout'ta üretilecek
element_id: e.base.id.clone(), ResolvedStyle::default(),
element_type: e.type_str().to_string(), )
content: None, // SVG collect_layout'ta uretilecek
style: ResolvedStyle::default(),
children_ids: vec![],
},
);
Ok(node)
} }
TemplateElement::PageBreak(e) => { TemplateElement::PageBreak(e) => {
// Küçük yükseklik — editörde görünür olması için (0.5mm ≈ 1.4pt)
let style = Style { let style = Style {
size: Size { size: Size {
width: Dimension::auto(), width: Dimension::auto(),
@@ -592,18 +556,12 @@ fn build_element(
}, },
..Default::default() ..Default::default()
}; };
let node = taffy.new_leaf(style)?; register_leaf(
node_map.insert( taffy, node_map, style,
node, &e.base.id, e.type_str(),
NodeInfo { None,
element_id: e.base.id.clone(), ResolvedStyle::default(),
element_type: e.type_str().to_string(), )
content: None,
style: ResolvedStyle::default(),
children_ids: vec![],
},
);
Ok(node)
} }
} }
} }
@@ -763,62 +721,12 @@ fn collect_layout(
let w_mm = pt_to_mm(layout.size.width); let w_mm = pt_to_mm(layout.size.width);
let h_mm = pt_to_mm(layout.size.height); let h_mm = pt_to_mm(layout.size.height);
// Chart elementleri icin SVG uret (boyutlar artik belli) // Chart elementleri için SVG üret (boyutlar artık belli)
let content = if info.element_type == "chart" { let content = if info.element_type == "chart" {
resolved.charts.get(&info.element_id).map(|cd| { resolved.charts.get(&info.element_id).map(|cd| {
use crate::chart_layout::DEFAULT_COLORS;
use crate::{ChartRenderData, ChartSeriesData};
// Renk paleti olustur
let n_colors = cd.categories.len().max(cd.series.len()).max(1);
let colors: Vec<String> = (0..n_colors)
.map(|i| {
cd.style
.colors
.as_ref()
.and_then(|c| c.get(i).cloned())
.unwrap_or_else(|| DEFAULT_COLORS[i % DEFAULT_COLORS.len()].to_string())
})
.collect();
ResolvedContent::Chart { ResolvedContent::Chart {
svg: crate::chart_render::render_svg(cd, w_mm, h_mm), svg: crate::chart_render::render_svg(cd, w_mm, h_mm),
chart_data: Box::new(ChartRenderData { chart_data: Box::new(crate::ChartRenderData::from(cd)),
chart_type: cd.chart_type.clone(),
categories: cd.categories.clone(),
series: cd
.series
.iter()
.map(|s| ChartSeriesData {
name: s.name.clone(),
values: s.values.clone(),
})
.collect(),
title_text: cd.title.as_ref().map(|t| t.text.clone()),
title_font_size: cd.title.as_ref().and_then(|t| t.font_size),
title_color: cd.title.as_ref().and_then(|t| t.color.clone()),
colors,
show_labels: cd.labels.as_ref().is_some_and(|l| l.show),
label_font_size: cd.labels.as_ref().and_then(|l| l.font_size),
show_grid: cd.axis.as_ref().and_then(|a| a.show_grid).unwrap_or(true),
grid_color: cd.axis.as_ref().and_then(|a| a.grid_color.clone()),
bar_gap: cd.style.bar_gap,
stacked: matches!(
cd.group_mode,
Some(dreport_core::models::GroupMode::Stacked)
),
inner_radius: cd.style.inner_radius,
show_points: cd.style.show_points,
line_width: cd.style.line_width,
background_color: cd.style.background_color.clone(),
label_color: cd.labels.as_ref().and_then(|l| l.color.clone()),
legend_show: cd.legend.as_ref().is_some_and(|l| l.show),
legend_position: cd.legend.as_ref().and_then(|l| l.position.clone()),
legend_font_size: cd.legend.as_ref().and_then(|l| l.font_size),
x_label: cd.axis.as_ref().and_then(|a| a.x_label.clone()),
y_label: cd.axis.as_ref().and_then(|a| a.y_label.clone()),
title_align: cd.title.as_ref().and_then(|t| t.align.clone()),
}),
} }
}) })
} else { } else {

File diff suppressed because one or more lines are too long