Skip to main content

typst_library/visualize/
tiling.rs

1use std::hash::Hash;
2use std::sync::Arc;
3
4use ecow::{EcoString, eco_format};
5use typst_syntax::{Span, Spanned};
6use typst_utils::{LazyHash, Numeric};
7
8use crate::diag::{SourceResult, bail};
9use crate::engine::Engine;
10use crate::foundations::{Content, Repr, Resolve, Smart, StyleChain, func, scope, ty};
11use crate::introspection::Locator;
12use crate::layout::{Abs, Axes, Frame, Length, Region, Rel, Size};
13use crate::visualize::RelativeTo;
14
15/// A repeating tiling fill.
16///
17/// Typst supports the most common type of tilings, where a pattern is repeated
18/// in a grid-like fashion, covering the entire area of an element that is
19/// filled or stroked. The pattern is defined by a tile
20/// @tiling.constructor.size[`size`] and a body defining the content of each
21/// cell. You can also add horizontal or vertical
22/// @tiling.constructor.spacing[`spacing`] between the cells of the tiling and
23/// @tiling.constructor.offset[`offset`] the starting position of the tiling.
24///
25/// = Example <example>
26/// ```example
27/// #let pat = tiling(size: (30pt, 30pt), {
28///   place(line(start: (0%, 0%), end: (100%, 100%)))
29///   place(line(start: (0%, 100%), end: (100%, 0%)))
30/// })
31///
32/// #rect(fill: pat, width: 100%, height: 60pt, stroke: 1pt)
33/// ```
34///
35/// = Tilings on text <tilings-on-text>
36/// Tilings are also supported on text, but only when setting
37/// @tiling.constructor.relative[`relative`] to either `{auto}` (the default
38/// value) or `{"parent"}`. To create word-by-word or glyph-by-glyph tilings,
39/// you can wrap the words or characters of your text in @box[boxes] manually or
40/// through a @reference:styling:show-rules[show rule].
41///
42/// ```example
43/// #let pat = tiling(
44///   size: (30pt, 30pt),
45///   relative: "parent",
46///   square(
47///     size: 30pt,
48///     fill: gradient
49///       .conic(..color.map.rainbow),
50///   )
51/// )
52///
53/// #set text(fill: pat)
54/// #lorem(10)
55/// ```
56#[ty(scope, cast, keywords = ["pattern"])]
57#[derive(Debug, Clone, Eq, PartialEq, Hash)]
58pub struct Tiling(Arc<TilingInner>);
59
60/// The internal representation of a [`Tiling`].
61#[derive(Debug, Clone, Eq, PartialEq, Hash)]
62struct TilingInner {
63    /// The tiling's rendered content.
64    frame: LazyHash<Frame>,
65    /// The tiling's tile size.
66    size: Size,
67    /// The tiling's tile spacing.
68    spacing: Size,
69    /// The tiling's tile offset.
70    offset: Size,
71    /// The tiling's relative transform.
72    relative: Smart<RelativeTo>,
73}
74
75#[scope]
76impl Tiling {
77    /// Construct a new tiling.
78    ///
79    /// ```example
80    /// #let pat = tiling(
81    ///   size: (20pt, 20pt),
82    ///   relative: "parent",
83    ///   place(
84    ///     dx: 5pt,
85    ///     dy: 5pt,
86    ///     rotate(45deg, square(
87    ///       size: 5pt,
88    ///       fill: black,
89    ///     )),
90    ///   ),
91    /// )
92    ///
93    /// #rect(width: 100%, height: 60pt, fill: pat)
94    /// ```
95    #[func(constructor)]
96    pub fn construct(
97        engine: &mut Engine,
98        span: Span,
99        /// The bounding box of each cell of the tiling, specified as a `(x, y)`
100        /// pair.
101        ///
102        /// If set to `{auto}`, the tiling takes on the size of the laid-out
103        /// content.
104        #[named]
105        #[default(Spanned::detached(Smart::Auto))]
106        size: Spanned<Smart<Axes<Length>>>,
107        /// The spacing between cells of the tiling, specified as a `(x, y)`
108        /// pair.
109        ///
110        /// If the spacing is lower than the size of the tiling, the tiling will
111        /// overlap with itself. If it is higher, the tiling will have gaps.
112        ///
113        /// ```example
114        /// >>> #set page(width: 5 * 30pt + 4 * 10pt + 2 * 15pt)
115        /// #let pat = tiling(
116        ///   size: (30pt, 30pt),
117        ///   spacing: (10pt, 20pt),
118        ///   square(size: 30pt, fill: gradient.conic(..color.map.rainbow)),
119        /// )
120        ///
121        /// #rect(
122        ///   width: 100%,
123        ///   height: 80pt,
124        ///   fill: pat,
125        ///   stroke: (thickness: 1pt, dash: "dotted"),
126        /// )
127        /// ```
128        #[named]
129        #[default(Spanned::detached(Axes::splat(Length::zero())))]
130        spacing: Spanned<Axes<Length>>,
131        /// Shifts the entire tile grid without affecting the tile size or
132        /// spacing.
133        ///
134        /// The offset is specified as a `(x, y)` pair. Positive `x` values move
135        /// the pattern to the right and positive `y` values move it down.
136        /// Relative values are resolved against the tile size plus spacing.
137        ///
138        /// Note that the displacement caused by the offset affects the tiles
139        /// themselves while displacement of the inner contents (e.g. via
140        /// `{place(dx: .., dy: ..)}`) can cause clipping when the content
141        /// moves outside of the tile's bounding box.
142        ///
143        /// ```example
144        /// #set rect(width: 100%, height: 80pt, stroke: 1pt)
145        ///
146        /// #let pat = tiling(
147        ///   size: (20pt, 20pt),
148        ///   circle(radius: 10pt, fill: blue),
149        /// )
150        ///
151        /// #let pat-with-offset = tiling(
152        ///   size: (20pt, 20pt),
153        ///   offset: (50%, 50%),
154        ///   circle(radius: 10pt, fill: blue),
155        /// )
156        ///
157        /// #grid(
158        ///   columns: 2,
159        ///   column-gutter: 10pt,
160        ///   rect(fill: pat),
161        ///   rect(fill: pat-with-offset),
162        /// )
163        /// ```
164        #[named]
165        #[default(Spanned::new(Axes::splat(Rel::zero()), Span::detached()))]
166        offset: Spanned<Axes<Rel<Length>>>,
167        /// Determines relative to which element's bounding box the tiling is
168        /// drawn.
169        ///
170        /// By default, tilings are drawn relative to the shape they are being
171        /// painted on (`{"self"}`), unless the tiling is applied on text, in
172        /// which case they are relative to the closest ancestor container
173        /// (`{"parent"}`).
174        ///
175        /// The parent of an element is the innermost @box or @block that
176        /// contains the element, or, if there is none, the page itself.
177        ///
178        /// ```example
179        /// #let pat = tiling(
180        ///   size: (20pt, 20pt),
181        ///   spacing: (5pt, 5pt),
182        ///   relative: "self",
183        ///   circle(radius: 10pt, fill: teal),
184        /// )
185        ///
186        /// #let pat-with-parent = tiling(
187        ///   size: (20pt, 20pt),
188        ///   spacing: (5pt, 5pt),
189        ///   relative: "parent",
190        ///   circle(radius: 10pt, fill: teal),
191        /// )
192        ///
193        /// #set raw(lang: "typc")
194        /// #table(
195        ///   columns: (1fr, 1fr, 1fr),
196        ///   rows: (auto, 80pt),
197        ///   table.header(`"self"`, `"parent"`, `"parent"`),
198        ///
199        ///   // This one is local to the cell itself.
200        ///   table.cell(fill: pat, none),
201        ///
202        ///   // These two are both page-relative, so the
203        ///   // pattern is continous.
204        ///   table.cell(fill: pat-with-parent, none),
205        ///   table.cell(fill: pat-with-parent, none),
206        /// )
207        /// ```
208        #[named]
209        #[default(Smart::Auto)]
210        relative: Smart<RelativeTo>,
211        /// The content of each cell of the tiling.
212        body: Content,
213    ) -> SourceResult<Tiling> {
214        let size_span = size.span;
215        if let Smart::Custom(size) = size.v {
216            // Ensure that sizes are absolute.
217            if !size.x.em.is_zero() || !size.y.em.is_zero() {
218                bail!(size_span, "tile size must be absolute");
219            }
220
221            // Ensure that sizes are non-zero and finite.
222            if size.x.is_zero()
223                || size.y.is_zero()
224                || !size.x.is_finite()
225                || !size.y.is_finite()
226            {
227                bail!(size_span, "tile size must be non-zero and non-infinite");
228            }
229        }
230
231        // Ensure that spacing is absolute.
232        if !spacing.v.x.em.is_zero() || !spacing.v.y.em.is_zero() {
233            bail!(spacing.span, "tile spacing must be absolute");
234        }
235
236        // Ensure that spacing is finite.
237        if !spacing.v.x.is_finite() || !spacing.v.y.is_finite() {
238            bail!(spacing.span, "tile spacing must be finite");
239        }
240
241        // Ensure that offset is not font-relative.
242        if !offset.v.x.abs.em.is_zero() || !offset.v.y.abs.em.is_zero() {
243            bail!(offset.span, "tile offset must not be font-relative");
244        }
245
246        // Ensure that offset is finite.
247        if !offset.v.x.rel.get().is_finite()
248            || !offset.v.x.abs.is_finite()
249            || !offset.v.y.rel.get().is_finite()
250            || !offset.v.y.abs.is_finite()
251        {
252            bail!(offset.span, "tile offset must be finite");
253        }
254
255        // The size of the frame
256        let size = size.v.map(|l| l.map(|a| a.abs));
257        let region = size.unwrap_or_else(|| Axes::splat(Abs::inf()));
258
259        // Layout the tiling.
260        let locator = Locator::root();
261        let styles = StyleChain::new(&engine.library.styles);
262        let pod = Region::new(region, Axes::splat(false));
263        let mut frame =
264            (engine.library.routines.layout_frame)(engine, &body, locator, styles, pod)?;
265
266        // Set the size of the frame if the size is enforced.
267        if let Smart::Custom(size) = size {
268            frame.set_size(size);
269        }
270
271        // Check that the frame is non-zero.
272        if frame.width().is_zero() || frame.height().is_zero() {
273            bail!(
274                span, "tile size must be non-zero";
275                hint: "try setting the size manually";
276            );
277        }
278
279        let size = frame.size();
280        let spacing = spacing.v.map(|l| l.abs);
281        let offset = offset
282            .v
283            .map(|l| l.resolve(styles))
284            .zip_map(size + spacing, Rel::relative_to);
285
286        Ok(Self(Arc::new(TilingInner {
287            size,
288            frame: LazyHash::new(frame),
289            spacing,
290            offset,
291            relative,
292        })))
293    }
294}
295
296impl Tiling {
297    /// Set the relative placement of the tiling.
298    pub fn with_relative(mut self, relative: RelativeTo) -> Self {
299        if let Some(this) = Arc::get_mut(&mut self.0) {
300            this.relative = Smart::Custom(relative);
301        } else {
302            self.0 = Arc::new(TilingInner {
303                relative: Smart::Custom(relative),
304                ..self.0.as_ref().clone()
305            });
306        }
307
308        self
309    }
310
311    /// Return the offset of the tiling in absolute units.
312    pub fn offset(&self) -> Size {
313        self.0.offset
314    }
315
316    /// Return the frame of the tiling.
317    pub fn frame(&self) -> &Frame {
318        &self.0.frame
319    }
320
321    /// Return the size of the tiling in absolute units.
322    pub fn size(&self) -> Size {
323        self.0.size
324    }
325
326    /// Return the spacing of the tiling in absolute units.
327    pub fn spacing(&self) -> Size {
328        self.0.spacing
329    }
330
331    /// Returns the relative placement of the tiling.
332    pub fn relative(&self) -> Smart<RelativeTo> {
333        self.0.relative
334    }
335
336    /// Returns the relative placement of the tiling.
337    pub fn unwrap_relative(&self, on_text: bool) -> RelativeTo {
338        self.0.relative.unwrap_or_else(|| {
339            if on_text { RelativeTo::Parent } else { RelativeTo::Self_ }
340        })
341    }
342}
343
344impl Repr for Tiling {
345    fn repr(&self) -> EcoString {
346        let mut out =
347            eco_format!("tiling(({}, {})", self.0.size.x.repr(), self.0.size.y.repr());
348
349        if !self.0.spacing.is_zero() {
350            out.push_str(", spacing: (");
351            out.push_str(&self.0.spacing.x.repr());
352            out.push_str(", ");
353            out.push_str(&self.0.spacing.y.repr());
354            out.push(')');
355        }
356
357        if !self.0.offset.is_zero() {
358            out.push_str(", offset: (");
359            out.push_str(&self.0.offset.x.repr());
360            out.push_str(", ");
361            out.push_str(&self.0.offset.y.repr());
362            out.push(')');
363        }
364
365        out.push_str(", ..)");
366
367        out
368    }
369}