diff --git a/src/eval/dict.rs b/src/eval/dict.rs index 8893ce483..654c90eb5 100644 --- a/src/eval/dict.rs +++ b/src/eval/dict.rs @@ -76,6 +76,11 @@ impl Dict { } } + /// Remove the value if the dictionary contains the given key. + pub fn take(&mut self, key: &str) -> Option { + Arc::make_mut(&mut self.0).remove(key) + } + /// Clear the dictionary. pub fn clear(&mut self) { if Arc::strong_count(&self.0) == 1 { diff --git a/src/eval/value.rs b/src/eval/value.rs index a7da99c92..294aac946 100644 --- a/src/eval/value.rs +++ b/src/eval/value.rs @@ -8,7 +8,8 @@ use std::sync::Arc; use super::{ops, Args, Array, Dict, Func, RawLength, Regex}; use crate::diag::{with_alternative, StrResult}; use crate::geom::{ - Angle, Color, Dir, Em, Fraction, Length, Paint, Ratio, Relative, RgbaColor, Sides, + Angle, Color, Corners, Dir, Em, Fraction, Length, Paint, Ratio, Relative, RgbaColor, + Sides, }; use crate::library::text::RawNode; use crate::model::{Content, Group, Layout, LayoutNode, Pattern}; @@ -516,6 +517,71 @@ impl Cast> for Spanned { } } +dynamic! { + Dir: "direction", +} + +dynamic! { + Regex: "regular expression", +} + +dynamic! { + Group: "group", +} + +castable! { + usize, + Expected: "non-negative integer", + Value::Int(int) => int.try_into().map_err(|_| { + if int < 0 { + "must be at least zero" + } else { + "number too large" + } + })?, +} + +castable! { + NonZeroUsize, + Expected: "positive integer", + Value::Int(int) => int + .try_into() + .and_then(|int: usize| int.try_into()) + .map_err(|_| if int <= 0 { + "must be positive" + } else { + "number too large" + })?, +} + +castable! { + Paint, + Expected: "color", + Value::Color(color) => Paint::Solid(color), +} + +castable! { + String, + Expected: "string", + Value::Str(string) => string.into(), +} + +castable! { + LayoutNode, + Expected: "content", + Value::None => Self::default(), + Value::Str(text) => Content::Text(text).pack(), + Value::Content(content) => content.pack(), +} + +castable! { + Pattern, + Expected: "function, string or regular expression", + Value::Func(func) => Self::Node(func.node()?), + Value::Str(text) => Self::text(&text), + @regex: Regex => Self::Regex(regex.clone()), +} + impl Cast for Option { fn is(value: &Value) -> bool { matches!(value, Value::None) || T::is(value) @@ -609,112 +675,84 @@ impl Cast for Smart { impl Cast for Sides where - T: Cast + Default + Clone, + T: Cast + Default + Copy, { fn is(value: &Value) -> bool { matches!(value, Value::Dict(_)) || T::is(value) } - fn cast(value: Value) -> StrResult { - match value { - Value::Dict(dict) => { - for (key, _) in &dict { - if !matches!( - key.as_str(), - "left" | "top" | "right" | "bottom" | "x" | "y" | "rest" - ) { - return Err(format!("unexpected key {key:?}")); - } - } + fn cast(mut value: Value) -> StrResult { + if let Value::Dict(dict) = &mut value { + let mut take = |key| dict.take(key).map(T::cast).transpose(); - let sides = Sides { - left: dict.get("left").or(dict.get("x")), - top: dict.get("top").or(dict.get("y")), - right: dict.get("right").or(dict.get("x")), - bottom: dict.get("bottom").or(dict.get("y")), - }; + let rest = take("rest")?; + let x = take("x")?.or(rest); + let y = take("y")?.or(rest); + let sides = Sides { + left: take("left")?.or(x), + top: take("top")?.or(y), + right: take("right")?.or(x), + bottom: take("bottom")?.or(y), + }; - Ok(sides.map(|side| { - side.or(dict.get("rest")) - .cloned() - .and_then(T::cast) - .unwrap_or_default() - })) + if let Some((key, _)) = dict.iter().next() { + return Err(format!("unexpected key {key:?}")); } - v => T::cast(v).map(Sides::splat).map_err(|msg| { + + Ok(sides.map(Option::unwrap_or_default)) + } else { + T::cast(value).map(Self::splat).map_err(|msg| { with_alternative( msg, - "dictionary with any of `left`, `top`, `right`, `bottom`, \ + "dictionary with any of \ + `left`, `top`, `right`, `bottom`, \ `x`, `y`, or `rest` as keys", ) - }), + }) } } } -dynamic! { - Dir: "direction", -} +impl Cast for Corners +where + T: Cast + Default + Copy, +{ + fn is(value: &Value) -> bool { + matches!(value, Value::Dict(_)) || T::is(value) + } -dynamic! { - Regex: "regular expression", -} + fn cast(mut value: Value) -> StrResult { + if let Value::Dict(dict) = &mut value { + let mut take = |key| dict.take(key).map(T::cast).transpose(); -dynamic! { - Group: "group", -} + let rest = take("rest")?; + let left = take("left")?.or(rest); + let top = take("top")?.or(rest); + let right = take("right")?.or(rest); + let bottom = take("bottom")?.or(rest); + let corners = Corners { + top_left: take("top-left")?.or(top).or(left), + top_right: take("top-right")?.or(top).or(right), + bottom_right: take("bottom-right")?.or(bottom).or(right), + bottom_left: take("bottom-left")?.or(bottom).or(left), + }; -castable! { - usize, - Expected: "non-negative integer", - Value::Int(int) => int.try_into().map_err(|_| { - if int < 0 { - "must be at least zero" + if let Some((key, _)) = dict.iter().next() { + return Err(format!("unexpected key {key:?}")); + } + + Ok(corners.map(Option::unwrap_or_default)) } else { - "number too large" + T::cast(value).map(Self::splat).map_err(|msg| { + with_alternative( + msg, + "dictionary with any of \ + `top-left`, `top-right`, `bottom-right`, `bottom-left`, \ + `left`, `top`, `right`, `bottom`, or `rest` as keys", + ) + }) } - })?, -} - -castable! { - NonZeroUsize, - Expected: "positive integer", - Value::Int(int) => int - .try_into() - .and_then(|int: usize| int.try_into()) - .map_err(|_| if int <= 0 { - "must be positive" - } else { - "number too large" - })?, -} - -castable! { - Paint, - Expected: "color", - Value::Color(color) => Paint::Solid(color), -} - -castable! { - String, - Expected: "string", - Value::Str(string) => string.into(), -} - -castable! { - LayoutNode, - Expected: "content", - Value::None => Self::default(), - Value::Str(text) => Content::Text(text).pack(), - Value::Content(content) => content.pack(), -} - -castable! { - Pattern, - Expected: "function, string or regular expression", - Value::Func(func) => Self::Node(func.node()?), - Value::Str(text) => Self::text(&text), - @regex: Regex => Self::Regex(regex.clone()), + } } #[cfg(test)] diff --git a/src/geom/corners.rs b/src/geom/corners.rs new file mode 100644 index 000000000..54fcd12f3 --- /dev/null +++ b/src/geom/corners.rs @@ -0,0 +1,122 @@ +use super::*; + +/// A container with components for the four corners of a rectangle. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Corners { + /// The value for the top left corner. + pub top_left: T, + /// The value for the top right corner. + pub top_right: T, + /// The value for the bottom right corner. + pub bottom_right: T, + /// The value for the bottom left corner. + pub bottom_left: T, +} + +impl Corners { + /// Create a new instance from the four components. + pub const fn new(top_left: T, top_right: T, bottom_right: T, bottom_left: T) -> Self { + Self { + top_left, + top_right, + bottom_right, + bottom_left, + } + } + + /// Create an instance with four equal components. + pub fn splat(value: T) -> Self + where + T: Clone, + { + Self { + top_left: value.clone(), + top_right: value.clone(), + bottom_right: value.clone(), + bottom_left: value, + } + } + + /// Map the individual fields with `f`. + pub fn map(self, mut f: F) -> Corners + where + F: FnMut(T) -> U, + { + Corners { + top_left: f(self.top_left), + top_right: f(self.top_right), + bottom_right: f(self.bottom_right), + bottom_left: f(self.bottom_left), + } + } + + /// Zip two instances into an instance. + pub fn zip(self, other: Corners, mut f: F) -> Corners + where + F: FnMut(T, V) -> W, + { + Corners { + top_left: f(self.top_left, other.top_left), + top_right: f(self.top_right, other.top_right), + bottom_right: f(self.bottom_right, other.bottom_right), + bottom_left: f(self.bottom_left, other.bottom_left), + } + } + + /// An iterator over the corners, starting with the top left corner, + /// clockwise. + pub fn iter(&self) -> impl Iterator { + [ + &self.top_left, + &self.top_right, + &self.bottom_right, + &self.bottom_left, + ] + .into_iter() + } + + /// Whether all sides are equal. + pub fn is_uniform(&self) -> bool + where + T: PartialEq, + { + self.top_left == self.top_right + && self.top_right == self.bottom_right + && self.bottom_right == self.bottom_left + } +} + +impl Get for Corners { + type Component = T; + + fn get(self, corner: Corner) -> T { + match corner { + Corner::TopLeft => self.top_left, + Corner::TopRight => self.top_right, + Corner::BottomRight => self.bottom_right, + Corner::BottomLeft => self.bottom_left, + } + } + + fn get_mut(&mut self, corner: Corner) -> &mut T { + match corner { + Corner::TopLeft => &mut self.top_left, + Corner::TopRight => &mut self.top_right, + Corner::BottomRight => &mut self.bottom_right, + Corner::BottomLeft => &mut self.bottom_left, + } + } +} + +/// The four corners of a rectangle. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum Corner { + /// The top left corner. + TopLeft, + /// The top right corner. + TopRight, + /// The bottom right corner. + BottomRight, + /// The bottom left corner. + BottomLeft, +} diff --git a/src/geom/mod.rs b/src/geom/mod.rs index bdd08fe5a..fc8ccb6c0 100644 --- a/src/geom/mod.rs +++ b/src/geom/mod.rs @@ -4,6 +4,7 @@ mod macros; mod align; mod angle; +mod corners; mod dir; mod em; mod fraction; @@ -22,6 +23,7 @@ mod transform; pub use align::*; pub use angle::*; +pub use corners::*; pub use dir::*; pub use em::*; pub use fraction::*; diff --git a/src/geom/rect.rs b/src/geom/rect.rs index dceb35777..dfea2c452 100644 --- a/src/geom/rect.rs +++ b/src/geom/rect.rs @@ -7,13 +7,13 @@ use std::mem; pub struct RoundedRect { /// The size of the rectangle. pub size: Size, - /// The radius at each side. - pub radius: Sides, + /// The radius at each corner. + pub radius: Corners, } impl RoundedRect { /// Create a new rounded rectangle. - pub fn new(size: Size, radius: Sides) -> Self { + pub fn new(size: Size, radius: Corners) -> Self { Self { size, radius } } @@ -73,20 +73,20 @@ impl RoundedRect { let mut always_continuous = true; for side in [Side::Top, Side::Right, Side::Bottom, Side::Left] { - let is_continuous = strokes.get(side) == strokes.get(side.next_cw()); - connection = connection.advance(is_continuous && side != Side::Left); - always_continuous &= is_continuous; + let continuous = strokes.get(side) == strokes.get(side.next_cw()); + connection = connection.advance(continuous && side != Side::Left); + always_continuous &= continuous; draw_side( &mut path, side, self.size, - self.radius.get(side.next_ccw()), - self.radius.get(side), + self.radius.get(side.start_corner()), + self.radius.get(side.end_corner()), connection, ); - if !is_continuous { + if !continuous { res.push((mem::take(&mut path), strokes.get(side))); } } @@ -109,8 +109,8 @@ fn draw_side( path: &mut Path, side: Side, size: Size, - radius_left: Length, - radius_right: Length, + start_radius: Length, + end_radius: Length, connection: Connection, ) { let angle_left = Angle::deg(if connection.prev { 90.0 } else { 45.0 }); @@ -118,23 +118,23 @@ fn draw_side( let length = size.get(side.axis()); // The arcs for a border of the rectangle along the x-axis, starting at (0,0). - let p1 = Point::with_x(radius_left); + let p1 = Point::with_x(start_radius); let mut arc1 = bezier_arc( p1 + Point::new( - -angle_left.sin() * radius_left, - (1.0 - angle_left.cos()) * radius_left, + -angle_left.sin() * start_radius, + (1.0 - angle_left.cos()) * start_radius, ), - Point::new(radius_left, radius_left), + Point::new(start_radius, start_radius), p1, ); - let p2 = Point::with_x(length - radius_right); + let p2 = Point::with_x(length - end_radius); let mut arc2 = bezier_arc( p2, - Point::new(length - radius_right, radius_right), + Point::new(length - end_radius, end_radius), p2 + Point::new( - angle_right.sin() * radius_right, - (1.0 - angle_right.cos()) * radius_right, + angle_right.sin() * end_radius, + (1.0 - angle_right.cos()) * end_radius, ), ); @@ -152,16 +152,16 @@ fn draw_side( arc2 = arc2.map(|x| x.transform(transform)); if !connection.prev { - path.move_to(if radius_left.is_zero() { arc1[3] } else { arc1[0] }); + path.move_to(if start_radius.is_zero() { arc1[3] } else { arc1[0] }); } - if !radius_left.is_zero() { + if !start_radius.is_zero() { path.cubic_to(arc1[1], arc1[2], arc1[3]); } path.line_to(arc2[0]); - if !connection.next && !radius_right.is_zero() { + if !connection.next && !end_radius.is_zero() { path.cubic_to(arc2[1], arc2[2], arc2[3]); } } diff --git a/src/geom/sides.rs b/src/geom/sides.rs index 938539fec..727489162 100644 --- a/src/geom/sides.rs +++ b/src/geom/sides.rs @@ -48,17 +48,17 @@ impl Sides { /// Zip two instances into an instance. pub fn zip(self, other: Sides, mut f: F) -> Sides where - F: FnMut(T, V, Side) -> W, + F: FnMut(T, V) -> W, { Sides { - left: f(self.left, other.left, Side::Left), - top: f(self.top, other.top, Side::Top), - right: f(self.right, other.right, Side::Right), - bottom: f(self.bottom, other.bottom, Side::Bottom), + left: f(self.left, other.left), + top: f(self.top, other.top), + right: f(self.right, other.right), + bottom: f(self.bottom, other.bottom), } } - /// An iterator over the sides. + /// An iterator over the sides, starting with the left side, clockwise. pub fn iter(&self) -> impl Iterator { [&self.left, &self.top, &self.right, &self.bottom].into_iter() } @@ -157,6 +157,21 @@ impl Side { } } + /// The first corner of the side in clockwise order. + pub fn start_corner(self) -> Corner { + match self { + Self::Left => Corner::BottomLeft, + Self::Top => Corner::TopLeft, + Self::Right => Corner::TopRight, + Self::Bottom => Corner::BottomRight, + } + } + + /// The second corner of the side in clockwise order. + pub fn end_corner(self) -> Corner { + self.next_cw().start_corner() + } + /// Return the corresponding axis. pub fn axis(self) -> SpecAxis { match self { diff --git a/src/library/graphics/shape.rs b/src/library/graphics/shape.rs index eed3c9d9e..5cc5a76d2 100644 --- a/src/library/graphics/shape.rs +++ b/src/library/graphics/shape.rs @@ -33,9 +33,11 @@ impl ShapeNode { /// How much to extend the shape's dimensions beyond the allocated space. #[property(resolve, fold)] pub const OUTSET: Sides>> = Sides::splat(Relative::zero()); + /// How much to round the shape's corners. #[property(skip, resolve, fold)] - pub const RADIUS: Sides>> = Sides::splat(Relative::zero()); + pub const RADIUS: Corners>> = + Corners::splat(Relative::zero()); fn construct(_: &mut Machine, args: &mut Args) -> TypResult { let size = match S { diff --git a/src/model/property.rs b/src/model/property.rs index 0e171939d..8681da7d5 100644 --- a/src/model/property.rs +++ b/src/model/property.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use super::{Interruption, NodeId, StyleChain}; use crate::eval::{RawLength, Smart}; -use crate::geom::{Length, Numeric, Relative, Sides, Spec}; +use crate::geom::{Corners, Length, Numeric, Relative, Sides, Spec}; use crate::library::layout::PageNode; use crate::library::structure::{EnumNode, ListNode}; use crate::library::text::ParNode; @@ -191,12 +191,15 @@ impl Resolve for Sides { type Output = Sides; fn resolve(self, styles: StyleChain) -> Self::Output { - Sides { - left: self.left.resolve(styles), - right: self.right.resolve(styles), - top: self.top.resolve(styles), - bottom: self.bottom.resolve(styles), - } + self.map(|v| v.resolve(styles)) + } +} + +impl Resolve for Corners { + type Output = Corners; + + fn resolve(self, styles: StyleChain) -> Self::Output { + self.map(|v| v.resolve(styles)) } } @@ -252,7 +255,7 @@ where type Output = Sides; fn fold(self, outer: Self::Output) -> Self::Output { - self.zip(outer, |inner, outer, _| inner.fold(outer)) + self.zip(outer, |inner, outer| inner.fold(outer)) } } @@ -260,7 +263,7 @@ impl Fold for Sides>> { type Output = Sides>; fn fold(self, outer: Self::Output) -> Self::Output { - self.zip(outer, |inner, outer, _| inner.unwrap_or(outer)) + self.zip(outer, |inner, outer| inner.unwrap_or(outer)) } } @@ -268,7 +271,26 @@ impl Fold for Sides>>> { type Output = Sides>>; fn fold(self, outer: Self::Output) -> Self::Output { - self.zip(outer, |inner, outer, _| inner.unwrap_or(outer)) + self.zip(outer, |inner, outer| inner.unwrap_or(outer)) + } +} + +impl Fold for Corners +where + T: Fold, +{ + type Output = Corners; + + fn fold(self, outer: Self::Output) -> Self::Output { + self.zip(outer, |inner, outer| inner.fold(outer)) + } +} + +impl Fold for Corners>> { + type Output = Corners>; + + fn fold(self, outer: Self::Output) -> Self::Output { + self.zip(outer, |inner, outer| inner.unwrap_or(outer)) } } diff --git a/tests/ref/graphics/shape-rect.png b/tests/ref/graphics/shape-rect.png index 2c2c7d7c1..e2ea05029 100644 Binary files a/tests/ref/graphics/shape-rect.png and b/tests/ref/graphics/shape-rect.png differ diff --git a/tests/typ/graphics/shape-rect.typ b/tests/typ/graphics/shape-rect.typ index 5495da8c8..7d1101808 100644 --- a/tests/typ/graphics/shape-rect.typ +++ b/tests/typ/graphics/shape-rect.typ @@ -30,8 +30,13 @@ // Rounded corners. #rect(width: 2cm, radius: 60%) -#rect(width: 1cm, radius: (x: 5pt, y: 10pt)) -#rect(width: 1.25cm, radius: (left: 2pt, top: 5pt, right: 8pt, bottom: 11pt)) +#rect(width: 1cm, radius: (left: 10pt, right: 5pt)) +#rect(width: 1.25cm, radius: ( + top-left: 2pt, + top-right: 5pt, + bottom-right: 8pt, + bottom-left: 11pt +)) // Different strokes. [ @@ -54,3 +59,11 @@ Use the `*const T` pointer or the `&mut T` reference. --- // Error: 15-38 unexpected key "cake" #rect(radius: (left: 10pt, cake: 5pt)) + +--- +// Error: 15-21 expected stroke or none or dictionary with any of `left`, `top`, `right`, `bottom`, `x`, `y`, or `rest` as keys or auto, found array +#rect(stroke: (1, 2)) + +--- +// Error: 15-19 expected relative length or none or dictionary with any of `top-left`, `top-right`, `bottom-right`, `bottom-left`, `left`, `top`, `right`, `bottom`, or `rest` as keys, found color +#rect(radius: blue)