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}