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>, /// 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, /// 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>>>, /// 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>>, /// 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>>, /// 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>>, /// 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, } impl Show for Packed { fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { 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, /// 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::>("size")?.map(|s| s.map(Rel::from)); match size { None => args.named("width")?, size => size, } )] pub width: Smart>, /// 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, /// How to stroke the square. See the /// [rectangle's documentation]($rect.stroke) for more details. #[resolve] #[fold] pub stroke: Smart>>>, /// How much to round the square's corners. See the /// [rectangle's documentation]($rect.radius) for more details. #[resolve] #[fold] pub radius: Corners>>, /// 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>>, /// 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>>, /// 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, } impl Show for Packed { fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { 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>, /// 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, /// How to stroke the ellipse. See the /// [rectangle's documentation]($rect.stroke) for more details. #[resolve] #[fold] pub stroke: Smart>, /// 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>>, /// 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>>, /// 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, } impl Show for Packed { fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { 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::>("radius")? .map(|s| s.map(|r| 2.0 * Rel::from(r))); match size { None => args.named("width")?, size => size, } )] pub width: Smart>, /// 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, /// How to stroke the circle. See the /// [rectangle's documentation]($rect.stroke) for more details. #[resolve] #[fold] #[default(Smart::Auto)] pub stroke: Smart>, /// 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>>, /// 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>>, /// 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, } impl Show for Packed { fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { 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, /// The shape's fill rule. pub fill_rule: FillRule, /// The shape's border stroke. pub stroke: Option, } /// 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) -> 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(), } } }