summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2022-05-03 23:56:57 +0200
committerGitHub <noreply@github.com>2022-05-03 23:56:57 +0200
commitaa10ea8470763afe98d5ff558381f0a0beb0c017 (patch)
tree938f58a8e0faa4f5216920fc3e82d86301d2f094
parentf9e115daf54c29358f890b137f50a33a781af680 (diff)
parent51d0de09c6f7e2af4db3b65c3fe9595c501b82c9 (diff)
Merge pull request #69 from typst/shapeees
Advanced shapes
-rw-r--r--src/eval/value.rs43
-rw-r--r--src/export/pdf.rs5
-rw-r--r--src/export/render.rs6
-rw-r--r--src/frame.rs54
-rw-r--r--src/geom/mod.rs46
-rw-r--r--src/geom/path.rs19
-rw-r--r--src/geom/rect.rs184
-rw-r--r--src/geom/sides.rs67
-rw-r--r--src/geom/transform.rs7
-rw-r--r--src/library/graphics/shape.rs83
-rw-r--r--src/library/layout/page.rs29
-rw-r--r--src/model/layout.rs6
-rw-r--r--src/model/styles.rs31
-rw-r--r--tests/ref/graphics/shape-ellipse.pngbin22046 -> 24571 bytes
-rw-r--r--tests/ref/graphics/shape-fill-stroke.pngbin2997 -> 6371 bytes
-rw-r--r--tests/ref/graphics/shape-rect.pngbin7261 -> 13791 bytes
-rw-r--r--tests/typ/code/let.typ2
-rw-r--r--tests/typ/code/target.typ2
-rw-r--r--tests/typ/graphics/shape-aspect.typ2
-rw-r--r--tests/typ/graphics/shape-circle.typ4
-rw-r--r--tests/typ/graphics/shape-ellipse.typ5
-rw-r--r--tests/typ/graphics/shape-fill-stroke.typ10
-rw-r--r--tests/typ/graphics/shape-rect.typ31
-rw-r--r--tests/typ/graphics/shape-square.typ2
-rw-r--r--tests/typ/layout/columns.typ6
-rw-r--r--tests/typ/layout/page-margin.typ10
-rw-r--r--tests/typ/layout/page-marginals.typ5
-rw-r--r--tests/typ/layout/page.typ2
-rw-r--r--tests/typ/style/construct.typ6
-rw-r--r--tests/typeset.rs10
30 files changed, 555 insertions, 122 deletions
diff --git a/src/eval/value.rs b/src/eval/value.rs
index 6ce815a4..352906aa 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,47 @@ impl<T: Cast> Cast for Smart<T> {
}
}
+impl<T> Cast for Sides<T>
+where
+ T: Cast + Default + Clone,
+{
+ 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/export/pdf.rs b/src/export/pdf.rs
index 067eb277..7cd6fbfc 100644
--- a/src/export/pdf.rs
+++ b/src/export/pdf.rs
@@ -16,9 +16,10 @@ use ttf_parser::{name_id, GlyphId, Tag};
use super::subset::subset;
use crate::font::{find_name, FaceId, FontStore};
-use crate::frame::{Element, Frame, Geometry, Group, Shape, Text};
+use crate::frame::{Element, Frame, Group, Text};
use crate::geom::{
- self, Color, Em, Length, Numeric, Paint, Point, Size, Stroke, Transform,
+ self, Color, Em, Geometry, Length, Numeric, Paint, Point, Shape, Size, Stroke,
+ Transform,
};
use crate::image::{Image, ImageId, ImageStore, RasterImage};
use crate::Context;
diff --git a/src/export/render.rs b/src/export/render.rs
index c3b92d31..50257e1c 100644
--- a/src/export/render.rs
+++ b/src/export/render.rs
@@ -7,8 +7,10 @@ use tiny_skia as sk;
use ttf_parser::{GlyphId, OutlineBuilder};
use usvg::FitTo;
-use crate::frame::{Element, Frame, Geometry, Group, Shape, Text};
-use crate::geom::{self, Length, Paint, PathElement, Size, Stroke, Transform};
+use crate::frame::{Element, Frame, Group, Text};
+use crate::geom::{
+ self, Geometry, Length, Paint, PathElement, Shape, Size, Stroke, Transform,
+};
use crate::image::{Image, RasterImage, Svg};
use crate::Context;
diff --git a/src/frame.rs b/src/frame.rs
index 5ee6e77e..80e25f3b 100644
--- a/src/frame.rs
+++ b/src/frame.rs
@@ -5,7 +5,7 @@ use std::sync::Arc;
use crate::font::FaceId;
use crate::geom::{
- Align, Em, Length, Numeric, Paint, Path, Point, Size, Spec, Stroke, Transform,
+ Align, Em, Length, Numeric, Paint, Point, Shape, Size, Spec, Transform,
};
use crate::image::ImageId;
use crate::util::{EcoString, MaybeShared};
@@ -40,6 +40,14 @@ impl Frame {
self.elements.insert(0, (pos, element));
}
+ /// Add multiple elements at a position in the background.
+ pub fn prepend_multiple<I>(&mut self, insert: I)
+ where
+ I: IntoIterator<Item = (Point, Element)>,
+ {
+ self.elements.splice(0 .. 0, insert);
+ }
+
/// Add an element at a position in the foreground.
pub fn push(&mut self, pos: Point, element: Element) {
self.elements.push((pos, element));
@@ -297,47 +305,3 @@ pub struct Glyph {
/// The first character of the glyph's cluster.
pub c: char,
}
-
-/// A geometric shape with optional fill and stroke.
-#[derive(Debug, Clone, Eq, PartialEq)]
-pub struct Shape {
- /// The shape's geometry.
- pub geometry: Geometry,
- /// The shape's background fill.
- pub fill: Option<Paint>,
- /// The shape's border stroke.
- pub stroke: Option<Stroke>,
-}
-
-/// A shape's geometry.
-#[derive(Debug, Clone, Eq, PartialEq)]
-pub enum Geometry {
- /// A line to a point (relative to its position).
- Line(Point),
- /// A rectangle with its origin in the topleft corner.
- Rect(Size),
- /// A ellipse with its origin in the topleft corner.
- Ellipse(Size),
- /// A bezier path.
- Path(Path),
-}
-
-impl Geometry {
- /// Fill the geometry without a stroke.
- pub fn filled(self, fill: Paint) -> Shape {
- Shape {
- geometry: self,
- fill: Some(fill),
- stroke: None,
- }
- }
-
- /// Stroke the geometry without a fill.
- pub fn stroked(self, stroke: Stroke) -> Shape {
- Shape {
- geometry: self,
- fill: None,
- stroke: Some(stroke),
- }
- }
-}
diff --git a/src/geom/mod.rs b/src/geom/mod.rs
index a6f53c87..bdd08fe5 100644
--- a/src/geom/mod.rs
+++ b/src/geom/mod.rs
@@ -13,6 +13,7 @@ mod paint;
mod path;
mod point;
mod ratio;
+mod rect;
mod relative;
mod scalar;
mod sides;
@@ -30,6 +31,7 @@ pub use paint::*;
pub use path::*;
pub use point::*;
pub use ratio::*;
+pub use rect::*;
pub use relative::*;
pub use scalar::*;
pub use sides::*;
@@ -60,6 +62,50 @@ pub trait Get<Index> {
}
}
+/// A geometric shape with optional fill and stroke.
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Shape {
+ /// The shape's geometry.
+ pub geometry: Geometry,
+ /// The shape's background fill.
+ pub fill: Option<Paint>,
+ /// The shape's border stroke.
+ pub stroke: Option<Stroke>,
+}
+
+/// A shape's geometry.
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub enum Geometry {
+ /// A line to a point (relative to its position).
+ Line(Point),
+ /// A rectangle with its origin in the topleft corner.
+ Rect(Size),
+ /// A ellipse with its origin in the topleft corner.
+ Ellipse(Size),
+ /// A bezier path.
+ Path(Path),
+}
+
+impl Geometry {
+ /// Fill the geometry without a stroke.
+ pub fn filled(self, fill: Paint) -> Shape {
+ Shape {
+ geometry: self,
+ fill: Some(fill),
+ stroke: None,
+ }
+ }
+
+ /// Stroke the geometry without a fill.
+ pub fn stroked(self, stroke: Stroke) -> Shape {
+ Shape {
+ geometry: self,
+ fill: None,
+ stroke: Some(stroke),
+ }
+ }
+}
+
/// A numeric type.
pub trait Numeric:
Sized
diff --git a/src/geom/path.rs b/src/geom/path.rs
index 836be1b4..d0c3c75d 100644
--- a/src/geom/path.rs
+++ b/src/geom/path.rs
@@ -71,3 +71,22 @@ impl Path {
self.0.push(PathElement::ClosePath);
}
}
+
+/// Get the control points for a bezier curve that describes a circular arc for
+/// a start point, an end point and a center of the circle whose arc connects
+/// the two.
+pub fn bezier_arc(start: Point, center: Point, end: Point) -> [Point; 4] {
+ // https://stackoverflow.com/a/44829356/1567835
+ let a = start - center;
+ let b = end - center;
+
+ let q1 = a.x.to_raw() * a.x.to_raw() + a.y.to_raw() * a.y.to_raw();
+ let q2 = q1 + a.x.to_raw() * b.x.to_raw() + a.y.to_raw() * b.y.to_raw();
+ let k2 = (4.0 / 3.0) * ((2.0 * q1 * q2).sqrt() - q2)
+ / (a.x.to_raw() * b.y.to_raw() - a.y.to_raw() * b.x.to_raw());
+
+ let control_1 = Point::new(center.x + a.x - k2 * a.y, center.y + a.y + k2 * a.x);
+ let control_2 = Point::new(center.x + b.x + k2 * b.y, center.y + b.y - k2 * b.x);
+
+ [start, control_1, control_2, end]
+}
diff --git a/src/geom/rect.rs b/src/geom/rect.rs
new file mode 100644
index 00000000..34160b04
--- /dev/null
+++ b/src/geom/rect.rs
@@ -0,0 +1,184 @@
+use super::*;
+
+use std::mem;
+
+/// A rectangle with rounded corners.
+#[derive(Debug, Copy, Clone, PartialEq)]
+pub struct Rect {
+ size: Size,
+ radius: Sides<Length>,
+}
+
+impl Rect {
+ /// Create a new rectangle.
+ pub fn new(size: Size, radius: Sides<Length>) -> Self {
+ Self { size, radius }
+ }
+
+ /// Output all constituent shapes of the rectangle in order. The last one is
+ /// in the foreground. The function will output multiple items if the stroke
+ /// properties differ by side.
+ pub fn shapes(
+ self,
+ fill: Option<Paint>,
+ stroke: Sides<Option<Stroke>>,
+ ) -> Vec<Shape> {
+ let mut res = vec![];
+ if fill.is_some() || (stroke.iter().any(Option::is_some) && stroke.is_uniform()) {
+ res.push(Shape {
+ geometry: self.fill_geometry(),
+ fill,
+ stroke: if stroke.is_uniform() { stroke.top } else { None },
+ });
+ }
+
+ if !stroke.is_uniform() {
+ for (path, stroke) in self.stroke_segments(stroke) {
+ if stroke.is_some() {
+ res.push(Shape {
+ geometry: Geometry::Path(path),
+ fill: None,
+ stroke,
+ });
+ }
+ }
+ }
+
+ res
+ }
+
+ /// Output the shape of the rectangle as a path or primitive rectangle,
+ /// depending on whether it is rounded.
+ fn fill_geometry(self) -> Geometry {
+ if self.radius.iter().copied().all(Length::is_zero) {
+ Geometry::Rect(self.size)
+ } else {
+ let mut paths = self.stroke_segments(Sides::splat(None));
+ assert_eq!(paths.len(), 1);
+
+ Geometry::Path(paths.pop().unwrap().0)
+ }
+ }
+
+ /// Output the minimum number of paths along the rectangles border.
+ fn stroke_segments(
+ self,
+ strokes: Sides<Option<Stroke>>,
+ ) -> Vec<(Path, Option<Stroke>)> {
+ let mut res = vec![];
+
+ let mut connection = Connection::default();
+ let mut path = Path::new();
+ 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;
+
+ draw_side(
+ &mut path,
+ side,
+ self.size,
+ self.radius.get(side.next_ccw()),
+ self.radius.get(side),
+ connection,
+ );
+
+ if !is_continuous {
+ res.push((mem::take(&mut path), strokes.get(side)));
+ }
+ }
+
+ if always_continuous {
+ path.close_path();
+ }
+
+ if !path.0.is_empty() {
+ res.push((path, strokes.left));
+ }
+
+ res
+ }
+}
+
+/// Draws one side of the rounded rectangle. Will always draw the left arc. The
+/// right arc will be drawn halfway iff there is no connection.
+fn draw_side(
+ path: &mut Path,
+ side: Side,
+ size: Size,
+ radius_left: Length,
+ radius_right: Length,
+ connection: Connection,
+) {
+ let angle_left = Angle::deg(if connection.prev { 90.0 } else { 45.0 });
+ let angle_right = Angle::deg(if connection.next { 90.0 } else { 45.0 });
+
+ 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 mut arc1 = bezier_arc(
+ p1 + Point::new(
+ -angle_left.sin() * radius_left,
+ (1.0 - angle_left.cos()) * radius_left,
+ ),
+ Point::new(radius_left, radius_left),
+ p1,
+ );
+
+ let p2 = Point::with_x(length - radius_right);
+ let mut arc2 = bezier_arc(
+ p2,
+ Point::new(length - radius_right, radius_right),
+ p2 + Point::new(
+ angle_right.sin() * radius_right,
+ (1.0 - angle_right.cos()) * radius_right,
+ ),
+ );
+
+ let transform = match side {
+ Side::Left => Transform::rotate(Angle::deg(-90.0))
+ .post_concat(Transform::translate(Length::zero(), size.y)),
+ Side::Bottom => Transform::rotate(Angle::deg(180.0))
+ .post_concat(Transform::translate(size.x, size.y)),
+ Side::Right => Transform::rotate(Angle::deg(90.0))
+ .post_concat(Transform::translate(size.x, Length::zero())),
+ _ => Transform::identity(),
+ };
+
+ arc1 = arc1.map(|x| x.transform(transform));
+ arc2 = arc2.map(|x| x.transform(transform));
+
+ if !connection.prev {
+ path.move_to(if radius_left.is_zero() { arc1[3] } else { arc1[0] });
+ }
+
+ if !radius_left.is_zero() {
+ path.cubic_to(arc1[1], arc1[2], arc1[3]);
+ }
+
+ path.line_to(arc2[0]);
+
+ if !connection.next && !radius_right.is_zero() {
+ path.cubic_to(arc2[1], arc2[2], arc2[3]);
+ }
+}
+
+/// A state machine that indicates which sides of the border strokes in a 2D
+/// polygon are connected to their neighboring sides.
+#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
+struct Connection {
+ prev: bool,
+ next: bool,
+}
+
+impl Connection {
+ /// Advance to the next clockwise side of the polygon. The argument
+ /// indicates whether the border is connected on the right side of the next
+ /// edge.
+ pub fn advance(self, next: bool) -> Self {
+ Self { prev: self.next, next }
+ }
+}
diff --git a/src/geom/sides.rs b/src/geom/sides.rs
index 3584a1ce..43e470d2 100644
--- a/src/geom/sides.rs
+++ b/src/geom/sides.rs
@@ -31,6 +31,45 @@ impl<T> Sides<T> {
bottom: value,
}
}
+
+ /// Maps the individual fields with `f`.
+ pub fn map<F, U>(self, mut f: F) -> Sides<U>
+ where
+ F: FnMut(T) -> U,
+ {
+ Sides {
+ left: f(self.left),
+ top: f(self.top),
+ right: f(self.right),
+ bottom: f(self.bottom),
+ }
+ }
+
+ /// 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,
+ {
+ 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),
+ }
+ }
+
+ /// Returns an iterator over the sides.
+ pub fn iter(&self) -> impl Iterator<Item = &T> {
+ [&self.left, &self.top, &self.right, &self.bottom].into_iter()
+ }
+
+ /// Returns whether all sides are equal.
+ pub fn is_uniform(&self) -> bool
+ where
+ T: PartialEq,
+ {
+ self.left == self.top && self.top == self.right && self.right == self.bottom
+ }
}
impl<T> Sides<T>
@@ -100,4 +139,32 @@ impl Side {
Self::Bottom => Self::Top,
}
}
+
+ /// The next side, clockwise.
+ pub fn next_cw(self) -> Self {
+ match self {
+ Self::Left => Self::Top,
+ Self::Top => Self::Right,
+ Self::Right => Self::Bottom,
+ Self::Bottom => Self::Left,
+ }
+ }
+
+ /// The next side, counter-clockwise.
+ pub fn next_ccw(self) -> Self {
+ match self {
+ Self::Left => Self::Bottom,
+ Self::Top => Self::Left,
+ Self::Right => Self::Top,
+ Self::Bottom => Self::Right,
+ }
+ }
+
+ /// Return the corresponding axis.
+ pub fn axis(self) -> SpecAxis {
+ match self {
+ Self::Left | Self::Right => SpecAxis::Vertical,
+ Self::Top | Self::Bottom => SpecAxis::Horizontal,
+ }
+ }
}
diff --git a/src/geom/transform.rs b/src/geom/transform.rs
index 28a1af80..40c8e9e4 100644
--- a/src/geom/transform.rs
+++ b/src/geom/transform.rs
@@ -53,7 +53,7 @@ impl Transform {
}
/// Pre-concatenate another transformation.
- pub fn pre_concat(&self, prev: Self) -> Self {
+ pub fn pre_concat(self, prev: Self) -> Self {
Transform {
sx: self.sx * prev.sx + self.kx * prev.ky,
ky: self.ky * prev.sx + self.sy * prev.ky,
@@ -63,6 +63,11 @@ impl Transform {
ty: self.ky.of(prev.tx) + self.sy.of(prev.ty) + self.ty,
}
}
+
+ /// Post-concatenate another transformation.
+ pub fn post_concat(self, next: Self) -> Self {
+ next.pre_concat(self)
+ }
}
impl Default for Transform {
diff --git a/src/library/graphics/shape.rs b/src/library/graphics/shape.rs
index 49c74c2f..40b6e1e3 100644
--- a/src/library/graphics/shape.rs
+++ b/src/library/graphics/shape.rs
@@ -25,9 +25,19 @@ impl<const S: ShapeKind> ShapeNode<S> {
pub const FILL: Option<Paint> = None;
/// How to stroke the shape.
#[property(resolve, fold)]
- pub const STROKE: Smart<Option<RawStroke>> = Smart::Auto;
+ pub const STROKE: Smart<Sides<Option<RawStroke>>> = Smart::Auto;
+
/// How much to pad the shape's content.
- pub const PADDING: Relative<RawLength> = Relative::zero();
+ #[property(resolve, fold)]
+ pub const INSET: Sides<Option<Relative<RawLength>>> = Sides::splat(Relative::zero());
+
+ /// 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(resolve, fold)]
+ pub const RADIUS: Sides<Option<Relative<RawLength>>> = Sides::splat(Relative::zero());
fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> {
let size = match S {
@@ -50,6 +60,30 @@ impl<const S: ShapeKind> ShapeNode<S> {
Self(args.find()?).pack().sized(Spec::new(width, height)),
))
}
+
+ 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")?);
+ }
+
+ styles.set_opt(Self::INSET, args.named("inset")?);
+ styles.set_opt(Self::OUTSET, args.named("outset")?);
+
+ if !is_round(S) {
+ styles.set_opt(Self::RADIUS, args.named("radius")?);
+ }
+
+ Ok(styles)
+ }
}
impl<const S: ShapeKind> Layout for ShapeNode<S> {
@@ -61,13 +95,13 @@ impl<const S: ShapeKind> Layout for ShapeNode<S> {
) -> TypResult<Vec<Arc<Frame>>> {
let mut frames;
if let Some(child) = &self.0 {
- let mut padding = styles.get(Self::PADDING);
+ let mut inset = styles.get(Self::INSET);
if is_round(S) {
- padding.rel += Ratio::new(0.5 - SQRT_2 / 4.0);
+ inset = inset.map(|side| side + Ratio::new(0.5 - SQRT_2 / 4.0));
}
// Pad the child.
- let child = child.clone().padded(Sides::splat(padding));
+ let child = child.clone().padded(inset.map(|side| side.map(RawLength::from)));
let mut pod = Regions::one(regions.first, regions.base, regions.expand);
frames = child.layout(ctx, &pod, styles)?;
@@ -114,19 +148,38 @@ impl<const S: ShapeKind> Layout for ShapeNode<S> {
// Add fill and/or stroke.
let fill = styles.get(Self::FILL);
let stroke = match styles.get(Self::STROKE) {
- Smart::Auto => fill.is_none().then(Stroke::default),
- Smart::Custom(stroke) => stroke.map(RawStroke::unwrap_or_default),
+ Smart::Auto if fill.is_none() => Sides::splat(Some(Stroke::default())),
+ Smart::Auto => Sides::splat(None),
+ Smart::Custom(strokes) => {
+ strokes.map(|s| s.map(RawStroke::unwrap_or_default))
+ }
};
- if fill.is_some() || stroke.is_some() {
- let geometry = if is_round(S) {
- Geometry::Ellipse(frame.size)
- } else {
- Geometry::Rect(frame.size)
- };
+ let outset = styles.get(Self::OUTSET).relative_to(frame.size);
+ let size = frame.size + outset.sum_by_axis();
+
+ let radius = styles
+ .get(Self::RADIUS)
+ .map(|side| side.relative_to(size.x.min(size.y) / 2.0));
- let shape = Shape { geometry, fill, stroke };
- frame.prepend(Point::zero(), Element::Shape(shape));
+ let pos = Point::new(-outset.left, -outset.top);
+
+ if fill.is_some() || stroke.iter().any(Option::is_some) {
+ if is_round(S) {
+ let shape = Shape {
+ geometry: Geometry::Ellipse(size),
+ fill,
+ stroke: stroke.left,
+ };
+ frame.prepend(pos, Element::Shape(shape));
+ } else {
+ frame.prepend_multiple(
+ Rect::new(size, radius)
+ .shapes(fill, stroke)
+ .into_iter()
+ .map(|x| (pos, Element::Shape(x))),
+ )
+ }
}
// Apply link if it exists.
diff --git a/src/library/layout/page.rs b/src/library/layout/page.rs
index 4307d2f9..c8495e64 100644
--- a/src/library/layout/page.rs
+++ b/src/library/layout/page.rs
@@ -18,14 +18,10 @@ impl PageNode {
/// Whether the page is flipped into landscape orientation.
pub const FLIPPED: bool = false;
- /// The left margin.
- pub const LEFT: Smart<Relative<RawLength>> = Smart::Auto;
- /// The right margin.
- pub const RIGHT: Smart<Relative<RawLength>> = Smart::Auto;
- /// The top margin.
- pub const TOP: Smart<Relative<RawLength>> = Smart::Auto;
- /// The bottom margin.
- pub const BOTTOM: Smart<Relative<RawLength>> = Smart::Auto;
+ /// The page margin.
+ #[property(fold)]
+ pub const MARGINS: Sides<Option<Smart<Relative<RawLength>>>> =
+ Sides::splat(Smart::Auto);
/// How many columns the page has.
pub const COLUMNS: NonZeroUsize = NonZeroUsize::new(1).unwrap();
@@ -53,15 +49,7 @@ impl PageNode {
styles.set_opt(Self::WIDTH, args.named("width")?);
styles.set_opt(Self::HEIGHT, args.named("height")?);
-
- let all = args.named("margins")?;
- let hor = args.named("horizontal")?;
- let ver = args.named("vertical")?;
- styles.set_opt(Self::LEFT, args.named("left")?.or(hor).or(all));
- styles.set_opt(Self::TOP, args.named("top")?.or(ver).or(all));
- styles.set_opt(Self::RIGHT, args.named("right")?.or(hor).or(all));
- styles.set_opt(Self::BOTTOM, args.named("bottom")?.or(ver).or(all));
-
+ styles.set_opt(Self::MARGINS, args.named("margins")?);
styles.set_opt(Self::FLIPPED, args.named("flipped")?);
styles.set_opt(Self::FILL, args.named("fill")?);
styles.set_opt(Self::COLUMNS, args.named("columns")?);
@@ -96,12 +84,7 @@ impl PageNode {
// Determine the margins.
let default = Relative::from(0.1190 * min);
- let padding = Sides {
- left: styles.get(Self::LEFT).unwrap_or(default),
- right: styles.get(Self::RIGHT).unwrap_or(default),
- top: styles.get(Self::TOP).unwrap_or(default),
- bottom: styles.get(Self::BOTTOM).unwrap_or(default),
- };
+ let padding = styles.get(Self::MARGINS).map(|side| side.unwrap_or(default));
let mut child = self.0.clone();
diff --git a/src/model/layout.rs b/src/model/layout.rs
index 51154286..3b82ddc2 100644
--- a/src/model/layout.rs
+++ b/src/model/layout.rs
@@ -8,8 +8,10 @@ use std::sync::Arc;
use super::{Barrier, NodeId, Resolve, StyleChain, StyleEntry};
use crate::diag::TypResult;
use crate::eval::{RawAlign, RawLength};
-use crate::frame::{Element, Frame, Geometry};
-use crate::geom::{Align, Length, Paint, Point, Relative, Sides, Size, Spec, Stroke};
+use crate::frame::{Element, Frame};
+use crate::geom::{
+ Align, Geometry, Length, Paint, Point, Relative, Sides, Size, Spec, Stroke,
+};
use crate::library::graphics::MoveNode;
use crate::library::layout::{AlignNode, PadNode};
use crate::util::Prehashed;
diff --git a/src/model/styles.rs b/src/model/styles.rs
index eb7a7053..ae4c1586 100644
--- a/src/model/styles.rs
+++ b/src/model/styles.rs
@@ -7,8 +7,8 @@ use std::sync::Arc;
use super::{Content, Show, ShowNode};
use crate::diag::{At, TypResult};
-use crate::eval::{Args, Func, Node, Smart, Value};
-use crate::geom::{Numeric, Relative, Sides, Spec};
+use crate::eval::{Args, Func, Node, RawLength, Smart, Value};
+use crate::geom::{Length, Numeric, Relative, Sides, Spec};
use crate::library::layout::PageNode;
use crate::library::structure::{EnumNode, ListNode};
use crate::library::text::{FontFamily, ParNode, TextNode};
@@ -459,6 +459,33 @@ where
}
}
+impl<T> Fold for Sides<T>
+where
+ T: Fold,
+{
+ type Output = Sides<T::Output>;
+
+ fn fold(self, outer: Self::Output) -> Self::Output {
+ self.zip(outer, |inner, outer, _| inner.fold(outer))
+ }
+}
+
+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))
+ }
+}
+
+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))
+ }
+}
+
/// A scoped property barrier.
///
/// Barriers interact with [scoped](StyleMap::scoped) styles: A scoped style
diff --git a/tests/ref/graphics/shape-ellipse.png b/tests/ref/graphics/shape-ellipse.png
index 740f005f..296fc14e 100644
--- a/tests/ref/graphics/shape-ellipse.png
+++ b/tests/ref/graphics/shape-ellipse.png
Binary files differ
diff --git a/tests/ref/graphics/shape-fill-stroke.png b/tests/ref/graphics/shape-fill-stroke.png
index f2278c88..91cddcc2 100644
--- a/tests/ref/graphics/shape-fill-stroke.png
+++ b/tests/ref/graphics/shape-fill-stroke.png
Binary files differ
diff --git a/tests/ref/graphics/shape-rect.png b/tests/ref/graphics/shape-rect.png
index 1fdb0dac..5bbaf3db 100644
--- a/tests/ref/graphics/shape-rect.png
+++ b/tests/ref/graphics/shape-rect.png
Binary files differ
diff --git a/tests/typ/code/let.typ b/tests/typ/code/let.typ
index a95d651a..c3be64a5 100644
--- a/tests/typ/code/let.typ
+++ b/tests/typ/code/let.typ
@@ -11,7 +11,7 @@
// Syntax sugar for function definitions.
#let fill = conifer
-#let rect(body) = rect(width: 2cm, fill: fill, padding: 5pt, body)
+#let rect(body) = rect(width: 2cm, fill: fill, inset: 5pt, body)
#rect[Hi!]
---
diff --git a/tests/typ/code/target.typ b/tests/typ/code/target.typ
index 6c321592..b0a3fbf3 100644
--- a/tests/typ/code/target.typ
+++ b/tests/typ/code/target.typ
@@ -7,6 +7,6 @@
#let d = 3
#let value = [hi]
#let item(a, b) = a + b
-#let fn = rect.with(fill: conifer, padding: 5pt)
+#let fn = rect.with(fill: conifer, inset: 5pt)
Some _includable_ text.
diff --git a/tests/typ/graphics/shape-aspect.typ b/tests/typ/graphics/shape-aspect.typ
index 970857b6..70d689f7 100644
--- a/tests/typ/graphics/shape-aspect.typ
+++ b/tests/typ/graphics/shape-aspect.typ
@@ -11,7 +11,7 @@
---
// Test alignment in automatically sized square and circle.
#set text(8pt)
-#square(padding: 4pt)[
+#square(inset: 4pt)[
Hey there, #align(center + bottom, rotate(180deg, [you!]))
]
#circle(align(center + horizon, [Hey.]))
diff --git a/tests/typ/graphics/shape-circle.typ b/tests/typ/graphics/shape-circle.typ
index dc1e3f24..13ff67de 100644
--- a/tests/typ/graphics/shape-circle.typ
+++ b/tests/typ/graphics/shape-circle.typ
@@ -16,13 +16,13 @@ Auto-sized circle. \
Center-aligned rect in auto-sized circle.
#circle(fill: forest, stroke: conifer,
align(center + horizon,
- rect(fill: conifer, padding: 5pt)[But, soft!]
+ rect(fill: conifer, inset: 5pt)[But, soft!]
)
)
Rect in auto-sized circle. \
#circle(fill: forest,
- rect(fill: conifer, stroke: white, padding: 4pt)[
+ rect(fill: conifer, stroke: white, inset: 4pt)[
#set text(8pt)
But, soft! what light through yonder window breaks?
]
diff --git a/tests/typ/graphics/shape-ellipse.typ b/tests/typ/graphics/shape-ellipse.typ
index 995eabb9..ba4d0d0a 100644
--- a/tests/typ/graphics/shape-ellipse.typ
+++ b/tests/typ/graphics/shape-ellipse.typ
@@ -17,7 +17,10 @@ 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?
]
+
+
+An inline #ellipse(width: 8pt, height: 6pt, outset: (top: 3pt, rest: 5.5pt)) ellipse. \ No newline at end of file
diff --git a/tests/typ/graphics/shape-fill-stroke.typ b/tests/typ/graphics/shape-fill-stroke.typ
index c09cb065..d14d0981 100644
--- a/tests/typ/graphics/shape-fill-stroke.typ
+++ b/tests/typ/graphics/shape-fill-stroke.typ
@@ -38,3 +38,13 @@
#sq(stroke: blue)
#sq(fill: teal, stroke: blue)
#sq(fill: teal, stroke: 2pt + blue)
+
+---
+// Test stroke composition.
+#set square(stroke: 4pt)
+#set text("Roboto")
+#square(
+ stroke: (left: red, top: yellow, right: green, bottom: blue),
+ radius: 100%, align(center+horizon)[*G*],
+ inset: 8pt
+)
diff --git a/tests/typ/graphics/shape-rect.typ b/tests/typ/graphics/shape-rect.typ
index e035fc91..a29550b5 100644
--- a/tests/typ/graphics/shape-rect.typ
+++ b/tests/typ/graphics/shape-rect.typ
@@ -8,7 +8,7 @@
#set page(width: 150pt)
// Fit to text.
-#rect(fill: conifer, padding: 3pt)[Textbox]
+#rect(fill: conifer, inset: 3pt)[Textbox]
// Empty with fixed width and height.
#block(rect(
@@ -18,7 +18,7 @@
))
// Fixed width, text height.
-#rect(width: 2cm, fill: rgb("9650d6"), padding: 5pt)[Fixed and padded]
+#rect(width: 2cm, fill: rgb("9650d6"), inset: 5pt)[Fixed and padded]
// Page width, fixed height.
#rect(height: 1cm, width: 100%, fill: rgb("734ced"))[Topleft]
@@ -27,3 +27,30 @@
\{#rect(width: 0.5in, height: 7pt, fill: rgb("d6cd67"))
#rect(width: 0.5in, height: 7pt, fill: rgb("edd466"))
#rect(width: 0.5in, height: 7pt, fill: rgb("e3be62"))\}
+
+// 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))
+
+// Different strokes.
+[
+ #set rect(stroke: (right: red))
+ #rect(width: 100%, fill: lime, stroke: (x: 5pt, y: 1pt))
+]
+
+---
+// Outset padding.
+#show node: raw as [
+ #set text("IBM Plex Mono", 8pt)
+ #h(.7em, weak: true)
+ #rect(radius: 3pt, outset: (y: 3pt, x: 2.5pt), fill: rgb(239, 241, 243))[{node.text}]
+ #h(.7em, weak: true)
+]
+
+Use the `*const ptr` pointer.
+
+---
+// Error: 15-38 unexpected key "cake"
+#rect(radius: (left: 10pt, cake: 5pt))
+
diff --git a/tests/typ/graphics/shape-square.typ b/tests/typ/graphics/shape-square.typ
index c4ece778..622fa9c8 100644
--- a/tests/typ/graphics/shape-square.typ
+++ b/tests/typ/graphics/shape-square.typ
@@ -7,7 +7,7 @@
---
// Test auto-sized square.
-#square(fill: eastern, padding: 5pt)[
+#square(fill: eastern, inset: 5pt)[
#set text(fill: white, weight: "bold")
Typst
]
diff --git a/tests/typ/layout/columns.typ b/tests/typ/layout/columns.typ
index ce291fb2..1e77e6bc 100644
--- a/tests/typ/layout/columns.typ
+++ b/tests/typ/layout/columns.typ
@@ -16,7 +16,7 @@
// Test the `columns` function.
#set page(width: auto)
-#rect(width: 180pt, height: 100pt, padding: 8pt, columns(2, [
+#rect(width: 180pt, height: 100pt, inset: 8pt, columns(2, [
A special plight has befallen our document.
Columns in text boxes reigned down unto the soil
to waste a year's crop of rich layouts.
@@ -40,7 +40,7 @@ a page for a test but it does get the job done.
// Test the expansion behavior.
#set page(height: 2.5cm, width: 7.05cm)
-#rect(padding: 6pt, columns(2, [
+#rect(inset: 6pt, columns(2, [
ABC \
BCD
#colbreak()
@@ -73,7 +73,7 @@ D
// Test an empty second column.
#set page(width: 7.05cm, columns: 2)
-#rect(width: 100%, padding: 3pt)[So there isn't anything in the second column?]
+#rect(width: 100%, inset: 3pt)[So there isn't anything in the second column?]
---
// Test columns when one of them is empty.
diff --git a/tests/typ/layout/page-margin.typ b/tests/typ/layout/page-margin.typ
index 44126d2d..290c4081 100644
--- a/tests/typ/layout/page-margin.typ
+++ b/tests/typ/layout/page-margin.typ
@@ -11,10 +11,10 @@
---
// Set individual margins.
#set page(height: 40pt)
-[#set page(left: 0pt); #align(left)[Left]]
-[#set page(right: 0pt); #align(right)[Right]]
-[#set page(top: 0pt); #align(top)[Top]]
-[#set page(bottom: 0pt); #align(bottom)[Bottom]]
+[#set page(margins: (left: 0pt)); #align(left)[Left]]
+[#set page(margins: (right: 0pt)); #align(right)[Right]]
+[#set page(margins: (top: 0pt)); #align(top)[Top]]
+[#set page(margins: (bottom: 0pt)); #align(bottom)[Bottom]]
// Ensure that specific margins override general margins.
-[#set page(margins: 0pt, left: 20pt); Overriden]
+[#set page(margins: (rest: 0pt, left: 20pt)); Overriden]
diff --git a/tests/typ/layout/page-marginals.typ b/tests/typ/layout/page-marginals.typ
index 9fd193c6..6e8e3d85 100644
--- a/tests/typ/layout/page-marginals.typ
+++ b/tests/typ/layout/page-marginals.typ
@@ -1,7 +1,6 @@
#set page(
paper: "a8",
- margins: 30pt,
- horizontal: 15pt,
+ margins: (x: 15pt, y: 30pt),
header: align(horizon, {
text(eastern)[*Typst*]
h(1fr)
@@ -18,5 +17,5 @@ do wear it; cast it off. It is my lady, O, it is my love! O, that she knew she
were! She speaks yet she says nothing: what of that? Her eye discourses; I will
answer it.
-#set page(header: none, height: auto, top: 15pt, bottom: 25pt)
+#set page(header: none, height: auto, margins: (top: 15pt, bottom: 25pt))
The END.
diff --git a/tests/typ/layout/page.typ b/tests/typ/layout/page.typ
index 89d0f2fb..3157ebf9 100644
--- a/tests/typ/layout/page.typ
+++ b/tests/typ/layout/page.typ
@@ -24,7 +24,7 @@
// Test page fill.
#set page(width: 80pt, height: 40pt, fill: eastern)
#text(15pt, "Roboto", fill: white, smallcaps: true)[Typst]
-#page(width: 40pt, fill: none, margins: auto, top: 10pt)[Hi]
+#page(width: 40pt, fill: none, margins: (top: 10pt, rest: auto))[Hi]
---
// Just page followed by pagebreak.
diff --git a/tests/typ/style/construct.typ b/tests/typ/style/construct.typ
index f01b534b..890c4b94 100644
--- a/tests/typ/style/construct.typ
+++ b/tests/typ/style/construct.typ
@@ -16,17 +16,17 @@
// but the B should be center-aligned.
#set par(align: center)
#par(align: right)[
- A #rect(width: 2cm, fill: conifer, padding: 4pt)[B]
+ A #rect(width: 2cm, fill: conifer, inset: 4pt)[B]
]
---
// The inner rectangle should also be yellow here.
// (and therefore invisible)
-[#set rect(fill: yellow);#text(1em, rect(padding: 5pt, rect()))]
+[#set rect(fill: yellow);#text(1em, rect(inset: 5pt, rect()))]
---
// The inner rectangle should not be yellow here.
-A #rect(fill: yellow, padding: 5pt, rect()) B
+A #rect(fill: yellow, inset: 5pt, rect()) B
---
// The inner list should not be indented extra.
diff --git a/tests/typeset.rs b/tests/typeset.rs
index 9f84a733..b0452163 100644
--- a/tests/typeset.rs
+++ b/tests/typeset.rs
@@ -12,7 +12,7 @@ use walkdir::WalkDir;
use typst::diag::Error;
use typst::eval::{Smart, Value};
use typst::frame::{Element, Frame};
-use typst::geom::{Length, RgbaColor};
+use typst::geom::{Length, RgbaColor, Sides};
use typst::library::layout::PageNode;
use typst::library::text::{TextNode, TextSize};
use typst::loading::FsLoader;
@@ -64,10 +64,10 @@ fn main() {
let mut styles = StyleMap::new();
styles.set(PageNode::WIDTH, Smart::Custom(Length::pt(120.0).into()));
styles.set(PageNode::HEIGHT, Smart::Auto);
- styles.set(PageNode::LEFT, Smart::Custom(Length::pt(10.0).into()));
- styles.set(PageNode::TOP, Smart::Custom(Length::pt(10.0).into()));
- styles.set(PageNode::RIGHT, Smart::Custom(Length::pt(10.0).into()));
- styles.set(PageNode::BOTTOM, Smart::Custom(Length::pt(10.0).into()));
+ styles.set(
+ PageNode::MARGINS,
+ Sides::splat(Some(Smart::Custom(Length::pt(10.0).into()))),
+ );
styles.set(TextNode::SIZE, TextSize(Length::pt(10.0).into()));
// Hook up an assert function into the global scope.