use std::fmt::Write;
use gpui::{App, Hsla, Rems, Rgba, SharedString};
use super::{
geometry::{Geometry, Ink, Rect, frame},
series::tangents,
};
use crate::theme::{ActiveTheme, Palette, Radius, TextSize};
/// An SVG page: its size, the plot's frame, and the theme's colors and measures at the base rem.
pub(crate) struct Sheet {
pub size: (f32, f32),
pub frame: Rect,
stroke: f32,
hairline: f32,
corner: f32,
text: f32,
font: SharedString,
colors: Palette,
horizontal: bool,
smooth: bool,
}
impl Sheet {
pub(crate) fn new(size: (f32, f32), horizontal: bool, smooth: bool, cx: &App) -> Self {
let theme = cx.theme();
let (sizes, rem) = (theme.chart(), theme.base_rem());
let pixels = |length: Rems| f32::from(length.to_pixels(rem));
Self {
size,
frame: frame(
size,
pixels(sizes.gutter),
pixels(sizes.foot),
pixels(sizes.inset),
),
stroke: pixels(sizes.stroke),
hairline: f32::from(sizes.hairline),
corner: pixels(theme.radius(Radius::Sm)),
text: pixels(theme.text_size(TextSize::Xs)),
font: theme.font_family.clone(),
colors: theme.colors.clone(),
horizontal,
smooth,
}
}
}
/// A color as SVG writes it, and its opacity apart.
fn hex(color: Hsla) -> (String, f32) {
let rgba: Rgba = color.into();
let byte = |channel: f32| (channel.clamp(0.0, 1.0) * 255.0).round() as u8;
(
format!(
"#{:02x}{:02x}{:02x}",
byte(rgba.r),
byte(rgba.g),
byte(rgba.b)
),
rgba.a,
)
}
/// Text safe inside SVG.
fn escaped(text: &str) -> String {
text.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
}
/// A path's commands through `points`, straight or smooth.
fn path(points: &[(f32, f32)], smooth: bool) -> String {
let mut d = String::new();
let Some((x, y)) = points.first() else {
return d;
};
write!(d, "M{x:.1},{y:.1}").expect("a string takes writes");
if !smooth || points.len() < 3 {
for (x, y) in &points[1..] {
write!(d, " L{x:.1},{y:.1}").expect("a string takes writes");
}
return d;
}
let slopes = tangents(points);
for ix in 0..points.len() - 1 {
let ((x0, y0), (x1, y1)) = (points[ix], points[ix + 1]);
let third = (x1 - x0) / 3.0;
let (ax, ay, bx, by) = (
x0 + third,
y0 + slopes[ix] * third,
x1 - third,
y1 - slopes[ix + 1] * third,
);
write!(d, " C{ax:.1},{ay:.1} {bx:.1},{by:.1} {x1:.1},{y1:.1}")
.expect("a string takes writes");
}
d
}
/// A chart as a standalone SVG drawing: gridlines, marks and axis labels.
pub(crate) fn svg(geometry: &Geometry, sheet: &Sheet, format: &dyn Fn(f64) -> String) -> String {
let (colors, frame, (width, height)) = (&sheet.colors, sheet.frame, sheet.size);
let ink = |ink: Ink| hex(ink.color(colors)).0;
let (grid, _) = hex(colors.border);
let (label, _) = hex(colors.fg_subtle);
let (back, _) = hex(colors.bg);
let (gap, drop) = (sheet.text * 0.75, sheet.text * 1.6);
let (below, beside) = (frame.y + frame.h + drop, frame.x - gap);
let mut out = format!(
r#"");
out
}
#[cfg(test)]
mod tests {
use super::{escaped, path};
#[test]
fn paths_and_text_write_as_svg_reads_them() {
assert_eq!(
path(&[(0.0, 10.0), (5.0, 2.5)], false),
"M0.0,10.0 L5.0,2.5"
);
assert!(path(&[(0.0, 0.0), (1.0, 2.0), (2.0, 0.0)], true).contains(" C"));
assert_eq!(escaped("R&D "), "R&D <net>");
}
}