summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2022-06-14 15:07:13 +0200
committerLaurenz <laurmaedje@gmail.com>2022-06-14 17:17:54 +0200
commit7a6c2cce7747f7632f0be012f49b548db3e62a2d (patch)
tree30103d743fcf22c6838e5ce3b6c632abe15e78b9 /src
parent6832ca2a26df5a9407bd2b0266cc0bab328ebeba (diff)
Make radius configuration unconfusing
Diffstat (limited to 'src')
-rw-r--r--src/eval/dict.rs5
-rw-r--r--src/eval/value.rs212
-rw-r--r--src/geom/corners.rs122
-rw-r--r--src/geom/mod.rs2
-rw-r--r--src/geom/rect.rs44
-rw-r--r--src/geom/sides.rs27
-rw-r--r--src/library/graphics/shape.rs4
-rw-r--r--src/model/property.rs42
8 files changed, 332 insertions, 126 deletions
diff --git a/src/eval/dict.rs b/src/eval/dict.rs
index 8893ce48..654c90eb 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<Value> {
+ 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 a7da99c9..294aac94 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<T: Cast> Cast<Spanned<Value>> for Spanned<T> {
}
}
+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<T: Cast> Cast for Option<T> {
fn is(value: &Value) -> bool {
matches!(value, Value::None) || T::is(value)
@@ -609,112 +675,84 @@ impl<T: Cast> Cast for Smart<T> {
impl<T> Cast for Sides<T>
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<Self> {
- 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<Self> {
+ 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",
-}
-
-dynamic! {
- Regex: "regular expression",
-}
+impl<T> Cast for Corners<T>
+where
+ T: Cast + Default + Copy,
+{
+ fn is(value: &Value) -> bool {
+ matches!(value, Value::Dict(_)) || T::is(value)
+ }
-dynamic! {
- Group: "group",
-}
+ fn cast(mut value: Value) -> StrResult<Self> {
+ if let Value::Dict(dict) = &mut value {
+ let mut take = |key| dict.take(key).map(T::cast).transpose();
+
+ 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),
+ };
+
+ if let Some((key, _)) = dict.iter().next() {
+ return Err(format!("unexpected key {key:?}"));
+ }
-castable! {
- usize,
- Expected: "non-negative integer",
- Value::Int(int) => int.try_into().map_err(|_| {
- if int < 0 {
- "must be at least zero"
+ 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 00000000..54fcd12f
--- /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<T> {
+ /// 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<T> Corners<T> {
+ /// 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<F, U>(self, mut f: F) -> Corners<U>
+ 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<F, V, W>(self, other: Corners<V>, mut f: F) -> Corners<W>
+ 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<Item = &T> {
+ [
+ &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<T> Get<Corner> for Corners<T> {
+ 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 bdd08fe5..fc8ccb6c 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 dceb3577..dfea2c45 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<Length>,
+ /// The radius at each corner.
+ pub radius: Corners<Length>,
}
impl RoundedRect {
/// Create a new rounded rectangle.
- pub fn new(size: Size, radius: Sides<Length>) -> Self {
+ pub fn new(size: Size, radius: Corners<Length>) -> 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 938539fe..72748916 100644
--- a/src/geom/sides.rs
+++ b/src/geom/sides.rs
@@ -48,17 +48,17 @@ impl<T> Sides<T> {
/// Zip two instances into an instance.
pub fn zip<F, V, W>(self, other: Sides<V>, mut f: F) -> Sides<W>
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<Item = &T> {
[&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 eed3c9d9..5cc5a76d 100644
--- a/src/library/graphics/shape.rs
+++ b/src/library/graphics/shape.rs
@@ -33,9 +33,11 @@ impl<const S: ShapeKind> ShapeNode<S> {
/// How much to extend the shape's dimensions beyond the allocated space.
#[property(resolve, fold)]
pub const OUTSET: Sides<Option<Relative<RawLength>>> = Sides::splat(Relative::zero());
+
/// How much to round the shape's corners.
#[property(skip, resolve, fold)]
- pub const RADIUS: Sides<Option<Relative<RawLength>>> = Sides::splat(Relative::zero());
+ pub const RADIUS: Corners<Option<Relative<RawLength>>> =
+ Corners::splat(Relative::zero());
fn construct(_: &mut Machine, args: &mut Args) -> TypResult<Content> {
let size = match S {
diff --git a/src/model/property.rs b/src/model/property.rs
index 0e171939..8681da7d 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<T: Resolve> Resolve for Sides<T> {
type Output = Sides<T::Output>;
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<T: Resolve> Resolve for Corners<T> {
+ type Output = Corners<T::Output>;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ self.map(|v| v.resolve(styles))
}
}
@@ -252,7 +255,7 @@ where
type Output = Sides<T::Output>;
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<Option<Relative<Length>>> {
type Output = Sides<Relative<Length>>;
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<Option<Smart<Relative<RawLength>>>> {
type Output = Sides<Smart<Relative<RawLength>>>;
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<T> Fold for Corners<T>
+where
+ T: Fold,
+{
+ type Output = Corners<T::Output>;
+
+ fn fold(self, outer: Self::Output) -> Self::Output {
+ self.zip(outer, |inner, outer| inner.fold(outer))
+ }
+}
+
+impl Fold for Corners<Option<Relative<Length>>> {
+ type Output = Corners<Relative<Length>>;
+
+ fn fold(self, outer: Self::Output) -> Self::Output {
+ self.zip(outer, |inner, outer| inner.unwrap_or(outer))
}
}