summaryrefslogtreecommitdiff
path: root/src/geom
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2023-06-06 21:13:59 +0200
committerLaurenz <laurmaedje@gmail.com>2023-06-06 22:06:16 +0200
commitfd417da04f7ca4b995de7f6510abafd3e9c31307 (patch)
tree3675529c75ca7363701ac8ea306de2cc1d3cbcb3 /src/geom
parent168bdf35bd773e67343c965cb473492cc5cae9e7 (diff)
Improve value casting infrastructure
Diffstat (limited to 'src/geom')
-rw-r--r--src/geom/abs.rs5
-rw-r--r--src/geom/align.rs67
-rw-r--r--src/geom/axes.rs8
-rw-r--r--src/geom/color.rs386
-rw-r--r--src/geom/corners.rs82
-rw-r--r--src/geom/dir.rs4
-rw-r--r--src/geom/em.rs5
-rw-r--r--src/geom/length.rs2
-rw-r--r--src/geom/mod.rs52
-rw-r--r--src/geom/paint.rs393
-rw-r--r--src/geom/rel.rs5
-rw-r--r--src/geom/sides.rs81
-rw-r--r--src/geom/smart.rs42
-rw-r--r--src/geom/stroke.rs224
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),
}