diff --git a/src/eval/value.rs b/src/eval/value.rs index 6ce815a4f..352906aab 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 Cast for Smart { } } +impl Cast for Sides +where + T: Cast + Default + Clone, +{ + 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/export/pdf.rs b/src/export/pdf.rs index 067eb2775..7cd6fbfc7 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 c3b92d315..50257e1c2 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 5ee6e77e4..80e25f3b4 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(&mut self, insert: I) + where + I: IntoIterator, + { + 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, - /// The shape's border stroke. - pub stroke: Option, -} - -/// 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 a6f53c87e..bdd08fe5a 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 { } } +/// 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, + /// The shape's border stroke. + pub stroke: Option, +} + +/// 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 836be1b49..d0c3c75d0 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 000000000..34160b041 --- /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, +} + +impl Rect { + /// Create a new rectangle. + pub fn new(size: Size, radius: Sides) -> 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, + stroke: Sides>, + ) -> Vec { + 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>, + ) -> Vec<(Path, Option)> { + 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 3584a1ce9..43e470d22 100644 --- a/src/geom/sides.rs +++ b/src/geom/sides.rs @@ -31,6 +31,45 @@ impl Sides { bottom: value, } } + + /// Maps the individual fields with `f`. + pub fn map(self, mut f: F) -> Sides + 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(self, other: Sides, mut f: F) -> Sides + 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 { + [&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 Sides @@ -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 28a1af809..40c8e9e4f 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 49c74c2f0..40b6e1e3e 100644 --- a/src/library/graphics/shape.rs +++ b/src/library/graphics/shape.rs @@ -25,9 +25,19 @@ impl ShapeNode { pub const FILL: Option = None; /// How to stroke the shape. #[property(resolve, fold)] - pub const STROKE: Smart> = Smart::Auto; + pub const STROKE: Smart>> = Smart::Auto; + /// How much to pad the shape's content. - pub const PADDING: Relative = Relative::zero(); + #[property(resolve, fold)] + pub const INSET: Sides>> = Sides::splat(Relative::zero()); + + /// 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(resolve, fold)] + pub const RADIUS: Sides>> = Sides::splat(Relative::zero()); fn construct(_: &mut Context, args: &mut Args) -> TypResult { let size = match S { @@ -50,6 +60,30 @@ impl ShapeNode { Self(args.find()?).pack().sized(Spec::new(width, height)), )) } + + 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")?); + } + + 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 Layout for ShapeNode { @@ -61,13 +95,13 @@ impl Layout for ShapeNode { ) -> TypResult>> { 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 Layout for ShapeNode { // 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 shape = Shape { geometry, fill, stroke }; - frame.prepend(Point::zero(), Element::Shape(shape)); + let radius = styles + .get(Self::RADIUS) + .map(|side| side.relative_to(size.x.min(size.y) / 2.0)); + + 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 4307d2f92..c8495e646 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> = Smart::Auto; - /// The right margin. - pub const RIGHT: Smart> = Smart::Auto; - /// The top margin. - pub const TOP: Smart> = Smart::Auto; - /// The bottom margin. - pub const BOTTOM: Smart> = Smart::Auto; + /// The page margin. + #[property(fold)] + pub const MARGINS: Sides>>> = + 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 511542862..3b82ddc28 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 eb7a70537..ae4c1586e 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 Fold for Sides +where + T: Fold, +{ + type Output = Sides; + + fn fold(self, outer: Self::Output) -> Self::Output { + self.zip(outer, |inner, outer, _| inner.fold(outer)) + } +} + +impl Fold for Sides>> { + type Output = Sides>; + + fn fold(self, outer: Self::Output) -> Self::Output { + self.zip(outer, |inner, outer, _| inner.unwrap_or(outer)) + } +} + +impl Fold for Sides>>> { + type Output = Sides>>; + + 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 740f005f3..296fc14ee 100644 Binary files a/tests/ref/graphics/shape-ellipse.png and b/tests/ref/graphics/shape-ellipse.png differ diff --git a/tests/ref/graphics/shape-fill-stroke.png b/tests/ref/graphics/shape-fill-stroke.png index f2278c887..91cddcc2e 100644 Binary files a/tests/ref/graphics/shape-fill-stroke.png and b/tests/ref/graphics/shape-fill-stroke.png differ diff --git a/tests/ref/graphics/shape-rect.png b/tests/ref/graphics/shape-rect.png index 1fdb0dac9..5bbaf3db7 100644 Binary files a/tests/ref/graphics/shape-rect.png and b/tests/ref/graphics/shape-rect.png differ diff --git a/tests/typ/code/let.typ b/tests/typ/code/let.typ index a95d651aa..c3be64a5d 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 6c3215920..b0a3fbf34 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 970857b66..70d689f7d 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 dc1e3f242..13ff67de1 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 995eabb9b..ba4d0d0ab 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 c09cb065c..d14d0981b 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 e035fc91d..a29550b52 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 c4ece7786..622fa9c82 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 ce291fb2f..1e77e6bc7 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 44126d2da..290c40819 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 9fd193c62..6e8e3d853 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 89d0f2fbd..3157ebf93 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 f01b534b4..890c4b94f 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 9f84a733a..b0452163b 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.