diff options
| author | Laurenz <laurmaedje@gmail.com> | 2023-06-06 21:13:59 +0200 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2023-06-06 22:06:16 +0200 |
| commit | fd417da04f7ca4b995de7f6510abafd3e9c31307 (patch) | |
| tree | 3675529c75ca7363701ac8ea306de2cc1d3cbcb3 /src/geom | |
| parent | 168bdf35bd773e67343c965cb473492cc5cae9e7 (diff) | |
Improve value casting infrastructure
Diffstat (limited to 'src/geom')
| -rw-r--r-- | src/geom/abs.rs | 5 | ||||
| -rw-r--r-- | src/geom/align.rs | 67 | ||||
| -rw-r--r-- | src/geom/axes.rs | 8 | ||||
| -rw-r--r-- | src/geom/color.rs | 386 | ||||
| -rw-r--r-- | src/geom/corners.rs | 82 | ||||
| -rw-r--r-- | src/geom/dir.rs | 4 | ||||
| -rw-r--r-- | src/geom/em.rs | 5 | ||||
| -rw-r--r-- | src/geom/length.rs | 2 | ||||
| -rw-r--r-- | src/geom/mod.rs | 52 | ||||
| -rw-r--r-- | src/geom/paint.rs | 393 | ||||
| -rw-r--r-- | src/geom/rel.rs | 5 | ||||
| -rw-r--r-- | src/geom/sides.rs | 81 | ||||
| -rw-r--r-- | src/geom/smart.rs | 42 | ||||
| -rw-r--r-- | src/geom/stroke.rs | 224 |
14 files changed, 672 insertions, 684 deletions
diff --git a/src/geom/abs.rs b/src/geom/abs.rs index 34c3d010..4ca3a9a1 100644 --- a/src/geom/abs.rs +++ b/src/geom/abs.rs @@ -214,8 +214,9 @@ impl<'a> Sum<&'a Self> for Abs { } } -cast_to_value! { - v: Abs => Value::Length(v.into()) +cast! { + Abs, + self => Value::Length(self.into()), } /// Different units of absolute measurement. diff --git a/src/geom/align.rs b/src/geom/align.rs index 42fc493e..dca35891 100644 --- a/src/geom/align.rs +++ b/src/geom/align.rs @@ -128,16 +128,27 @@ impl Debug for GenAlign { } } -cast_from_value! { - GenAlign: "alignment", +cast! { + type GenAlign: "alignment", } -cast_from_value! { - Axes<GenAlign>: "2d alignment", +cast! { + type Axes<GenAlign>: "2d alignment", } -cast_from_value! { +cast! { + Axes<Align>, + self => self.map(GenAlign::from).into_value(), +} + +cast! { Axes<Option<GenAlign>>, + self => match (self.x, self.y) { + (Some(x), Some(y)) => Axes::new(x, y).into_value(), + (Some(x), None) => x.into_value(), + (None, Some(y)) => y.into_value(), + (None, None) => Value::None, + }, align: GenAlign => { let mut aligns = Axes::default(); aligns.set(align.axis(), Some(align)); @@ -146,19 +157,6 @@ cast_from_value! { aligns: Axes<GenAlign> => aligns.map(Some), } -cast_to_value! { - v: Axes<Align> => v.map(GenAlign::from).into() -} - -cast_to_value! { - v: Axes<Option<GenAlign>> => match (v.x, v.y) { - (Some(x), Some(y)) => Axes::new(x, y).into(), - (Some(x), None) => x.into(), - (None, Some(y)) => y.into(), - (None, None) => Value::None, - } -} - impl From<Axes<GenAlign>> for Axes<Option<GenAlign>> { fn from(axes: Axes<GenAlign>) -> Self { axes.map(Some) @@ -213,8 +211,9 @@ impl Fold for Align { #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct HorizontalAlign(pub GenAlign); -cast_from_value! { +cast! { HorizontalAlign, + self => self.0.into_value(), align: GenAlign => { if align.axis() != Axis::X { Err("alignment must be horizontal")?; @@ -223,17 +222,14 @@ cast_from_value! { }, } -cast_to_value! { - v: HorizontalAlign => v.0.into() -} - /// Utility struct to restrict a passed alignment value to the vertical axis on /// cast. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct VerticalAlign(pub GenAlign); -cast_from_value! { +cast! { VerticalAlign, + self => self.0.into_value(), align: GenAlign => { if align.axis() != Axis::Y { Err("alignment must be vertical")?; @@ -241,26 +237,3 @@ cast_from_value! { Self(align) }, } - -cast_to_value! { - v: VerticalAlign => v.0.into() -} - -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub enum LeftRightAlternator { - Left, - Right, -} - -impl Iterator for LeftRightAlternator { - type Item = LeftRightAlternator; - - fn next(&mut self) -> Option<Self::Item> { - let r = Some(*self); - match self { - Self::Left => *self = Self::Right, - Self::Right => *self = Self::Left, - } - r - } -} diff --git a/src/geom/axes.rs b/src/geom/axes.rs index 511e6ff5..35c94129 100644 --- a/src/geom/axes.rs +++ b/src/geom/axes.rs @@ -2,7 +2,6 @@ use std::any::Any; use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Not}; use super::*; -use crate::eval::Array; /// A container with a horizontal and vertical component. #[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] @@ -274,8 +273,9 @@ impl BitAndAssign for Axes<bool> { } } -cast_from_value! { +cast! { Axes<Rel<Length>>, + self => array![self.x, self.y].into_value(), array: Array => { let mut iter = array.into_iter(); match (iter.next(), iter.next(), iter.next()) { @@ -285,10 +285,6 @@ cast_from_value! { }, } -cast_to_value! { - v: Axes<Rel<Length>> => Value::Array(array![v.x, v.y]) -} - impl<T: Resolve> Resolve for Axes<T> { type Output = Axes<T::Output>; diff --git a/src/geom/color.rs b/src/geom/color.rs new file mode 100644 index 00000000..c7676c2b --- /dev/null +++ b/src/geom/color.rs @@ -0,0 +1,386 @@ +use std::str::FromStr; + +use super::*; + +/// A color in a dynamic format. +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub enum Color { + /// An 8-bit luma color. + Luma(LumaColor), + /// An 8-bit RGBA color. + Rgba(RgbaColor), + /// An 8-bit CMYK color. + Cmyk(CmykColor), +} + +impl Color { + pub const BLACK: Self = Self::Rgba(RgbaColor::new(0x00, 0x00, 0x00, 0xFF)); + pub const GRAY: Self = Self::Rgba(RgbaColor::new(0xAA, 0xAA, 0xAA, 0xFF)); + pub const SILVER: Self = Self::Rgba(RgbaColor::new(0xDD, 0xDD, 0xDD, 0xFF)); + pub const WHITE: Self = Self::Rgba(RgbaColor::new(0xFF, 0xFF, 0xFF, 0xFF)); + pub const NAVY: Self = Self::Rgba(RgbaColor::new(0x00, 0x1f, 0x3f, 0xFF)); + pub const BLUE: Self = Self::Rgba(RgbaColor::new(0x00, 0x74, 0xD9, 0xFF)); + pub const AQUA: Self = Self::Rgba(RgbaColor::new(0x7F, 0xDB, 0xFF, 0xFF)); + pub const TEAL: Self = Self::Rgba(RgbaColor::new(0x39, 0xCC, 0xCC, 0xFF)); + pub const EASTERN: Self = Self::Rgba(RgbaColor::new(0x23, 0x9D, 0xAD, 0xFF)); + pub const PURPLE: Self = Self::Rgba(RgbaColor::new(0xB1, 0x0D, 0xC9, 0xFF)); + pub const FUCHSIA: Self = Self::Rgba(RgbaColor::new(0xF0, 0x12, 0xBE, 0xFF)); + pub const MAROON: Self = Self::Rgba(RgbaColor::new(0x85, 0x14, 0x4b, 0xFF)); + pub const RED: Self = Self::Rgba(RgbaColor::new(0xFF, 0x41, 0x36, 0xFF)); + pub const ORANGE: Self = Self::Rgba(RgbaColor::new(0xFF, 0x85, 0x1B, 0xFF)); + pub const YELLOW: Self = Self::Rgba(RgbaColor::new(0xFF, 0xDC, 0x00, 0xFF)); + pub const OLIVE: Self = Self::Rgba(RgbaColor::new(0x3D, 0x99, 0x70, 0xFF)); + pub const GREEN: Self = Self::Rgba(RgbaColor::new(0x2E, 0xCC, 0x40, 0xFF)); + pub const LIME: Self = Self::Rgba(RgbaColor::new(0x01, 0xFF, 0x70, 0xFF)); + + /// Convert this color to RGBA. + pub fn to_rgba(self) -> RgbaColor { + match self { + Self::Luma(luma) => luma.to_rgba(), + Self::Rgba(rgba) => rgba, + Self::Cmyk(cmyk) => cmyk.to_rgba(), + } + } + + /// Lighten this color by the given factor. + pub fn lighten(self, factor: Ratio) -> Self { + match self { + Self::Luma(luma) => Self::Luma(luma.lighten(factor)), + Self::Rgba(rgba) => Self::Rgba(rgba.lighten(factor)), + Self::Cmyk(cmyk) => Self::Cmyk(cmyk.lighten(factor)), + } + } + + /// Darken this color by the given factor. + pub fn darken(self, factor: Ratio) -> Self { + match self { + Self::Luma(luma) => Self::Luma(luma.darken(factor)), + Self::Rgba(rgba) => Self::Rgba(rgba.darken(factor)), + Self::Cmyk(cmyk) => Self::Cmyk(cmyk.darken(factor)), + } + } + + /// Negate this color. + pub fn negate(self) -> Self { + match self { + Self::Luma(luma) => Self::Luma(luma.negate()), + Self::Rgba(rgba) => Self::Rgba(rgba.negate()), + Self::Cmyk(cmyk) => Self::Cmyk(cmyk.negate()), + } + } +} + +impl Debug for Color { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Luma(c) => Debug::fmt(c, f), + Self::Rgba(c) => Debug::fmt(c, f), + Self::Cmyk(c) => Debug::fmt(c, f), + } + } +} + +/// An 8-bit grayscale color. +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct LumaColor(pub u8); + +impl LumaColor { + /// Construct a new luma color. + pub const fn new(luma: u8) -> Self { + Self(luma) + } + + /// Convert to an opque RGBA color. + pub const fn to_rgba(self) -> RgbaColor { + RgbaColor::new(self.0, self.0, self.0, u8::MAX) + } + + /// Convert to CMYK as a fraction of true black. + pub fn to_cmyk(self) -> CmykColor { + CmykColor::new( + round_u8(self.0 as f64 * 0.75), + round_u8(self.0 as f64 * 0.68), + round_u8(self.0 as f64 * 0.67), + round_u8(self.0 as f64 * 0.90), + ) + } + + /// Lighten this color by a factor. + pub fn lighten(self, factor: Ratio) -> Self { + let inc = round_u8((u8::MAX - self.0) as f64 * factor.get()); + Self(self.0.saturating_add(inc)) + } + + /// Darken this color by a factor. + pub fn darken(self, factor: Ratio) -> Self { + let dec = round_u8(self.0 as f64 * factor.get()); + Self(self.0.saturating_sub(dec)) + } + + /// Negate this color. + pub fn negate(self) -> Self { + Self(u8::MAX - self.0) + } +} + +impl Debug for LumaColor { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "luma({})", self.0) + } +} + +impl From<LumaColor> for Color { + fn from(luma: LumaColor) -> Self { + Self::Luma(luma) + } +} + +/// An 8-bit RGBA color. +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct RgbaColor { + /// Red channel. + pub r: u8, + /// Green channel. + pub g: u8, + /// Blue channel. + pub b: u8, + /// Alpha channel. + pub a: u8, +} + +impl RgbaColor { + /// Construct a new RGBA color. + pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self { + Self { r, g, b, a } + } + + /// Lighten this color by a factor. + /// + /// The alpha channel is not affected. + pub fn lighten(self, factor: Ratio) -> Self { + let lighten = + |c: u8| c.saturating_add(round_u8((u8::MAX - c) as f64 * factor.get())); + Self { + r: lighten(self.r), + g: lighten(self.g), + b: lighten(self.b), + a: self.a, + } + } + + /// Darken this color by a factor. + /// + /// The alpha channel is not affected. + pub fn darken(self, factor: Ratio) -> Self { + let darken = |c: u8| c.saturating_sub(round_u8(c as f64 * factor.get())); + Self { + r: darken(self.r), + g: darken(self.g), + b: darken(self.b), + a: self.a, + } + } + + /// Negate this color. + /// + /// The alpha channel is not affected. + pub fn negate(self) -> Self { + Self { + r: u8::MAX - self.r, + g: u8::MAX - self.g, + b: u8::MAX - self.b, + a: self.a, + } + } +} + +impl FromStr for RgbaColor { + type Err = &'static str; + + /// Constructs a new color from hex strings like the following: + /// - `#aef` (shorthand, with leading hashtag), + /// - `7a03c2` (without alpha), + /// - `abcdefff` (with alpha). + /// + /// The hashtag is optional and both lower and upper case are fine. + fn from_str(hex_str: &str) -> Result<Self, Self::Err> { + let hex_str = hex_str.strip_prefix('#').unwrap_or(hex_str); + if hex_str.chars().any(|c| !c.is_ascii_hexdigit()) { + return Err("color string contains non-hexadecimal letters"); + } + + let len = hex_str.len(); + let long = len == 6 || len == 8; + let short = len == 3 || len == 4; + let alpha = len == 4 || len == 8; + if !long && !short { + return Err("color string has wrong length"); + } + + let mut values: [u8; 4] = [u8::MAX; 4]; + for elem in if alpha { 0..4 } else { 0..3 } { + let item_len = if long { 2 } else { 1 }; + let pos = elem * item_len; + + let item = &hex_str[pos..(pos + item_len)]; + values[elem] = u8::from_str_radix(item, 16).unwrap(); + + if short { + // Duplicate number for shorthand notation, i.e. `a` -> `aa` + values[elem] += values[elem] * 16; + } + } + + Ok(Self::new(values[0], values[1], values[2], values[3])) + } +} + +impl Debug for RgbaColor { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + if f.alternate() { + write!(f, "rgba({}, {}, {}, {})", self.r, self.g, self.b, self.a,)?; + } else { + write!(f, "rgb(\"#{:02x}{:02x}{:02x}", self.r, self.g, self.b)?; + if self.a != 255 { + write!(f, "{:02x}", self.a)?; + } + write!(f, "\")")?; + } + Ok(()) + } +} + +impl<T: Into<RgbaColor>> From<T> for Color { + fn from(rgba: T) -> Self { + Self::Rgba(rgba.into()) + } +} + +cast! { + RgbaColor, + self => Value::Color(self.into()), +} + +/// An 8-bit CMYK color. +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct CmykColor { + /// The cyan component. + pub c: u8, + /// The magenta component. + pub m: u8, + /// The yellow component. + pub y: u8, + /// The key (black) component. + pub k: u8, +} + +impl CmykColor { + /// Construct a new CMYK color. + pub const fn new(c: u8, m: u8, y: u8, k: u8) -> Self { + Self { c, m, y, k } + } + + /// Convert this color to RGBA. + pub fn to_rgba(self) -> RgbaColor { + let k = self.k as f64 / 255.0; + let f = |c| { + let c = c as f64 / 255.0; + round_u8(255.0 * (1.0 - c) * (1.0 - k)) + }; + + RgbaColor { r: f(self.c), g: f(self.m), b: f(self.y), a: 255 } + } + + /// Lighten this color by a factor. + pub fn lighten(self, factor: Ratio) -> Self { + let lighten = |c: u8| c.saturating_sub(round_u8(c as f64 * factor.get())); + Self { + c: lighten(self.c), + m: lighten(self.m), + y: lighten(self.y), + k: lighten(self.k), + } + } + + /// Darken this color by a factor. + pub fn darken(self, factor: Ratio) -> Self { + let darken = + |c: u8| c.saturating_add(round_u8((u8::MAX - c) as f64 * factor.get())); + Self { + c: darken(self.c), + m: darken(self.m), + y: darken(self.y), + k: darken(self.k), + } + } + + /// Negate this color. + /// + /// Does not affect the key component. + pub fn negate(self) -> Self { + Self { + c: u8::MAX - self.c, + m: u8::MAX - self.m, + y: u8::MAX - self.y, + k: self.k, + } + } +} + +impl Debug for CmykColor { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + let g = |c| 100.0 * (c as f64 / 255.0); + write!( + f, + "cmyk({:.1}%, {:.1}%, {:.1}%, {:.1}%)", + g(self.c), + g(self.m), + g(self.y), + g(self.k), + ) + } +} + +impl From<CmykColor> for Color { + fn from(cmyk: CmykColor) -> Self { + Self::Cmyk(cmyk) + } +} + +/// Convert to the closest u8. +fn round_u8(value: f64) -> u8 { + value.round() as u8 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_color_strings() { + #[track_caller] + fn test(hex: &str, r: u8, g: u8, b: u8, a: u8) { + assert_eq!(RgbaColor::from_str(hex), Ok(RgbaColor::new(r, g, b, a))); + } + + test("f61243ff", 0xf6, 0x12, 0x43, 0xff); + test("b3d8b3", 0xb3, 0xd8, 0xb3, 0xff); + test("fCd2a9AD", 0xfc, 0xd2, 0xa9, 0xad); + test("233", 0x22, 0x33, 0x33, 0xff); + test("111b", 0x11, 0x11, 0x11, 0xbb); + } + + #[test] + fn test_parse_invalid_colors() { + #[track_caller] + fn test(hex: &str, message: &str) { + assert_eq!(RgbaColor::from_str(hex), Err(message)); + } + + test("a5", "color string has wrong length"); + test("12345", "color string has wrong length"); + test("f075ff011", "color string has wrong length"); + test("hmmm", "color string contains non-hexadecimal letters"); + test("14B2AH", "color string contains non-hexadecimal letters"); + } +} diff --git a/src/geom/corners.rs b/src/geom/corners.rs index 20e9bed0..5ee1e063 100644 --- a/src/geom/corners.rs +++ b/src/geom/corners.rs @@ -1,3 +1,5 @@ +use crate::eval::{CastInfo, FromValue, IntoValue, Reflect}; + use super::*; /// A container with components for the four corners of a rectangle. @@ -108,15 +110,47 @@ pub enum Corner { BottomLeft, } -impl<T> Cast for Corners<Option<T>> +impl<T: Reflect> Reflect for Corners<Option<T>> { + fn describe() -> CastInfo { + T::describe() + Dict::describe() + } + + fn castable(value: &Value) -> bool { + Dict::castable(value) || T::castable(value) + } +} + +impl<T> IntoValue for Corners<T> where - T: Cast + Clone, + T: PartialEq + IntoValue, { - fn is(value: &Value) -> bool { - matches!(value, Value::Dict(_)) || T::is(value) + fn into_value(self) -> Value { + if self.is_uniform() { + return self.top_left.into_value(); + } + + let mut dict = Dict::new(); + let mut handle = |key: &str, component: T| { + let value = component.into_value(); + if value != Value::None { + dict.insert(key.into(), value); + } + }; + + handle("top-left", self.top_left); + handle("top-right", self.top_right); + handle("bottom-right", self.bottom_right); + handle("bottom-left", self.bottom_left); + + Value::Dict(dict) } +} - fn cast(mut value: Value) -> StrResult<Self> { +impl<T> FromValue for Corners<Option<T>> +where + T: FromValue + Clone, +{ + fn from_value(mut value: Value) -> StrResult<Self> { let keys = [ "top-left", "top-right", @@ -131,7 +165,7 @@ where if let Value::Dict(dict) = &mut value { if dict.iter().any(|(key, _)| keys.contains(&key.as_str())) { - let mut take = |key| dict.take(key).ok().map(T::cast).transpose(); + let mut take = |key| dict.take(key).ok().map(T::from_value).transpose(); let rest = take("rest")?; let left = take("left")?.or_else(|| rest.clone()); let top = take("top")?.or_else(|| rest.clone()); @@ -157,16 +191,12 @@ where } } - if T::is(&value) { - Ok(Self::splat(Some(T::cast(value)?))) + if T::castable(&value) { + Ok(Self::splat(Some(T::from_value(value)?))) } else { - <Self as Cast>::error(value) + Err(Self::error(&value)) } } - - fn describe() -> CastInfo { - T::describe() + CastInfo::Type("dictionary") - } } impl<T: Resolve> Resolve for Corners<T> { @@ -187,29 +217,3 @@ impl<T: Fold> Fold for Corners<Option<T>> { }) } } - -impl<T> From<Corners<T>> for Value -where - T: PartialEq + Into<Value>, -{ - fn from(corners: Corners<T>) -> Self { - if corners.is_uniform() { - return corners.top_left.into(); - } - - let mut dict = Dict::new(); - let mut handle = |key: &str, component: T| { - let value = component.into(); - if value != Value::None { - dict.insert(key.into(), value); - } - }; - - handle("top-left", corners.top_left); - handle("top-right", corners.top_right); - handle("bottom-right", corners.bottom_right); - handle("bottom-left", corners.bottom_left); - - Value::Dict(dict) - } -} diff --git a/src/geom/dir.rs b/src/geom/dir.rs index bc4d66e1..48915471 100644 --- a/src/geom/dir.rs +++ b/src/geom/dir.rs @@ -74,6 +74,6 @@ impl Debug for Dir { } } -cast_from_value! { - Dir: "direction", +cast! { + type Dir: "direction", } diff --git a/src/geom/em.rs b/src/geom/em.rs index 2c63c81d..8dda9ff6 100644 --- a/src/geom/em.rs +++ b/src/geom/em.rs @@ -135,8 +135,9 @@ impl Sum for Em { } } -cast_to_value! { - v: Em => Value::Length(v.into()) +cast! { + Em, + self => Value::Length(self.into()), } impl Resolve for Em { diff --git a/src/geom/length.rs b/src/geom/length.rs index 9d6552da..7d0a9841 100644 --- a/src/geom/length.rs +++ b/src/geom/length.rs @@ -1,6 +1,6 @@ use super::*; -/// A length, possibly expressed with contextual units. +/// A size or distance, possibly expressed with contextual units. /// /// Currently supports absolute and font-relative units, but support could quite /// easily be extended to other units. diff --git a/src/geom/mod.rs b/src/geom/mod.rs index 8896c24c..4a9ecfe1 100644 --- a/src/geom/mod.rs +++ b/src/geom/mod.rs @@ -6,6 +6,7 @@ mod abs; mod align; mod angle; mod axes; +mod color; mod corners; mod dir; mod ellipse; @@ -26,29 +27,32 @@ mod smart; mod stroke; mod transform; -pub use self::abs::*; -pub use self::align::*; -pub use self::angle::*; -pub use self::axes::*; -pub use self::corners::*; -pub use self::dir::*; -pub use self::ellipse::*; -pub use self::em::*; -pub use self::fr::*; -pub use self::length::*; -pub use self::paint::*; -pub use self::path::*; -pub use self::point::*; -pub use self::ratio::*; -pub use self::rel::*; -pub use self::rounded::*; -pub use self::scalar::*; -pub use self::shape::*; -pub use self::sides::*; -pub use self::size::*; -pub use self::smart::*; -pub use self::stroke::*; -pub use self::transform::*; +pub use self::abs::{Abs, AbsUnit}; +pub use self::align::{Align, GenAlign, HorizontalAlign, VerticalAlign}; +pub use self::angle::{Angle, AngleUnit}; +pub use self::axes::{Axes, Axis}; +pub use self::color::{CmykColor, Color, LumaColor, RgbaColor}; +pub use self::corners::{Corner, Corners}; +pub use self::dir::Dir; +pub use self::ellipse::ellipse; +pub use self::em::Em; +pub use self::fr::Fr; +pub use self::length::Length; +pub use self::paint::Paint; +pub use self::path::{Path, PathItem}; +pub use self::point::Point; +pub use self::ratio::Ratio; +pub use self::rel::Rel; +pub use self::rounded::rounded_rect; +pub use self::scalar::Scalar; +pub use self::shape::{Geometry, Shape}; +pub use self::sides::{Side, Sides}; +pub use self::size::Size; +pub use self::smart::Smart; +pub use self::stroke::{ + DashLength, DashPattern, LineCap, LineJoin, PartialStroke, Stroke, +}; +pub use self::transform::Transform; use std::cmp::Ordering; use std::f64::consts::PI; @@ -58,7 +62,7 @@ use std::iter::Sum; use std::ops::*; use crate::diag::StrResult; -use crate::eval::{array, cast_from_value, cast_to_value, Cast, CastInfo, Dict, Value}; +use crate::eval::{array, cast, Array, Dict, Value}; use crate::model::{Fold, Resolve, StyleChain}; /// Generic access to a structure's components. diff --git a/src/geom/paint.rs b/src/geom/paint.rs index e9bd3a2e..10fa9fde 100644 --- a/src/geom/paint.rs +++ b/src/geom/paint.rs @@ -1,5 +1,3 @@ -use std::str::FromStr; - use super::*; /// How a fill or stroke should be painted. @@ -23,393 +21,10 @@ impl Debug for Paint { } } -cast_from_value! { +cast! { Paint, + self => match self { + Self::Solid(color) => Value::Color(color), + }, color: Color => Self::Solid(color), } - -cast_to_value! { - Paint::Solid(color): Paint => Value::Color(color) -} - -/// A color in a dynamic format. -#[derive(Copy, Clone, Eq, PartialEq, Hash)] -pub enum Color { - /// An 8-bit luma color. - Luma(LumaColor), - /// An 8-bit RGBA color. - Rgba(RgbaColor), - /// An 8-bit CMYK color. - Cmyk(CmykColor), -} - -impl Color { - pub const BLACK: Self = Self::Rgba(RgbaColor::new(0x00, 0x00, 0x00, 0xFF)); - pub const GRAY: Self = Self::Rgba(RgbaColor::new(0xAA, 0xAA, 0xAA, 0xFF)); - pub const SILVER: Self = Self::Rgba(RgbaColor::new(0xDD, 0xDD, 0xDD, 0xFF)); - pub const WHITE: Self = Self::Rgba(RgbaColor::new(0xFF, 0xFF, 0xFF, 0xFF)); - pub const NAVY: Self = Self::Rgba(RgbaColor::new(0x00, 0x1f, 0x3f, 0xFF)); - pub const BLUE: Self = Self::Rgba(RgbaColor::new(0x00, 0x74, 0xD9, 0xFF)); - pub const AQUA: Self = Self::Rgba(RgbaColor::new(0x7F, 0xDB, 0xFF, 0xFF)); - pub const TEAL: Self = Self::Rgba(RgbaColor::new(0x39, 0xCC, 0xCC, 0xFF)); - pub const EASTERN: Self = Self::Rgba(RgbaColor::new(0x23, 0x9D, 0xAD, 0xFF)); - pub const PURPLE: Self = Self::Rgba(RgbaColor::new(0xB1, 0x0D, 0xC9, 0xFF)); - pub const FUCHSIA: Self = Self::Rgba(RgbaColor::new(0xF0, 0x12, 0xBE, 0xFF)); - pub const MAROON: Self = Self::Rgba(RgbaColor::new(0x85, 0x14, 0x4b, 0xFF)); - pub const RED: Self = Self::Rgba(RgbaColor::new(0xFF, 0x41, 0x36, 0xFF)); - pub const ORANGE: Self = Self::Rgba(RgbaColor::new(0xFF, 0x85, 0x1B, 0xFF)); - pub const YELLOW: Self = Self::Rgba(RgbaColor::new(0xFF, 0xDC, 0x00, 0xFF)); - pub const OLIVE: Self = Self::Rgba(RgbaColor::new(0x3D, 0x99, 0x70, 0xFF)); - pub const GREEN: Self = Self::Rgba(RgbaColor::new(0x2E, 0xCC, 0x40, 0xFF)); - pub const LIME: Self = Self::Rgba(RgbaColor::new(0x01, 0xFF, 0x70, 0xFF)); - - /// Convert this color to RGBA. - pub fn to_rgba(self) -> RgbaColor { - match self { - Self::Luma(luma) => luma.to_rgba(), - Self::Rgba(rgba) => rgba, - Self::Cmyk(cmyk) => cmyk.to_rgba(), - } - } - - /// Lighten this color by the given factor. - pub fn lighten(self, factor: Ratio) -> Self { - match self { - Self::Luma(luma) => Self::Luma(luma.lighten(factor)), - Self::Rgba(rgba) => Self::Rgba(rgba.lighten(factor)), - Self::Cmyk(cmyk) => Self::Cmyk(cmyk.lighten(factor)), - } - } - - /// Darken this color by the given factor. - pub fn darken(self, factor: Ratio) -> Self { - match self { - Self::Luma(luma) => Self::Luma(luma.darken(factor)), - Self::Rgba(rgba) => Self::Rgba(rgba.darken(factor)), - Self::Cmyk(cmyk) => Self::Cmyk(cmyk.darken(factor)), - } - } - - /// Negate this color. - pub fn negate(self) -> Self { - match self { - Self::Luma(luma) => Self::Luma(luma.negate()), - Self::Rgba(rgba) => Self::Rgba(rgba.negate()), - Self::Cmyk(cmyk) => Self::Cmyk(cmyk.negate()), - } - } -} - -impl Debug for Color { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - Self::Luma(c) => Debug::fmt(c, f), - Self::Rgba(c) => Debug::fmt(c, f), - Self::Cmyk(c) => Debug::fmt(c, f), - } - } -} - -/// An 8-bit grayscale color. -#[derive(Copy, Clone, Eq, PartialEq, Hash)] -pub struct LumaColor(pub u8); - -impl LumaColor { - /// Construct a new luma color. - pub const fn new(luma: u8) -> Self { - Self(luma) - } - - /// Convert to an opque RGBA color. - pub const fn to_rgba(self) -> RgbaColor { - RgbaColor::new(self.0, self.0, self.0, u8::MAX) - } - - /// Convert to CMYK as a fraction of true black. - pub fn to_cmyk(self) -> CmykColor { - CmykColor::new( - round_u8(self.0 as f64 * 0.75), - round_u8(self.0 as f64 * 0.68), - round_u8(self.0 as f64 * 0.67), - round_u8(self.0 as f64 * 0.90), - ) - } - - /// Lighten this color by a factor. - pub fn lighten(self, factor: Ratio) -> Self { - let inc = round_u8((u8::MAX - self.0) as f64 * factor.get()); - Self(self.0.saturating_add(inc)) - } - - /// Darken this color by a factor. - pub fn darken(self, factor: Ratio) -> Self { - let dec = round_u8(self.0 as f64 * factor.get()); - Self(self.0.saturating_sub(dec)) - } - - /// Negate this color. - pub fn negate(self) -> Self { - Self(u8::MAX - self.0) - } -} - -impl Debug for LumaColor { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "luma({})", self.0) - } -} - -impl From<LumaColor> for Color { - fn from(luma: LumaColor) -> Self { - Self::Luma(luma) - } -} - -/// An 8-bit RGBA color. -#[derive(Copy, Clone, Eq, PartialEq, Hash)] -pub struct RgbaColor { - /// Red channel. - pub r: u8, - /// Green channel. - pub g: u8, - /// Blue channel. - pub b: u8, - /// Alpha channel. - pub a: u8, -} - -impl RgbaColor { - /// Construct a new RGBA color. - pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self { - Self { r, g, b, a } - } - - /// Lighten this color by a factor. - /// - /// The alpha channel is not affected. - pub fn lighten(self, factor: Ratio) -> Self { - let lighten = - |c: u8| c.saturating_add(round_u8((u8::MAX - c) as f64 * factor.get())); - Self { - r: lighten(self.r), - g: lighten(self.g), - b: lighten(self.b), - a: self.a, - } - } - - /// Darken this color by a factor. - /// - /// The alpha channel is not affected. - pub fn darken(self, factor: Ratio) -> Self { - let darken = |c: u8| c.saturating_sub(round_u8(c as f64 * factor.get())); - Self { - r: darken(self.r), - g: darken(self.g), - b: darken(self.b), - a: self.a, - } - } - - /// Negate this color. - /// - /// The alpha channel is not affected. - pub fn negate(self) -> Self { - Self { - r: u8::MAX - self.r, - g: u8::MAX - self.g, - b: u8::MAX - self.b, - a: self.a, - } - } -} - -impl FromStr for RgbaColor { - type Err = &'static str; - - /// Constructs a new color from hex strings like the following: - /// - `#aef` (shorthand, with leading hashtag), - /// - `7a03c2` (without alpha), - /// - `abcdefff` (with alpha). - /// - /// The hashtag is optional and both lower and upper case are fine. - fn from_str(hex_str: &str) -> Result<Self, Self::Err> { - let hex_str = hex_str.strip_prefix('#').unwrap_or(hex_str); - if hex_str.chars().any(|c| !c.is_ascii_hexdigit()) { - return Err("color string contains non-hexadecimal letters"); - } - - let len = hex_str.len(); - let long = len == 6 || len == 8; - let short = len == 3 || len == 4; - let alpha = len == 4 || len == 8; - if !long && !short { - return Err("color string has wrong length"); - } - - let mut values: [u8; 4] = [u8::MAX; 4]; - for elem in if alpha { 0..4 } else { 0..3 } { - let item_len = if long { 2 } else { 1 }; - let pos = elem * item_len; - - let item = &hex_str[pos..(pos + item_len)]; - values[elem] = u8::from_str_radix(item, 16).unwrap(); - - if short { - // Duplicate number for shorthand notation, i.e. `a` -> `aa` - values[elem] += values[elem] * 16; - } - } - - Ok(Self::new(values[0], values[1], values[2], values[3])) - } -} - -impl Debug for RgbaColor { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - if f.alternate() { - write!(f, "rgba({}, {}, {}, {})", self.r, self.g, self.b, self.a,)?; - } else { - write!(f, "rgb(\"#{:02x}{:02x}{:02x}", self.r, self.g, self.b)?; - if self.a != 255 { - write!(f, "{:02x}", self.a)?; - } - write!(f, "\")")?; - } - Ok(()) - } -} - -impl<T: Into<RgbaColor>> From<T> for Color { - fn from(rgba: T) -> Self { - Self::Rgba(rgba.into()) - } -} - -cast_to_value! { - v: RgbaColor => Value::Color(v.into()) -} - -/// An 8-bit CMYK color. -#[derive(Copy, Clone, Eq, PartialEq, Hash)] -pub struct CmykColor { - /// The cyan component. - pub c: u8, - /// The magenta component. - pub m: u8, - /// The yellow component. - pub y: u8, - /// The key (black) component. - pub k: u8, -} - -impl CmykColor { - /// Construct a new CMYK color. - pub const fn new(c: u8, m: u8, y: u8, k: u8) -> Self { - Self { c, m, y, k } - } - - /// Convert this color to RGBA. - pub fn to_rgba(self) -> RgbaColor { - let k = self.k as f64 / 255.0; - let f = |c| { - let c = c as f64 / 255.0; - round_u8(255.0 * (1.0 - c) * (1.0 - k)) - }; - - RgbaColor { r: f(self.c), g: f(self.m), b: f(self.y), a: 255 } - } - - /// Lighten this color by a factor. - pub fn lighten(self, factor: Ratio) -> Self { - let lighten = |c: u8| c.saturating_sub(round_u8(c as f64 * factor.get())); - Self { - c: lighten(self.c), - m: lighten(self.m), - y: lighten(self.y), - k: lighten(self.k), - } - } - - /// Darken this color by a factor. - pub fn darken(self, factor: Ratio) -> Self { - let darken = - |c: u8| c.saturating_add(round_u8((u8::MAX - c) as f64 * factor.get())); - Self { - c: darken(self.c), - m: darken(self.m), - y: darken(self.y), - k: darken(self.k), - } - } - - /// Negate this color. - /// - /// Does not affect the key component. - pub fn negate(self) -> Self { - Self { - c: u8::MAX - self.c, - m: u8::MAX - self.m, - y: u8::MAX - self.y, - k: self.k, - } - } -} - -impl Debug for CmykColor { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - let g = |c| 100.0 * (c as f64 / 255.0); - write!( - f, - "cmyk({:.1}%, {:.1}%, {:.1}%, {:.1}%)", - g(self.c), - g(self.m), - g(self.y), - g(self.k), - ) - } -} - -impl From<CmykColor> for Color { - fn from(cmyk: CmykColor) -> Self { - Self::Cmyk(cmyk) - } -} - -/// Convert to the closest u8. -fn round_u8(value: f64) -> u8 { - value.round() as u8 -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_color_strings() { - #[track_caller] - fn test(hex: &str, r: u8, g: u8, b: u8, a: u8) { - assert_eq!(RgbaColor::from_str(hex), Ok(RgbaColor::new(r, g, b, a))); - } - - test("f61243ff", 0xf6, 0x12, 0x43, 0xff); - test("b3d8b3", 0xb3, 0xd8, 0xb3, 0xff); - test("fCd2a9AD", 0xfc, 0xd2, 0xa9, 0xad); - test("233", 0x22, 0x33, 0x33, 0xff); - test("111b", 0x11, 0x11, 0x11, 0xbb); - } - - #[test] - fn test_parse_invalid_colors() { - #[track_caller] - fn test(hex: &str, message: &str) { - assert_eq!(RgbaColor::from_str(hex), Err(message)); - } - - test("a5", "color string has wrong length"); - test("12345", "color string has wrong length"); - test("f075ff011", "color string has wrong length"); - test("hmmm", "color string contains non-hexadecimal letters"); - test("14B2AH", "color string contains non-hexadecimal letters"); - } -} diff --git a/src/geom/rel.rs b/src/geom/rel.rs index cf1e73ef..88972222 100644 --- a/src/geom/rel.rs +++ b/src/geom/rel.rs @@ -240,6 +240,7 @@ impl Fold for Rel<Length> { } } -cast_to_value! { - v: Rel<Abs> => v.map(Length::from).into() +cast! { + Rel<Abs>, + self => self.map(Length::from).into_value(), } diff --git a/src/geom/sides.rs b/src/geom/sides.rs index a905a5f8..d4b72a9d 100644 --- a/src/geom/sides.rs +++ b/src/geom/sides.rs @@ -1,3 +1,5 @@ +use crate::eval::{CastInfo, FromValue, IntoValue, Reflect}; + use super::*; /// A container with left, top, right and bottom components. @@ -178,19 +180,51 @@ impl Side { } } -impl<T> Cast for Sides<Option<T>> +impl<T: Reflect> Reflect for Sides<Option<T>> { + fn describe() -> CastInfo { + T::describe() + Dict::describe() + } + + fn castable(value: &Value) -> bool { + Dict::castable(value) || T::castable(value) + } +} + +impl<T> IntoValue for Sides<T> where - T: Default + Cast + Clone, + T: PartialEq + IntoValue, { - fn is(value: &Value) -> bool { - matches!(value, Value::Dict(_)) || T::is(value) + fn into_value(self) -> Value { + if self.is_uniform() { + return self.left.into_value(); + } + + let mut dict = Dict::new(); + let mut handle = |key: &str, component: T| { + let value = component.into_value(); + if value != Value::None { + dict.insert(key.into(), value); + } + }; + + handle("left", self.left); + handle("top", self.top); + handle("right", self.right); + handle("bottom", self.bottom); + + Value::Dict(dict) } +} - fn cast(mut value: Value) -> StrResult<Self> { +impl<T> FromValue for Sides<Option<T>> +where + T: Default + FromValue + Clone, +{ + fn from_value(mut value: Value) -> StrResult<Self> { let keys = ["left", "top", "right", "bottom", "x", "y", "rest"]; if let Value::Dict(dict) = &mut value { if dict.iter().any(|(key, _)| keys.contains(&key.as_str())) { - let mut take = |key| dict.take(key).ok().map(T::cast).transpose(); + let mut take = |key| dict.take(key).ok().map(T::from_value).transpose(); let rest = take("rest")?; let x = take("x")?.or_else(|| rest.clone()); let y = take("y")?.or_else(|| rest.clone()); @@ -206,43 +240,14 @@ where } } - if T::is(&value) { - Ok(Self::splat(Some(T::cast(value)?))) + if T::castable(&value) { + Ok(Self::splat(Some(T::from_value(value)?))) } else { - <Self as Cast>::error(value) + Err(Self::error(&value)) } } - - fn describe() -> CastInfo { - T::describe() + CastInfo::Type("dictionary") - } } -impl<T> From<Sides<T>> for Value -where - T: PartialEq + Into<Value>, -{ - fn from(sides: Sides<T>) -> Self { - if sides.is_uniform() { - return sides.left.into(); - } - - let mut dict = Dict::new(); - let mut handle = |key: &str, component: T| { - let value = component.into(); - if value != Value::None { - dict.insert(key.into(), value); - } - }; - - handle("left", sides.left); - handle("top", sides.top); - handle("right", sides.right); - handle("bottom", sides.bottom); - - Value::Dict(dict) - } -} impl<T: Resolve> Resolve for Sides<T> { type Output = Sides<T::Output>; diff --git a/src/geom/smart.rs b/src/geom/smart.rs index d9f8fd16..a6271c20 100644 --- a/src/geom/smart.rs +++ b/src/geom/smart.rs @@ -1,3 +1,5 @@ +use crate::eval::{AutoValue, CastInfo, FromValue, IntoValue, Reflect}; + use super::*; /// A value that can be automatically determined. @@ -94,21 +96,32 @@ impl<T> Default for Smart<T> { } } -impl<T: Cast> Cast for Smart<T> { - fn is(value: &Value) -> bool { - matches!(value, Value::Auto) || T::is(value) +impl<T: Reflect> Reflect for Smart<T> { + fn castable(value: &Value) -> bool { + AutoValue::castable(value) || T::castable(value) } - fn cast(value: Value) -> StrResult<Self> { - match value { - Value::Auto => Ok(Self::Auto), - v if T::is(&v) => Ok(Self::Custom(T::cast(v)?)), - _ => <Self as Cast>::error(value), + fn describe() -> CastInfo { + T::describe() + AutoValue::describe() + } +} + +impl<T: IntoValue> IntoValue for Smart<T> { + fn into_value(self) -> Value { + match self { + Smart::Custom(v) => v.into_value(), + Smart::Auto => Value::Auto, } } +} - fn describe() -> CastInfo { - T::describe() + CastInfo::Type("auto") +impl<T: FromValue> FromValue for Smart<T> { + fn from_value(value: Value) -> StrResult<Self> { + match value { + Value::Auto => Ok(Self::Auto), + v if T::castable(&v) => Ok(Self::Custom(T::from_value(v)?)), + _ => Err(Self::error(&value)), + } } } @@ -131,12 +144,3 @@ where self.map(|inner| inner.fold(outer.unwrap_or_default())) } } - -impl<T: Into<Value>> From<Smart<T>> for Value { - fn from(v: Smart<T>) -> Self { - match v { - Smart::Custom(v) => v.into(), - Smart::Auto => Value::Auto, - } - } -} diff --git a/src/geom/stroke.rs b/src/geom/stroke.rs index 6539922c..66264d5d 100644 --- a/src/geom/stroke.rs +++ b/src/geom/stroke.rs @@ -1,3 +1,5 @@ +use crate::eval::{Cast, FromValue}; + use super::*; /// A stroke of a geometric shape. @@ -169,8 +171,78 @@ impl<T: Debug> Debug for PartialStroke<T> { } } +impl Resolve for PartialStroke { + type Output = PartialStroke<Abs>; + + fn resolve(self, styles: StyleChain) -> Self::Output { + 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, + } + } +} + +impl Fold for PartialStroke<Abs> { + type Output = Self; + + fn fold(self, outer: Self::Output) -> Self::Output { + 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.or(outer.miter_limit), + } + } +} + +cast! { + type PartialStroke: "stroke", + thickness: Length => Self { + thickness: Smart::Custom(thickness), + ..Default::default() + }, + color: Color => Self { + paint: Smart::Custom(color.into()), + ..Default::default() + }, + mut dict: Dict => { + fn take<T: FromValue>(dict: &mut Dict, key: &str) -> StrResult<Smart<T>> { + Ok(dict.take(key).ok().map(T::from_value) + .transpose()?.map(Smart::Custom).unwrap_or(Smart::Auto)) + } + + let paint = take::<Paint>(&mut dict, "paint")?; + 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(&["paint", "thickness", "cap", "join", "dash", "miter-limit"])?; + + Self { + paint, + thickness, + line_cap, + line_join, + dash_pattern, + miter_limit: miter_limit.map(Scalar), + } + }, +} + +cast! { + PartialStroke<Abs>, + self => self.map(Length::from).into_value(), +} + /// The line cap of a stroke -#[derive(Cast, Clone, Eq, PartialEq, Hash)] +#[derive(Copy, Clone, Eq, PartialEq, Hash, Cast)] pub enum LineCap { Butt, Round, @@ -188,7 +260,7 @@ impl Debug for LineCap { } /// The line join of a stroke -#[derive(Cast, Clone, Eq, PartialEq, Hash)] +#[derive(Copy, Clone, Eq, PartialEq, Hash, Cast)] pub enum LineJoin { Miter, Round, @@ -235,49 +307,22 @@ impl<T: Default> From<Vec<DashLength<T>>> for DashPattern<T> { } } -/// 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>; +impl Resolve for DashPattern { + type Output = DashPattern<Abs>; fn resolve(self, styles: StyleChain) -> Self::Output { - match self { - Self::LineWidth => DashLength::LineWidth, - Self::Length(l) => DashLength::Length(l.resolve(styles)), + DashPattern { + array: self.array.into_iter().map(|l| l.resolve(styles)).collect(), + phase: self.phase.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 +// Same names as tikz: +// https://tex.stackexchange.com/questions/45275/tikz-get-values-for-predefined-dash-patterns +cast! { + DashPattern, + "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(), @@ -288,19 +333,13 @@ cast_from_value! { "dash-dotted" => vec![Abs::pt(3.0).into(), Abs::pt(2.0).into(), DashLength::LineWidth, Abs::pt(2.0).into()].into(), "densely-dash-dotted" => vec![Abs::pt(3.0).into(), Abs::pt(1.0).into(), DashLength::LineWidth, Abs::pt(1.0).into()].into(), "loosely-dash-dotted" => 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(), - } - }, + + 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) + let phase = dict.take("phase").ok().map(Value::cast) .transpose()?.unwrap_or(Length::zero()); - dict.finish(&["array", "phase"])?; - Self { array, phase, @@ -308,82 +347,41 @@ cast_from_value! { }, } -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), - } - } +/// The length of a dash in a line dash pattern +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum DashLength<T = Length> { + LineWidth, + Length(T), } -cast_from_value! { - PartialStroke: "stroke", - thickness: Length => Self { - thickness: Smart::Custom(thickness), - ..Default::default() - }, - color: Color => Self { - paint: Smart::Custom(color.into()), - ..Default::default() - }, - 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, "paint")?; - 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(&["paint", "thickness", "cap", "join", "dash", "miter-limit"])?; - - Self { - paint, - thickness, - line_cap, - line_join, - dash_pattern, - miter_limit: miter_limit.map(Scalar), - } - }, +impl From<Abs> for DashLength { + fn from(l: Abs) -> Self { + DashLength::Length(l.into()) + } } -impl Resolve for PartialStroke { - type Output = PartialStroke<Abs>; - - fn resolve(self, styles: StyleChain) -> Self::Output { - 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, +impl<T> DashLength<T> { + fn finish(self, line_width: T) -> T { + match self { + Self::LineWidth => line_width, + Self::Length(l) => l, } } } -impl Fold for PartialStroke<Abs> { - type Output = Self; +impl Resolve for DashLength { + type Output = DashLength<Abs>; - fn fold(self, outer: Self::Output) -> Self::Output { - 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.or(outer.miter_limit), + fn resolve(self, styles: StyleChain) -> Self::Output { + match self { + Self::LineWidth => DashLength::LineWidth, + Self::Length(v) => DashLength::Length(v.resolve(styles)), } } } -cast_to_value! { - v: PartialStroke<Abs> => v.map(Length::from).into() +cast! { + DashLength, + "dot" => Self::LineWidth, + v: Length => Self::Length(v), } |
