mirror of
https://github.com/duhanbalci/dreport.git
synced 2026-07-01 18:39:16 +00:00
refactor
This commit is contained in:
@@ -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('"', """)
|
.replace('"', """)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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::*;
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user