summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMartin Haug <mhaug@live.de>2022-05-02 15:49:46 +0200
committerMartin Haug <mhaug@live.de>2022-05-02 16:35:11 +0200
commit7b6f3a0ab9ae0dac19f62b62b9ecc96ea942a89e (patch)
treec1722475478ffae72f0bf7a8d96e11358625d147
parent84a4961a5dd03072b0e94c715957475d4ae21e4f (diff)
A new `Cast` implementation for `Sides`
Reinstate circle
-rw-r--r--src/eval/value.rs40
-rw-r--r--src/library/graphics/shape.rs115
-rw-r--r--tests/typ/graphics/shape-ellipse.typ2
3 files changed, 92 insertions, 65 deletions
diff --git a/src/eval/value.rs b/src/eval/value.rs
index 6ce815a4..c32614df 100644
--- a/src/eval/value.rs
+++ b/src/eval/value.rs
@@ -8,7 +8,7 @@ use std::sync::Arc;
use super::{ops, Args, Array, Dict, Func, RawLength};
use crate::diag::{with_alternative, StrResult};
use crate::geom::{
- Angle, Color, Dir, Em, Fraction, Length, Paint, Ratio, Relative, RgbaColor,
+ Angle, Color, Dir, Em, Fraction, Length, Paint, Ratio, Relative, RgbaColor, Sides,
};
use crate::library::text::RawNode;
use crate::model::{Content, Layout, LayoutNode};
@@ -596,6 +596,44 @@ impl<T: Cast> Cast for Smart<T> {
}
}
+impl<T: Cast + Default + Clone> Cast for Sides<T> {
+ 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:?}"));
+ }
+ }
+
+ let sides = Sides {
+ left: dict.get("left".into()).or_else(|_| dict.get("x".into())),
+ top: dict.get("top".into()).or_else(|_| dict.get("y".into())),
+ right: dict.get("right".into()).or_else(|_| dict.get("x".into())),
+ bottom: dict.get("bottom".into()).or_else(|_| dict.get("y".into())),
+ }
+ .map(|side| {
+ side.or_else(|_| dict.get("rest".into()))
+ .and_then(|v| T::cast(v.clone()))
+ .unwrap_or_default()
+ });
+
+ Ok(sides)
+ }
+ v => T::cast(v)
+ .map(Sides::splat)
+ .map_err(|msg| with_alternative(msg, "dictionary")),
+ }
+ }
+}
+
dynamic! {
Dir: "direction",
}
diff --git a/src/library/graphics/shape.rs b/src/library/graphics/shape.rs
index 640c879b..f7cda9bf 100644
--- a/src/library/graphics/shape.rs
+++ b/src/library/graphics/shape.rs
@@ -5,26 +5,22 @@ use crate::library::text::TextNode;
/// Place a node into a sizable and fillable shape.
#[derive(Debug, Hash)]
-pub struct AngularNode<const S: ShapeKind>(pub Option<LayoutNode>);
+pub struct ShapeNode<const S: ShapeKind>(pub Option<LayoutNode>);
/// Place a node into a square.
-pub type SquareNode = AngularNode<SQUARE>;
+pub type SquareNode = ShapeNode<SQUARE>;
/// Place a node into a rectangle.
-pub type RectNode = AngularNode<RECT>;
+pub type RectNode = ShapeNode<RECT>;
-// /// Place a node into a sizable and fillable shape.
-// #[derive(Debug, Hash)]
-// pub struct RoundNode<const S: ShapeKind>(pub Option<LayoutNode>);
+/// Place a node into a circle.
+pub type CircleNode = ShapeNode<CIRCLE>;
-// /// Place a node into a circle.
-// pub type CircleNode = RoundNode<CIRCLE>;
-
-// /// Place a node into an ellipse.
-// pub type EllipseNode = RoundNode<ELLIPSE>;
+/// Place a node into an ellipse.
+pub type EllipseNode = ShapeNode<ELLIPSE>;
#[node]
-impl<const S: ShapeKind> AngularNode<S> {
+impl<const S: ShapeKind> ShapeNode<S> {
/// How to fill the shape.
pub const FILL: Option<Paint> = None;
/// How to stroke the shape.
@@ -44,7 +40,11 @@ impl<const S: ShapeKind> AngularNode<S> {
pub const RADIUS: Sides<Option<Relative<RawLength>>> = Sides::splat(Relative::zero());
fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> {
- let size = args.named::<RawLength>("size")?.map(Relative::from);
+ let size = match S {
+ SQUARE => args.named::<RawLength>("size")?.map(Relative::from),
+ CIRCLE => args.named::<RawLength>("radius")?.map(|r| 2.0 * Relative::from(r)),
+ _ => None,
+ };
let width = match size {
None => args.named("width")?,
@@ -60,53 +60,33 @@ impl<const S: ShapeKind> AngularNode<S> {
Self(args.find()?).pack().sized(Spec::new(width, height)),
))
}
-}
-castable! {
- Sides<Option<RawStroke>>,
- Expected: "stroke, dictionary with strokes for each side",
- Value::None => {
- Sides::splat(None)
- },
- Value::Dict(values) => {
- let get = |name: &str| values.get(name.into()).and_then(|v| v.clone().cast()).unwrap_or(None);
- Sides {
- top: get("top"),
- right: get("right"),
- bottom: get("bottom"),
- left: get("left"),
+ fn set(args: &mut Args) -> TypResult<StyleMap> {
+ let mut styles = StyleMap::new();
+ styles.set_opt(Self::FILL, args.named("fill")?);
+
+ if is_round(S) {
+ styles.set_opt(
+ Self::STROKE,
+ args.named::<Smart<Option<RawStroke>>>("stroke")?
+ .map(|some| some.map(Sides::splat)),
+ );
+ } else {
+ styles.set_opt(Self::STROKE, args.named("stroke")?);
}
- },
- Value::Length(thickness) => Sides::splat(Some(RawStroke {
- paint: Smart::Auto,
- thickness: Smart::Custom(thickness),
- })),
- Value::Color(color) => Sides::splat(Some(RawStroke {
- paint: Smart::Custom(color.into()),
- thickness: Smart::Auto,
- })),
- @stroke: RawStroke => Sides::splat(Some(*stroke)),
-}
-castable! {
- Sides<Option<Relative<RawLength>>>,
- Expected: "length or dictionary of lengths for each side",
- Value::None => Sides::splat(None),
- Value::Dict(values) => {
- let get = |name: &str| values.get(name.into()).and_then(|v| v.clone().cast()).unwrap_or(None);
- Sides {
- top: get("top"),
- right: get("right"),
- bottom: get("bottom"),
- left: get("left"),
+ styles.set_opt(Self::INSET, args.named("inset")?);
+ styles.set_opt(Self::OUTSET, args.named("outset")?);
+
+ if S != CIRCLE {
+ styles.set_opt(Self::RADIUS, args.named("radius")?);
}
- },
- Value::Length(l) => Sides::splat(Some(l.into())),
- Value::Ratio(r) => Sides::splat(Some(r.into())),
- Value::Relative(r) => Sides::splat(Some(r)),
+
+ Ok(styles)
+ }
}
-impl<const S: ShapeKind> Layout for AngularNode<S> {
+impl<const S: ShapeKind> Layout for ShapeNode<S> {
fn layout(
&self,
ctx: &mut Context,
@@ -115,7 +95,13 @@ impl<const S: ShapeKind> Layout for AngularNode<S> {
) -> TypResult<Vec<Arc<Frame>>> {
let mut frames;
if let Some(child) = &self.0 {
- let inset = styles.get(Self::INSET);
+ let mut inset = styles.get(Self::INSET);
+ if is_round(S) {
+ inset = inset.map(|mut side| {
+ side.rel += Ratio::new(0.5 - SQRT_2 / 4.0);
+ side
+ });
+ }
// Pad the child.
let child = child.clone().padded(inset.map(|side| side.map(RawLength::from)));
@@ -164,10 +150,12 @@ impl<const S: ShapeKind> Layout for AngularNode<S> {
// Add fill and/or stroke.
let fill = styles.get(Self::FILL);
- let stroke = match styles.get(Self::STROKE) {
+ let mut stroke = match styles.get(Self::STROKE) {
Smart::Auto if fill.is_none() => Sides::splat(Some(Stroke::default())),
Smart::Auto => Sides::splat(None),
- Smart::Custom(strokes) => strokes.map(|s| s.map(|s| s.unwrap_or_default())),
+ Smart::Custom(strokes) => {
+ strokes.map(|s| s.map(RawStroke::unwrap_or_default))
+ }
};
let outset = styles.get(Self::OUTSET);
@@ -191,13 +179,14 @@ impl<const S: ShapeKind> Layout for AngularNode<S> {
bottom: radius.bottom.relative_to(size.y / 2.0),
};
-
- if fill.is_some() || stroke.iter().any(Option::is_some) {
- let shape = Shape {
- geometry: Geometry::Rect(size, radius),
- fill,
- stroke,
+ if fill.is_some() || (stroke.iter().any(Option::is_some) && stroke.is_uniform()) {
+ let geometry = if is_round(S) {
+ Geometry::Ellipse(size)
+ } else {
+ Geometry::Rect(size, radius)
};
+
+ let shape = Shape { geometry, fill, stroke };
frame.prepend(Point::new(-outset.left, -outset.top), Element::Shape(shape));
}
diff --git a/tests/typ/graphics/shape-ellipse.typ b/tests/typ/graphics/shape-ellipse.typ
index 995eabb9..547acd38 100644
--- a/tests/typ/graphics/shape-ellipse.typ
+++ b/tests/typ/graphics/shape-ellipse.typ
@@ -17,7 +17,7 @@ Rect in ellipse in fixed rect. \
)
Auto-sized ellipse. \
-#ellipse(fill: conifer, stroke: 3pt + forest, padding: 3pt)[
+#ellipse(fill: conifer, stroke: 3pt + forest, inset: 3pt)[
#set text(8pt)
But, soft! what light through yonder window breaks?
]