typst_library/visualize/
shape.rs

1use crate::diag::SourceResult;
2use crate::engine::Engine;
3use crate::foundations::{
4    elem, Cast, Content, NativeElement, Packed, Show, Smart, StyleChain,
5};
6use crate::layout::{Abs, BlockElem, Corners, Length, Point, Rel, Sides, Size, Sizing};
7use crate::visualize::{Curve, FixedStroke, Paint, Stroke};
8
9/// A rectangle with optional content.
10///
11/// # Example
12/// ```example
13/// // Without content.
14/// #rect(width: 35%, height: 30pt)
15///
16/// // With content.
17/// #rect[
18///   Automatically sized \
19///   to fit the content.
20/// ]
21/// ```
22#[elem(title = "Rectangle", Show)]
23pub struct RectElem {
24    /// The rectangle's width, relative to its parent container.
25    pub width: Smart<Rel<Length>>,
26
27    /// The rectangle's height, relative to its parent container.
28    pub height: Sizing,
29
30    /// How to fill the rectangle.
31    ///
32    /// When setting a fill, the default stroke disappears. To create a
33    /// rectangle with both fill and stroke, you have to configure both.
34    ///
35    /// ```example
36    /// #rect(fill: blue)
37    /// ```
38    pub fill: Option<Paint>,
39
40    /// How to stroke the rectangle. This can be:
41    ///
42    /// - `{none}` to disable stroking
43    /// - `{auto}` for a stroke of `{1pt + black}` if and if only if no fill is
44    ///   given.
45    /// - Any kind of [stroke]
46    /// - A dictionary describing the stroke for each side individually. The
47    ///   dictionary can contain the following keys in order of precedence:
48    ///   - `top`: The top stroke.
49    ///   - `right`: The right stroke.
50    ///   - `bottom`: The bottom stroke.
51    ///   - `left`: The left stroke.
52    ///   - `x`: The horizontal stroke.
53    ///   - `y`: The vertical stroke.
54    ///   - `rest`: The stroke on all sides except those for which the
55    ///     dictionary explicitly sets a size.
56    ///
57    /// ```example
58    /// #stack(
59    ///   dir: ltr,
60    ///   spacing: 1fr,
61    ///   rect(stroke: red),
62    ///   rect(stroke: 2pt),
63    ///   rect(stroke: 2pt + red),
64    /// )
65    /// ```
66    #[resolve]
67    #[fold]
68    pub stroke: Smart<Sides<Option<Option<Stroke>>>>,
69
70    /// How much to round the rectangle's corners, relative to the minimum of
71    /// the width and height divided by two. This can be:
72    ///
73    /// - A relative length for a uniform corner radius.
74    /// - A dictionary: With a dictionary, the stroke for each side can be set
75    ///   individually. The dictionary can contain the following keys in order
76    ///   of precedence:
77    ///   - `top-left`: The top-left corner radius.
78    ///   - `top-right`: The top-right corner radius.
79    ///   - `bottom-right`: The bottom-right corner radius.
80    ///   - `bottom-left`: The bottom-left corner radius.
81    ///   - `left`: The top-left and bottom-left corner radii.
82    ///   - `top`: The top-left and top-right corner radii.
83    ///   - `right`: The top-right and bottom-right corner radii.
84    ///   - `bottom`: The bottom-left and bottom-right corner radii.
85    ///   - `rest`: The radii for all corners except those for which the
86    ///     dictionary explicitly sets a size.
87    ///
88    /// ```example
89    /// #set rect(stroke: 4pt)
90    /// #rect(
91    ///   radius: (
92    ///     left: 5pt,
93    ///     top-right: 20pt,
94    ///     bottom-right: 10pt,
95    ///   ),
96    ///   stroke: (
97    ///     left: red,
98    ///     top: yellow,
99    ///     right: green,
100    ///     bottom: blue,
101    ///   ),
102    /// )
103    /// ```
104    #[resolve]
105    #[fold]
106    pub radius: Corners<Option<Rel<Length>>>,
107
108    /// How much to pad the rectangle's content.
109    /// See the [box's documentation]($box.outset) for more details.
110    #[resolve]
111    #[fold]
112    #[default(Sides::splat(Some(Abs::pt(5.0).into())))]
113    pub inset: Sides<Option<Rel<Length>>>,
114
115    /// How much to expand the rectangle's size without affecting the layout.
116    /// See the [box's documentation]($box.outset) for more details.
117    #[resolve]
118    #[fold]
119    pub outset: Sides<Option<Rel<Length>>>,
120
121    /// The content to place into the rectangle.
122    ///
123    /// When this is omitted, the rectangle takes on a default size of at most
124    /// `{45pt}` by `{30pt}`.
125    #[positional]
126    #[borrowed]
127    pub body: Option<Content>,
128}
129
130impl Show for Packed<RectElem> {
131    fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
132        Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_rect)
133            .with_width(self.width(styles))
134            .with_height(self.height(styles))
135            .pack()
136            .spanned(self.span()))
137    }
138}
139
140/// A square with optional content.
141///
142/// # Example
143/// ```example
144/// // Without content.
145/// #square(size: 40pt)
146///
147/// // With content.
148/// #square[
149///   Automatically \
150///   sized to fit.
151/// ]
152/// ```
153#[elem(Show)]
154pub struct SquareElem {
155    /// The square's side length. This is mutually exclusive with `width` and
156    /// `height`.
157    #[external]
158    pub size: Smart<Length>,
159
160    /// The square's width. This is mutually exclusive with `size` and `height`.
161    ///
162    /// In contrast to `size`, this can be relative to the parent container's
163    /// width.
164    #[parse(
165        let size = args.named::<Smart<Length>>("size")?.map(|s| s.map(Rel::from));
166        match size {
167            None => args.named("width")?,
168            size => size,
169        }
170    )]
171    pub width: Smart<Rel<Length>>,
172
173    /// The square's height. This is mutually exclusive with `size` and `width`.
174    ///
175    /// In contrast to `size`, this can be relative to the parent container's
176    /// height.
177    #[parse(match size {
178        None => args.named("height")?,
179        size => size.map(Into::into),
180    })]
181    pub height: Sizing,
182
183    /// How to fill the square. See the [rectangle's documentation]($rect.fill)
184    /// for more details.
185    pub fill: Option<Paint>,
186
187    /// How to stroke the square. See the
188    /// [rectangle's documentation]($rect.stroke) for more details.
189    #[resolve]
190    #[fold]
191    pub stroke: Smart<Sides<Option<Option<Stroke>>>>,
192
193    /// How much to round the square's corners. See the
194    /// [rectangle's documentation]($rect.radius) for more details.
195    #[resolve]
196    #[fold]
197    pub radius: Corners<Option<Rel<Length>>>,
198
199    /// How much to pad the square's content. See the
200    /// [box's documentation]($box.inset) for more details.
201    #[resolve]
202    #[fold]
203    #[default(Sides::splat(Some(Abs::pt(5.0).into())))]
204    pub inset: Sides<Option<Rel<Length>>>,
205
206    /// How much to expand the square's size without affecting the layout. See
207    /// the [box's documentation]($box.outset) for more details.
208    #[resolve]
209    #[fold]
210    pub outset: Sides<Option<Rel<Length>>>,
211
212    /// The content to place into the square. The square expands to fit this
213    /// content, keeping the 1-1 aspect ratio.
214    ///
215    /// When this is omitted, the square takes on a default size of at most
216    /// `{30pt}`.
217    #[positional]
218    #[borrowed]
219    pub body: Option<Content>,
220}
221
222impl Show for Packed<SquareElem> {
223    fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
224        Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_square)
225            .with_width(self.width(styles))
226            .with_height(self.height(styles))
227            .pack()
228            .spanned(self.span()))
229    }
230}
231
232/// An ellipse with optional content.
233///
234/// # Example
235/// ```example
236/// // Without content.
237/// #ellipse(width: 35%, height: 30pt)
238///
239/// // With content.
240/// #ellipse[
241///   #set align(center)
242///   Automatically sized \
243///   to fit the content.
244/// ]
245/// ```
246#[elem(Show)]
247pub struct EllipseElem {
248    /// The ellipse's width, relative to its parent container.
249    pub width: Smart<Rel<Length>>,
250
251    /// The ellipse's height, relative to its parent container.
252    pub height: Sizing,
253
254    /// How to fill the ellipse. See the [rectangle's documentation]($rect.fill)
255    /// for more details.
256    pub fill: Option<Paint>,
257
258    /// How to stroke the ellipse. See the
259    /// [rectangle's documentation]($rect.stroke) for more details.
260    #[resolve]
261    #[fold]
262    pub stroke: Smart<Option<Stroke>>,
263
264    /// How much to pad the ellipse's content. See the
265    /// [box's documentation]($box.inset) for more details.
266    #[resolve]
267    #[fold]
268    #[default(Sides::splat(Some(Abs::pt(5.0).into())))]
269    pub inset: Sides<Option<Rel<Length>>>,
270
271    /// How much to expand the ellipse's size without affecting the layout. See
272    /// the [box's documentation]($box.outset) for more details.
273    #[resolve]
274    #[fold]
275    pub outset: Sides<Option<Rel<Length>>>,
276
277    /// The content to place into the ellipse.
278    ///
279    /// When this is omitted, the ellipse takes on a default size of at most
280    /// `{45pt}` by `{30pt}`.
281    #[positional]
282    #[borrowed]
283    pub body: Option<Content>,
284}
285
286impl Show for Packed<EllipseElem> {
287    fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
288        Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_ellipse)
289            .with_width(self.width(styles))
290            .with_height(self.height(styles))
291            .pack()
292            .spanned(self.span()))
293    }
294}
295
296/// A circle with optional content.
297///
298/// # Example
299/// ```example
300/// // Without content.
301/// #circle(radius: 25pt)
302///
303/// // With content.
304/// #circle[
305///   #set align(center + horizon)
306///   Automatically \
307///   sized to fit.
308/// ]
309/// ```
310#[elem(Show)]
311pub struct CircleElem {
312    /// The circle's radius. This is mutually exclusive with `width` and
313    /// `height`.
314    #[external]
315    pub radius: Length,
316
317    /// The circle's width. This is mutually exclusive with `radius` and
318    /// `height`.
319    ///
320    /// In contrast to `radius`, this can be relative to the parent container's
321    /// width.
322    #[parse(
323        let size = args
324            .named::<Smart<Length>>("radius")?
325            .map(|s| s.map(|r| 2.0 * Rel::from(r)));
326        match size {
327            None => args.named("width")?,
328            size => size,
329        }
330    )]
331    pub width: Smart<Rel<Length>>,
332
333    /// The circle's height. This is mutually exclusive with `radius` and
334    /// `width`.
335    ///
336    /// In contrast to `radius`, this can be relative to the parent container's
337    /// height.
338    #[parse(match size {
339        None => args.named("height")?,
340        size => size.map(Into::into),
341    })]
342    pub height: Sizing,
343
344    /// How to fill the circle. See the [rectangle's documentation]($rect.fill)
345    /// for more details.
346    pub fill: Option<Paint>,
347
348    /// How to stroke the circle. See the
349    /// [rectangle's documentation]($rect.stroke) for more details.
350    #[resolve]
351    #[fold]
352    #[default(Smart::Auto)]
353    pub stroke: Smart<Option<Stroke>>,
354
355    /// How much to pad the circle's content. See the
356    /// [box's documentation]($box.inset) for more details.
357    #[resolve]
358    #[fold]
359    #[default(Sides::splat(Some(Abs::pt(5.0).into())))]
360    pub inset: Sides<Option<Rel<Length>>>,
361
362    /// How much to expand the circle's size without affecting the layout. See
363    /// the [box's documentation]($box.outset) for more details.
364    #[resolve]
365    #[fold]
366    pub outset: Sides<Option<Rel<Length>>>,
367
368    /// The content to place into the circle. The circle expands to fit this
369    /// content, keeping the 1-1 aspect ratio.
370    #[positional]
371    #[borrowed]
372    pub body: Option<Content>,
373}
374
375impl Show for Packed<CircleElem> {
376    fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
377        Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_circle)
378            .with_width(self.width(styles))
379            .with_height(self.height(styles))
380            .pack()
381            .spanned(self.span()))
382    }
383}
384
385/// A geometric shape with optional fill and stroke.
386#[derive(Debug, Clone, Eq, PartialEq, Hash)]
387pub struct Shape {
388    /// The shape's geometry.
389    pub geometry: Geometry,
390    /// The shape's background fill.
391    pub fill: Option<Paint>,
392    /// The shape's fill rule.
393    pub fill_rule: FillRule,
394    /// The shape's border stroke.
395    pub stroke: Option<FixedStroke>,
396}
397
398/// A fill rule for curve drawing.
399#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Cast)]
400pub enum FillRule {
401    /// Specifies that "inside" is computed by a non-zero sum of signed edge crossings.
402    #[default]
403    NonZero,
404    /// Specifies that "inside" is computed by an odd number of edge crossings.
405    EvenOdd,
406}
407
408/// A shape's geometry.
409#[derive(Debug, Clone, Eq, PartialEq, Hash)]
410pub enum Geometry {
411    /// A line to a point (relative to its position).
412    Line(Point),
413    /// A rectangle with its origin in the topleft corner.
414    Rect(Size),
415    /// A curve consisting of movements, lines, and Bézier segments.
416    Curve(Curve),
417}
418
419impl Geometry {
420    /// Fill the geometry without a stroke.
421    pub fn filled(self, fill: impl Into<Paint>) -> Shape {
422        Shape {
423            geometry: self,
424            fill: Some(fill.into()),
425            fill_rule: FillRule::default(),
426            stroke: None,
427        }
428    }
429
430    /// Stroke the geometry without a fill.
431    pub fn stroked(self, stroke: FixedStroke) -> Shape {
432        Shape {
433            geometry: self,
434            fill: None,
435            fill_rule: FillRule::default(),
436            stroke: Some(stroke),
437        }
438    }
439
440    /// The bounding box of the geometry.
441    pub fn bbox_size(&self) -> Size {
442        match self {
443            Self::Line(line) => Size::new(line.x, line.y),
444            Self::Rect(rect) => *rect,
445            Self::Curve(curve) => curve.bbox_size(),
446        }
447    }
448}