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}