summaryrefslogtreecommitdiff
path: root/crates/typst-library/src/visualize/shape.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/typst-library/src/visualize/shape.rs')
-rw-r--r--crates/typst-library/src/visualize/shape.rs448
1 files changed, 448 insertions, 0 deletions
diff --git a/crates/typst-library/src/visualize/shape.rs b/crates/typst-library/src/visualize/shape.rs
new file mode 100644
index 00000000..01e316a8
--- /dev/null
+++ b/crates/typst-library/src/visualize/shape.rs
@@ -0,0 +1,448 @@
+use crate::diag::SourceResult;
+use crate::engine::Engine;
+use crate::foundations::{
+ elem, Cast, Content, NativeElement, Packed, Show, Smart, StyleChain,
+};
+use crate::layout::{Abs, BlockElem, Corners, Length, Point, Rel, Sides, Size, Sizing};
+use crate::visualize::{FixedStroke, Paint, Path, Stroke};
+
+/// A rectangle with optional content.
+///
+/// # Example
+/// ```example
+/// // Without content.
+/// #rect(width: 35%, height: 30pt)
+///
+/// // With content.
+/// #rect[
+/// Automatically sized \
+/// to fit the content.
+/// ]
+/// ```
+#[elem(title = "Rectangle", Show)]
+pub struct RectElem {
+ /// The rectangle's width, relative to its parent container.
+ pub width: Smart<Rel<Length>>,
+
+ /// The rectangle's height, relative to its parent container.
+ pub height: Sizing,
+
+ /// How to fill the rectangle.
+ ///
+ /// When setting a fill, the default stroke disappears. To create a
+ /// rectangle with both fill and stroke, you have to configure both.
+ ///
+ /// ```example
+ /// #rect(fill: blue)
+ /// ```
+ pub fill: Option<Paint>,
+
+ /// How to stroke the rectangle. This can be:
+ ///
+ /// - `{none}` to disable stroking
+ /// - `{auto}` for a stroke of `{1pt + black}` if and if only if no fill is
+ /// given.
+ /// - Any kind of [stroke]
+ /// - A dictionary describing the stroke for each side individually. The
+ /// dictionary can contain the following keys in order of precedence:
+ /// - `top`: The top stroke.
+ /// - `right`: The right stroke.
+ /// - `bottom`: The bottom stroke.
+ /// - `left`: The left stroke.
+ /// - `x`: The horizontal stroke.
+ /// - `y`: The vertical stroke.
+ /// - `rest`: The stroke on all sides except those for which the
+ /// dictionary explicitly sets a size.
+ ///
+ /// ```example
+ /// #stack(
+ /// dir: ltr,
+ /// spacing: 1fr,
+ /// rect(stroke: red),
+ /// rect(stroke: 2pt),
+ /// rect(stroke: 2pt + red),
+ /// )
+ /// ```
+ #[resolve]
+ #[fold]
+ pub stroke: Smart<Sides<Option<Option<Stroke>>>>,
+
+ /// How much to round the rectangle's corners, relative to the minimum of
+ /// the width and height divided by two. This can be:
+ ///
+ /// - A relative length for a uniform corner radius.
+ /// - A dictionary: With a dictionary, the stroke for each side can be set
+ /// individually. The dictionary can contain the following keys in order
+ /// of precedence:
+ /// - `top-left`: The top-left corner radius.
+ /// - `top-right`: The top-right corner radius.
+ /// - `bottom-right`: The bottom-right corner radius.
+ /// - `bottom-left`: The bottom-left corner radius.
+ /// - `left`: The top-left and bottom-left corner radii.
+ /// - `top`: The top-left and top-right corner radii.
+ /// - `right`: The top-right and bottom-right corner radii.
+ /// - `bottom`: The bottom-left and bottom-right corner radii.
+ /// - `rest`: The radii for all corners except those for which the
+ /// dictionary explicitly sets a size.
+ ///
+ /// ```example
+ /// #set rect(stroke: 4pt)
+ /// #rect(
+ /// radius: (
+ /// left: 5pt,
+ /// top-right: 20pt,
+ /// bottom-right: 10pt,
+ /// ),
+ /// stroke: (
+ /// left: red,
+ /// top: yellow,
+ /// right: green,
+ /// bottom: blue,
+ /// ),
+ /// )
+ /// ```
+ #[resolve]
+ #[fold]
+ pub radius: Corners<Option<Rel<Length>>>,
+
+ /// How much to pad the rectangle's content.
+ /// See the [box's documentation]($box.outset) for more details.
+ #[resolve]
+ #[fold]
+ #[default(Sides::splat(Some(Abs::pt(5.0).into())))]
+ pub inset: Sides<Option<Rel<Length>>>,
+
+ /// How much to expand the rectangle's size without affecting the layout.
+ /// See the [box's documentation]($box.outset) for more details.
+ #[resolve]
+ #[fold]
+ pub outset: Sides<Option<Rel<Length>>>,
+
+ /// The content to place into the rectangle.
+ ///
+ /// When this is omitted, the rectangle takes on a default size of at most
+ /// `{45pt}` by `{30pt}`.
+ #[positional]
+ #[borrowed]
+ pub body: Option<Content>,
+}
+
+impl Show for Packed<RectElem> {
+ fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
+ Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_rect)
+ .with_width(self.width(styles))
+ .with_height(self.height(styles))
+ .pack()
+ .spanned(self.span()))
+ }
+}
+
+/// A square with optional content.
+///
+/// # Example
+/// ```example
+/// // Without content.
+/// #square(size: 40pt)
+///
+/// // With content.
+/// #square[
+/// Automatically \
+/// sized to fit.
+/// ]
+/// ```
+#[elem(Show)]
+pub struct SquareElem {
+ /// The square's side length. This is mutually exclusive with `width` and
+ /// `height`.
+ #[external]
+ pub size: Smart<Length>,
+
+ /// The square's width. This is mutually exclusive with `size` and `height`.
+ ///
+ /// In contrast to `size`, this can be relative to the parent container's
+ /// width.
+ #[parse(
+ let size = args.named::<Smart<Length>>("size")?.map(|s| s.map(Rel::from));
+ match size {
+ None => args.named("width")?,
+ size => size,
+ }
+ )]
+ pub width: Smart<Rel<Length>>,
+
+ /// The square's height. This is mutually exclusive with `size` and `width`.
+ ///
+ /// In contrast to `size`, this can be relative to the parent container's
+ /// height.
+ #[parse(match size {
+ None => args.named("height")?,
+ size => size.map(Into::into),
+ })]
+ pub height: Sizing,
+
+ /// How to fill the square. See the [rectangle's documentation]($rect.fill)
+ /// for more details.
+ pub fill: Option<Paint>,
+
+ /// How to stroke the square. See the
+ /// [rectangle's documentation]($rect.stroke) for more details.
+ #[resolve]
+ #[fold]
+ pub stroke: Smart<Sides<Option<Option<Stroke>>>>,
+
+ /// How much to round the square's corners. See the
+ /// [rectangle's documentation]($rect.radius) for more details.
+ #[resolve]
+ #[fold]
+ pub radius: Corners<Option<Rel<Length>>>,
+
+ /// How much to pad the square's content. See the
+ /// [box's documentation]($box.inset) for more details.
+ #[resolve]
+ #[fold]
+ #[default(Sides::splat(Some(Abs::pt(5.0).into())))]
+ pub inset: Sides<Option<Rel<Length>>>,
+
+ /// How much to expand the square's size without affecting the layout. See
+ /// the [box's documentation]($box.outset) for more details.
+ #[resolve]
+ #[fold]
+ pub outset: Sides<Option<Rel<Length>>>,
+
+ /// The content to place into the square. The square expands to fit this
+ /// content, keeping the 1-1 aspect ratio.
+ ///
+ /// When this is omitted, the square takes on a default size of at most
+ /// `{30pt}`.
+ #[positional]
+ #[borrowed]
+ pub body: Option<Content>,
+}
+
+impl Show for Packed<SquareElem> {
+ fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
+ Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_square)
+ .with_width(self.width(styles))
+ .with_height(self.height(styles))
+ .pack()
+ .spanned(self.span()))
+ }
+}
+
+/// An ellipse with optional content.
+///
+/// # Example
+/// ```example
+/// // Without content.
+/// #ellipse(width: 35%, height: 30pt)
+///
+/// // With content.
+/// #ellipse[
+/// #set align(center)
+/// Automatically sized \
+/// to fit the content.
+/// ]
+/// ```
+#[elem(Show)]
+pub struct EllipseElem {
+ /// The ellipse's width, relative to its parent container.
+ pub width: Smart<Rel<Length>>,
+
+ /// The ellipse's height, relative to its parent container.
+ pub height: Sizing,
+
+ /// How to fill the ellipse. See the [rectangle's documentation]($rect.fill)
+ /// for more details.
+ pub fill: Option<Paint>,
+
+ /// How to stroke the ellipse. See the
+ /// [rectangle's documentation]($rect.stroke) for more details.
+ #[resolve]
+ #[fold]
+ pub stroke: Smart<Option<Stroke>>,
+
+ /// How much to pad the ellipse's content. See the
+ /// [box's documentation]($box.inset) for more details.
+ #[resolve]
+ #[fold]
+ #[default(Sides::splat(Some(Abs::pt(5.0).into())))]
+ pub inset: Sides<Option<Rel<Length>>>,
+
+ /// How much to expand the ellipse's size without affecting the layout. See
+ /// the [box's documentation]($box.outset) for more details.
+ #[resolve]
+ #[fold]
+ pub outset: Sides<Option<Rel<Length>>>,
+
+ /// The content to place into the ellipse.
+ ///
+ /// When this is omitted, the ellipse takes on a default size of at most
+ /// `{45pt}` by `{30pt}`.
+ #[positional]
+ #[borrowed]
+ pub body: Option<Content>,
+}
+
+impl Show for Packed<EllipseElem> {
+ fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
+ Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_ellipse)
+ .with_width(self.width(styles))
+ .with_height(self.height(styles))
+ .pack()
+ .spanned(self.span()))
+ }
+}
+
+/// A circle with optional content.
+///
+/// # Example
+/// ```example
+/// // Without content.
+/// #circle(radius: 25pt)
+///
+/// // With content.
+/// #circle[
+/// #set align(center + horizon)
+/// Automatically \
+/// sized to fit.
+/// ]
+/// ```
+#[elem(Show)]
+pub struct CircleElem {
+ /// The circle's radius. This is mutually exclusive with `width` and
+ /// `height`.
+ #[external]
+ pub radius: Length,
+
+ /// The circle's width. This is mutually exclusive with `radius` and
+ /// `height`.
+ ///
+ /// In contrast to `radius`, this can be relative to the parent container's
+ /// width.
+ #[parse(
+ let size = args
+ .named::<Smart<Length>>("radius")?
+ .map(|s| s.map(|r| 2.0 * Rel::from(r)));
+ match size {
+ None => args.named("width")?,
+ size => size,
+ }
+ )]
+ pub width: Smart<Rel<Length>>,
+
+ /// The circle's height. This is mutually exclusive with `radius` and
+ /// `width`.
+ ///
+ /// In contrast to `radius`, this can be relative to the parent container's
+ /// height.
+ #[parse(match size {
+ None => args.named("height")?,
+ size => size.map(Into::into),
+ })]
+ pub height: Sizing,
+
+ /// How to fill the circle. See the [rectangle's documentation]($rect.fill)
+ /// for more details.
+ pub fill: Option<Paint>,
+
+ /// How to stroke the circle. See the
+ /// [rectangle's documentation]($rect.stroke) for more details.
+ #[resolve]
+ #[fold]
+ #[default(Smart::Auto)]
+ pub stroke: Smart<Option<Stroke>>,
+
+ /// How much to pad the circle's content. See the
+ /// [box's documentation]($box.inset) for more details.
+ #[resolve]
+ #[fold]
+ #[default(Sides::splat(Some(Abs::pt(5.0).into())))]
+ pub inset: Sides<Option<Rel<Length>>>,
+
+ /// How much to expand the circle's size without affecting the layout. See
+ /// the [box's documentation]($box.outset) for more details.
+ #[resolve]
+ #[fold]
+ pub outset: Sides<Option<Rel<Length>>>,
+
+ /// The content to place into the circle. The circle expands to fit this
+ /// content, keeping the 1-1 aspect ratio.
+ #[positional]
+ #[borrowed]
+ pub body: Option<Content>,
+}
+
+impl Show for Packed<CircleElem> {
+ fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
+ Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_circle)
+ .with_width(self.width(styles))
+ .with_height(self.height(styles))
+ .pack()
+ .spanned(self.span()))
+ }
+}
+
+/// A geometric shape with optional fill and stroke.
+#[derive(Debug, Clone, Eq, PartialEq, Hash)]
+pub struct Shape {
+ /// The shape's geometry.
+ pub geometry: Geometry,
+ /// The shape's background fill.
+ pub fill: Option<Paint>,
+ /// The shape's fill rule.
+ pub fill_rule: FillRule,
+ /// The shape's border stroke.
+ pub stroke: Option<FixedStroke>,
+}
+
+/// A path filling rule.
+#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Cast)]
+pub enum FillRule {
+ /// Specifies that "inside" is computed by a non-zero sum of signed edge crossings.
+ #[default]
+ NonZero,
+ /// Specifies that "inside" is computed by an odd number of edge crossings.
+ EvenOdd,
+}
+
+/// A shape's geometry.
+#[derive(Debug, Clone, Eq, PartialEq, Hash)]
+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 bezier path.
+ Path(Path),
+}
+
+impl Geometry {
+ /// Fill the geometry without a stroke.
+ pub fn filled(self, fill: impl Into<Paint>) -> Shape {
+ Shape {
+ geometry: self,
+ fill: Some(fill.into()),
+ fill_rule: FillRule::default(),
+ stroke: None,
+ }
+ }
+
+ /// Stroke the geometry without a fill.
+ pub fn stroked(self, stroke: FixedStroke) -> Shape {
+ Shape {
+ geometry: self,
+ fill: None,
+ fill_rule: FillRule::default(),
+ stroke: Some(stroke),
+ }
+ }
+
+ /// The bounding box of the geometry.
+ pub fn bbox_size(&self) -> Size {
+ match self {
+ Self::Line(line) => Size::new(line.x, line.y),
+ Self::Rect(s) => *s,
+ Self::Path(p) => p.bbox_size(),
+ }
+ }
+}