From 7b6f3a0ab9ae0dac19f62b62b9ecc96ea942a89e Mon Sep 17 00:00:00 2001 From: Martin Haug Date: Mon, 2 May 2022 15:49:46 +0200 Subject: [PATCH] A new `Cast` implementation for `Sides` Reinstate circle --- src/eval/value.rs | 40 +++++++++- src/library/graphics/shape.rs | 115 ++++++++++++--------------- tests/typ/graphics/shape-ellipse.typ | 2 +- 3 files changed, 92 insertions(+), 65 deletions(-) diff --git a/src/eval/value.rs b/src/eval/value.rs index 6ce815a4f..c32614df2 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 Cast for Smart { } } +impl Cast for Sides { + 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:?}")); + } + } + + 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 640c879b9..f7cda9bf9 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(pub Option); +pub struct ShapeNode(pub Option); /// Place a node into a square. -pub type SquareNode = AngularNode; +pub type SquareNode = ShapeNode; /// Place a node into a rectangle. -pub type RectNode = AngularNode; +pub type RectNode = ShapeNode; -// /// Place a node into a sizable and fillable shape. -// #[derive(Debug, Hash)] -// pub struct RoundNode(pub Option); +/// Place a node into a circle. +pub type CircleNode = ShapeNode; -// /// Place a node into a circle. -// pub type CircleNode = RoundNode; - -// /// Place a node into an ellipse. -// pub type EllipseNode = RoundNode; +/// Place a node into an ellipse. +pub type EllipseNode = ShapeNode; #[node] -impl AngularNode { +impl ShapeNode { /// How to fill the shape. pub const FILL: Option = None; /// How to stroke the shape. @@ -44,7 +40,11 @@ impl AngularNode { pub const RADIUS: Sides>> = Sides::splat(Relative::zero()); fn construct(_: &mut Context, args: &mut Args) -> TypResult { - let size = args.named::("size")?.map(Relative::from); + let size = match S { + SQUARE => args.named::("size")?.map(Relative::from), + CIRCLE => args.named::("radius")?.map(|r| 2.0 * Relative::from(r)), + _ => None, + }; let width = match size { None => args.named("width")?, @@ -60,53 +60,33 @@ impl AngularNode { Self(args.find()?).pack().sized(Spec::new(width, height)), )) } -} -castable! { - Sides>, - 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 { + let mut styles = StyleMap::new(); + styles.set_opt(Self::FILL, args.named("fill")?); + + if is_round(S) { + styles.set_opt( + Self::STROKE, + args.named::>>("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>>, - 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 Layout for AngularNode { +impl Layout for ShapeNode { fn layout( &self, ctx: &mut Context, @@ -115,7 +95,13 @@ impl Layout for AngularNode { ) -> TypResult>> { 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 Layout for AngularNode { // 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 Layout for AngularNode { 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 995eabb9b..547acd382 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? ]