typst_library/visualize/
shape.rs

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