use std::str::FromStr;
use crate::color::Color;
#[derive(Debug, Default, Clone, PartialEq)]
pub enum StrokeStyle {
#[default]
Solid,
Dashed,
Dotted,
DashDot,
DashDotDot,
Custom(String),
}
impl FromStr for StrokeStyle {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"solid" => Ok(Self::Solid),
"dashed" => Ok(Self::Dashed),
"dotted" => Ok(Self::Dotted),
"dash-dot" | "dashdot" => Ok(Self::DashDot),
"dash-dot-dot" | "dashdotdot" => Ok(Self::DashDotDot),
_ => Ok(Self::Custom(s.to_string())),
}
}
}
impl StrokeStyle {
pub fn to_svg_value(&self) -> Option<String> {
match self {
Self::Solid => None,
Self::Dashed => Some("5,5".to_string()),
Self::Dotted => Some("2,3".to_string()),
Self::DashDot => Some("10,5,2,5".to_string()),
Self::DashDotDot => Some("10,5,2,5,2,5".to_string()),
Self::Custom(pattern) => Some(pattern.clone()),
}
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum StrokeCap {
#[default]
Butt,
Round,
Square,
}
impl StrokeCap {
pub fn to_svg_value(&self) -> &'static str {
match self {
Self::Butt => "butt",
Self::Round => "round",
Self::Square => "square",
}
}
}
impl FromStr for StrokeCap {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"butt" => Ok(Self::Butt),
"round" => Ok(Self::Round),
"square" => Ok(Self::Square),
_ => Err(format!(
"invalid stroke cap `{s}`, valid values: butt, round, square"
)),
}
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum StrokeJoin {
#[default]
Miter,
Round,
Bevel,
}
impl StrokeJoin {
pub fn to_svg_value(&self) -> &'static str {
match self {
Self::Miter => "miter",
Self::Round => "round",
Self::Bevel => "bevel",
}
}
}
impl FromStr for StrokeJoin {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"miter" => Ok(Self::Miter),
"round" => Ok(Self::Round),
"bevel" => Ok(Self::Bevel),
_ => Err(format!(
"invalid stroke join `{s}`, valid values: miter, round, bevel"
)),
}
}
}
#[derive(Debug, Clone)]
pub struct StrokeDefinition {
color: Color,
width: f32,
style: StrokeStyle,
cap: StrokeCap,
join: StrokeJoin,
}
impl StrokeDefinition {
pub fn default_solid() -> Self {
Self {
color: Color::default(),
width: 2.0,
style: StrokeStyle::Solid,
cap: StrokeCap::Butt,
join: StrokeJoin::Miter,
}
}
pub fn default_dashed() -> Self {
Self {
color: Color::default(),
width: 1.0,
style: StrokeStyle::Dashed,
cap: StrokeCap::Butt,
join: StrokeJoin::Miter,
}
}
pub fn new(color: Color, width: f32) -> Self {
Self {
color,
width,
..Self::default()
}
}
pub fn solid(color: Color, width: f32) -> Self {
Self::new(color, width)
}
pub fn dashed(color: Color, width: f32) -> Self {
let mut stroke = Self::new(color, width);
stroke.set_style(StrokeStyle::Dashed);
stroke
}
pub fn dotted(color: Color, width: f32) -> Self {
let mut stroke = Self::new(color, width);
stroke.set_style(StrokeStyle::Dotted);
stroke
}
pub fn color(&self) -> Color {
self.color
}
pub fn width(&self) -> f32 {
self.width
}
pub fn style(&self) -> &StrokeStyle {
&self.style
}
pub fn cap(&self) -> StrokeCap {
self.cap
}
pub fn join(&self) -> StrokeJoin {
self.join
}
pub fn set_color(&mut self, color: Color) {
self.color = color;
}
pub fn set_width(&mut self, width: f32) {
self.width = width;
}
pub fn set_style(&mut self, style: StrokeStyle) {
self.style = style;
}
pub fn set_cap(&mut self, cap: StrokeCap) {
self.cap = cap;
}
pub fn set_join(&mut self, join: StrokeJoin) {
self.join = join;
}
}
impl Default for StrokeDefinition {
fn default() -> Self {
Self {
color: Color::default(),
width: 1.0,
style: StrokeStyle::default(),
cap: StrokeCap::default(),
join: StrokeJoin::default(),
}
}
}
#[macro_export]
macro_rules! apply_stroke {
($element:expr, $stroke:expr) => {{
let mut elem = $element
.set("stroke", $stroke.color().to_string())
.set("stroke-opacity", $stroke.color().alpha())
.set("stroke-width", $stroke.width())
.set("stroke-linecap", $stroke.cap().to_svg_value())
.set("stroke-linejoin", $stroke.join().to_svg_value());
if let Some(dasharray) = $stroke.style().to_svg_value() {
elem = elem.set("stroke-dasharray", dasharray);
}
elem
}};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stroke_default() {
let stroke = StrokeDefinition::default();
assert_eq!(stroke.width(), 1.0);
assert_eq!(stroke.color().to_string(), "black");
assert_eq!(*stroke.style(), StrokeStyle::Solid);
assert_eq!(stroke.cap(), StrokeCap::Butt);
assert_eq!(stroke.join(), StrokeJoin::Miter);
}
#[test]
fn test_stroke_constructors() {
let color = Color::new("red").unwrap();
let solid = StrokeDefinition::solid(color, 2.0);
assert_eq!(solid.width(), 2.0);
assert_eq!(*solid.style(), StrokeStyle::Solid);
let dashed = StrokeDefinition::dashed(color, 1.5);
assert_eq!(*dashed.style(), StrokeStyle::Dashed);
let dotted = StrokeDefinition::dotted(color, 1.0);
assert_eq!(*dotted.style(), StrokeStyle::Dotted);
}
#[test]
fn test_stroke_setters_builder_style() {
let mut stroke = StrokeDefinition::new(Color::new("blue").unwrap(), 3.0);
stroke.set_style(StrokeStyle::DashDot);
stroke.set_cap(StrokeCap::Round);
stroke.set_join(StrokeJoin::Round);
assert_eq!(stroke.width(), 3.0);
assert_eq!(*stroke.style(), StrokeStyle::DashDot);
assert_eq!(stroke.cap(), StrokeCap::Round);
assert_eq!(stroke.join(), StrokeJoin::Round);
}
#[test]
fn test_stroke_setters() {
let mut stroke = StrokeDefinition::default();
stroke.set_color(Color::new("green").unwrap());
stroke.set_width(2.5);
stroke.set_style(StrokeStyle::Dashed);
stroke.set_cap(StrokeCap::Square);
stroke.set_join(StrokeJoin::Bevel);
assert_eq!(stroke.color().to_string(), "green");
assert_eq!(stroke.width(), 2.5);
assert_eq!(*stroke.style(), StrokeStyle::Dashed);
assert_eq!(stroke.cap(), StrokeCap::Square);
assert_eq!(stroke.join(), StrokeJoin::Bevel);
}
#[test]
fn test_stroke_style_dasharray() {
assert_eq!(StrokeStyle::Solid.to_svg_value(), None);
assert_eq!(StrokeStyle::Dashed.to_svg_value(), Some("5,5".to_string()));
assert_eq!(StrokeStyle::Dotted.to_svg_value(), Some("2,3".to_string()));
assert_eq!(
StrokeStyle::DashDot.to_svg_value(),
Some("10,5,2,5".to_string())
);
assert_eq!(
StrokeStyle::DashDotDot.to_svg_value(),
Some("10,5,2,5,2,5".to_string())
);
let custom = StrokeStyle::Custom("15,3,3,3".to_string());
assert_eq!(custom.to_svg_value(), Some("15,3,3,3".to_string()));
}
#[test]
fn test_stroke_cap_svg_values() {
assert_eq!(StrokeCap::Butt.to_svg_value(), "butt");
assert_eq!(StrokeCap::Round.to_svg_value(), "round");
assert_eq!(StrokeCap::Square.to_svg_value(), "square");
}
#[test]
fn test_stroke_join_svg_values() {
assert_eq!(StrokeJoin::Miter.to_svg_value(), "miter");
assert_eq!(StrokeJoin::Round.to_svg_value(), "round");
assert_eq!(StrokeJoin::Bevel.to_svg_value(), "bevel");
}
#[test]
fn test_stroke_cap_from_str() {
use std::str::FromStr;
assert_eq!(StrokeCap::from_str("butt").unwrap(), StrokeCap::Butt);
assert_eq!(StrokeCap::from_str("round").unwrap(), StrokeCap::Round);
assert_eq!(StrokeCap::from_str("square").unwrap(), StrokeCap::Square);
let result = StrokeCap::from_str("invalid");
assert!(result.is_err());
assert!(result.unwrap_err().contains("invalid stroke cap"));
}
#[test]
fn test_stroke_join_from_str() {
use std::str::FromStr;
assert_eq!(StrokeJoin::from_str("miter").unwrap(), StrokeJoin::Miter);
assert_eq!(StrokeJoin::from_str("round").unwrap(), StrokeJoin::Round);
assert_eq!(StrokeJoin::from_str("bevel").unwrap(), StrokeJoin::Bevel);
let result = StrokeJoin::from_str("invalid");
assert!(result.is_err());
assert!(result.unwrap_err().contains("invalid stroke join"));
}
#[test]
fn test_stroke_style_from_str() {
use std::str::FromStr;
assert_eq!(StrokeStyle::from_str("solid").unwrap(), StrokeStyle::Solid);
assert_eq!(
StrokeStyle::from_str("dashed").unwrap(),
StrokeStyle::Dashed
);
assert_eq!(
StrokeStyle::from_str("dotted").unwrap(),
StrokeStyle::Dotted
);
assert_eq!(
StrokeStyle::from_str("dash-dot").unwrap(),
StrokeStyle::DashDot
);
assert_eq!(
StrokeStyle::from_str("dashdot").unwrap(),
StrokeStyle::DashDot
);
assert_eq!(
StrokeStyle::from_str("dash-dot-dot").unwrap(),
StrokeStyle::DashDotDot
);
assert_eq!(
StrokeStyle::from_str("dashdotdot").unwrap(),
StrokeStyle::DashDotDot
);
assert_eq!(
StrokeStyle::from_str("10,5,2,5").unwrap(),
StrokeStyle::Custom("10,5,2,5".to_string())
);
assert_eq!(
StrokeStyle::from_str("5,5").unwrap(),
StrokeStyle::Custom("5,5".to_string())
);
assert_eq!(
StrokeStyle::from_str("arbitrary-pattern").unwrap(),
StrokeStyle::Custom("arbitrary-pattern".to_string())
);
}
}