diff options
Diffstat (limited to 'src/geom')
| -rw-r--r-- | src/geom/sides.rs | 34 | ||||
| -rw-r--r-- | src/geom/stroke.rs | 285 |
2 files changed, 299 insertions, 20 deletions
diff --git a/src/geom/sides.rs b/src/geom/sides.rs index 25b1fab5..d9a020da 100644 --- a/src/geom/sides.rs +++ b/src/geom/sides.rs @@ -188,22 +188,30 @@ where fn cast(mut value: Value) -> StrResult<Self> { if let Value::Dict(dict) = &mut value { - let mut take = |key| dict.take(key).ok().map(T::cast).transpose(); - - let rest = take("rest")?; - let x = take("x")?.or_else(|| rest.clone()); - let y = take("y")?.or_else(|| rest.clone()); - let sides = Sides { - left: take("left")?.or_else(|| x.clone()), - top: take("top")?.or_else(|| y.clone()), - right: take("right")?.or_else(|| x.clone()), - bottom: take("bottom")?.or_else(|| y.clone()), + let mut try_cast = || -> StrResult<_> { + let mut take = |key| dict.take(key).ok().map(T::cast).transpose(); + + let rest = take("rest")?; + let x = take("x")?.or_else(|| rest.clone()); + let y = take("y")?.or_else(|| rest.clone()); + let sides = Sides { + left: take("left")?.or_else(|| x.clone()), + top: take("top")?.or_else(|| y.clone()), + right: take("right")?.or_else(|| x.clone()), + bottom: take("bottom")?.or_else(|| y.clone()), + }; + + dict.finish(&["left", "top", "right", "bottom", "x", "y", "rest"])?; + + Ok(sides) }; - dict.finish(&["left", "top", "right", "bottom", "x", "y", "rest"])?; + if let Ok(res) = try_cast() { + return Ok(res); + } + } - Ok(sides) - } else if T::is(&value) { + if T::is(&value) { Ok(Self::splat(Some(T::cast(value)?))) } else { <Self as Cast>::error(value) diff --git a/src/geom/stroke.rs b/src/geom/stroke.rs index 4dba06d9..344da3c5 100644 --- a/src/geom/stroke.rs +++ b/src/geom/stroke.rs @@ -7,6 +7,14 @@ pub struct Stroke { pub paint: Paint, /// The stroke's thickness. pub thickness: Abs, + /// The stroke's line cap. + pub line_cap: LineCap, + /// The stroke's line join. + pub line_join: LineJoin, + /// The stroke's line dash pattern. + pub dash_pattern: Option<DashPattern<Abs, Abs>>, + /// The miter limit. Defaults to 4.0, same as `tiny-skia`. + pub miter_limit: Scalar, } impl Default for Stroke { @@ -14,6 +22,10 @@ impl Default for Stroke { Self { paint: Paint::Solid(Color::BLACK), thickness: Abs::pt(1.0), + line_cap: LineCap::Butt, + line_join: LineJoin::Miter, + dash_pattern: None, + miter_limit: Scalar(4.0), } } } @@ -29,14 +41,41 @@ pub struct PartialStroke<T = Length> { pub paint: Smart<Paint>, /// The stroke's thickness. pub thickness: Smart<T>, + /// The stroke's line cap. + pub line_cap: Smart<LineCap>, + /// The stroke's line join. + pub line_join: Smart<LineJoin>, + /// The stroke's line dash pattern. + pub dash_pattern: Smart<Option<DashPattern<T>>>, + /// The miter limit. + pub miter_limit: Smart<Scalar>, } impl PartialStroke<Abs> { /// Unpack the stroke, filling missing fields from the `default`. pub fn unwrap_or(self, default: Stroke) -> Stroke { + let thickness = self.thickness.unwrap_or(default.thickness); + let dash_pattern = self + .dash_pattern + .map(|pattern| { + pattern.map(|pattern| DashPattern { + array: pattern + .array + .into_iter() + .map(|l| l.finish(thickness)) + .collect(), + phase: pattern.phase, + }) + }) + .unwrap_or(default.dash_pattern); + Stroke { paint: self.paint.unwrap_or(default.paint), - thickness: self.thickness.unwrap_or(default.thickness), + thickness, + line_cap: self.line_cap.unwrap_or(default.line_cap), + line_join: self.line_join.unwrap_or(default.line_join), + dash_pattern, + miter_limit: self.miter_limit.unwrap_or(default.miter_limit), } } @@ -48,13 +87,205 @@ impl PartialStroke<Abs> { impl<T: Debug> Debug for PartialStroke<T> { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match (&self.paint, &self.thickness) { - (Smart::Custom(paint), Smart::Custom(thickness)) => { - write!(f, "{thickness:?} + {paint:?}") + let Self { + paint, + thickness, + line_cap, + line_join, + dash_pattern, + miter_limit, + } = &self; + if line_cap.is_auto() + && line_join.is_auto() + && dash_pattern.is_auto() + && miter_limit.is_auto() + { + match (&self.paint, &self.thickness) { + (Smart::Custom(paint), Smart::Custom(thickness)) => { + write!(f, "{thickness:?} + {paint:?}") + } + (Smart::Custom(paint), Smart::Auto) => paint.fmt(f), + (Smart::Auto, Smart::Custom(thickness)) => thickness.fmt(f), + (Smart::Auto, Smart::Auto) => f.pad("<stroke>"), + } + } else { + write!(f, "(")?; + let mut sep = ""; + if let Smart::Custom(paint) = &paint { + write!(f, "{}color: {:?}", sep, paint)?; + sep = ", "; + } + if let Smart::Custom(thickness) = &thickness { + write!(f, "{}thickness: {:?}", sep, thickness)?; + sep = ", "; + } + if let Smart::Custom(cap) = &line_cap { + write!(f, "{}cap: {:?}", sep, cap)?; + sep = ", "; + } + if let Smart::Custom(join) = &line_join { + write!(f, "{}join: {:?}", sep, join)?; + sep = ", "; + } + if let Smart::Custom(dash) = &dash_pattern { + write!(f, "{}dash: {:?}", sep, dash)?; + sep = ", "; + } + if let Smart::Custom(miter_limit) = &miter_limit { + write!(f, "{}miter-limit: {:?}", sep, miter_limit)?; + } + write!(f, ")")?; + Ok(()) + } + } +} + +/// The line cap of a stroke +#[derive(Cast, Clone, Eq, PartialEq, Hash)] +pub enum LineCap { + Butt, + Round, + Square, +} + +impl Debug for LineCap { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + LineCap::Butt => write!(f, "\"butt\""), + LineCap::Round => write!(f, "\"round\""), + LineCap::Square => write!(f, "\"square\""), + } + } +} + +/// The line join of a stroke +#[derive(Cast, Clone, Eq, PartialEq, Hash)] +pub enum LineJoin { + Miter, + Round, + Bevel, +} + +impl Debug for LineJoin { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + LineJoin::Miter => write!(f, "\"miter\""), + LineJoin::Round => write!(f, "\"round\""), + LineJoin::Bevel => write!(f, "\"bevel\""), + } + } +} + +/// A line dash pattern +#[derive(Clone, Eq, PartialEq, Hash)] +pub struct DashPattern<T = Length, DT = DashLength<T>> { + /// The dash array. + pub array: Vec<DT>, + /// The dash phase. + pub phase: T, +} + +impl<T: Debug, DT: Debug> Debug for DashPattern<T, DT> { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "(array: (")?; + for (i, elem) in self.array.iter().enumerate() { + if i == 0 { + write!(f, "{:?}", elem)?; + } else { + write!(f, ", {:?}", elem)?; } - (Smart::Custom(paint), Smart::Auto) => paint.fmt(f), - (Smart::Auto, Smart::Custom(thickness)) => thickness.fmt(f), - (Smart::Auto, Smart::Auto) => f.pad("<stroke>"), + } + write!(f, "), phase: {:?})", self.phase)?; + Ok(()) + } +} + +impl<T: Default> From<Vec<DashLength<T>>> for DashPattern<T> { + fn from(array: Vec<DashLength<T>>) -> Self { + Self { array, phase: T::default() } + } +} + +/// The length of a dash in a line dash pattern +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum DashLength<T = Length> { + LineWidth, + Length(T), +} + +impl From<Abs> for DashLength { + fn from(l: Abs) -> Self { + DashLength::Length(l.into()) + } +} + +impl<T> DashLength<T> { + fn finish(self, line_width: T) -> T { + match self { + Self::LineWidth => line_width, + Self::Length(l) => l, + } + } +} + +cast_from_value! { + DashLength: "dash length", + "dot" => Self::LineWidth, + l: Length => Self::Length(l), +} + +impl Resolve for DashLength { + type Output = DashLength<Abs>; + + fn resolve(self, styles: StyleChain) -> Self::Output { + match self { + Self::LineWidth => DashLength::LineWidth, + Self::Length(l) => DashLength::Length(l.resolve(styles)), + } + } +} + +cast_from_value! { + DashPattern: "dash pattern", + // Use same names as tikz: + // https://tex.stackexchange.com/questions/45275/tikz-get-values-for-predefined-dash-patterns + "solid" => Vec::new().into(), + "dotted" => vec![DashLength::LineWidth, Abs::pt(2.0).into()].into(), + "densely-dotted" => vec![DashLength::LineWidth, Abs::pt(1.0).into()].into(), + "loosely-dotted" => vec![DashLength::LineWidth, Abs::pt(4.0).into()].into(), + "dashed" => vec![Abs::pt(3.0).into(), Abs::pt(3.0).into()].into(), + "densely-dashed" => vec![Abs::pt(3.0).into(), Abs::pt(2.0).into()].into(), + "loosely-dashed" => vec![Abs::pt(3.0).into(), Abs::pt(6.0).into()].into(), + "dashdotted" => vec![Abs::pt(3.0).into(), Abs::pt(2.0).into(), DashLength::LineWidth, Abs::pt(2.0).into()].into(), + "densely-dashdotted" => vec![Abs::pt(3.0).into(), Abs::pt(1.0).into(), DashLength::LineWidth, Abs::pt(1.0).into()].into(), + "loosely-dashdotted" => vec![Abs::pt(3.0).into(), Abs::pt(4.0).into(), DashLength::LineWidth, Abs::pt(4.0).into()].into(), + array: Vec<DashLength> => { + Self { + array, + phase: Length::zero(), + } + }, + mut dict: Dict => { + let array: Vec<DashLength> = dict.take("array")?.cast()?; + let phase = dict.take("phase").ok().map(Length::cast) + .transpose()?.unwrap_or(Length::zero()); + + dict.finish(&["array", "phase"])?; + + Self { + array, + phase, + } + }, +} + +impl Resolve for DashPattern { + type Output = DashPattern<Abs>; + + fn resolve(self, styles: StyleChain) -> Self::Output { + DashPattern { + array: self.array.into_iter().map(|l| l.resolve(styles)).collect(), + phase: self.phase.resolve(styles), } } } @@ -64,10 +295,42 @@ cast_from_value! { thickness: Length => Self { paint: Smart::Auto, thickness: Smart::Custom(thickness), + line_cap: Smart::Auto, + line_join: Smart::Auto, + dash_pattern: Smart::Auto, + miter_limit: Smart::Auto, }, color: Color => Self { paint: Smart::Custom(color.into()), thickness: Smart::Auto, + line_cap: Smart::Auto, + line_join: Smart::Auto, + dash_pattern: Smart::Auto, + miter_limit: Smart::Auto, + }, + mut dict: Dict => { + fn take<T: Cast<Value>>(dict: &mut Dict, key: &str) -> StrResult<Smart<T>> { + Ok(dict.take(key).ok().map(T::cast) + .transpose()?.map(Smart::Custom).unwrap_or(Smart::Auto)) + } + + let paint = take::<Paint>(&mut dict, "color")?; + let thickness = take::<Length>(&mut dict, "thickness")?; + let line_cap = take::<LineCap>(&mut dict, "cap")?; + let line_join = take::<LineJoin>(&mut dict, "join")?; + let dash_pattern = take::<Option<DashPattern>>(&mut dict, "dash")?; + let miter_limit = take::<f64>(&mut dict, "miter-limit")?; + + dict.finish(&["color", "thickness", "cap", "join", "dash", "miter-limit"])?; + + Self { + paint, + thickness, + line_cap, + line_join, + dash_pattern, + miter_limit: miter_limit.map(Scalar), + } }, } @@ -78,6 +341,10 @@ impl Resolve for PartialStroke { PartialStroke { paint: self.paint, thickness: self.thickness.resolve(styles), + line_cap: self.line_cap, + line_join: self.line_join, + dash_pattern: self.dash_pattern.resolve(styles), + miter_limit: self.miter_limit, } } } @@ -89,6 +356,10 @@ impl Fold for PartialStroke<Abs> { Self { paint: self.paint.or(outer.paint), thickness: self.thickness.or(outer.thickness), + line_cap: self.line_cap.or(outer.line_cap), + line_join: self.line_join.or(outer.line_join), + dash_pattern: self.dash_pattern.or(outer.dash_pattern), + miter_limit: self.miter_limit, } } } |
