typst_library/visualize/
tiling.rs

1use std::hash::Hash;
2use std::sync::Arc;
3
4use ecow::{eco_format, EcoString};
5use typst_syntax::{Span, Spanned};
6use typst_utils::{LazyHash, Numeric};
7
8use crate::diag::{bail, SourceResult};
9use crate::engine::Engine;
10use crate::foundations::{func, repr, scope, ty, Content, Smart, StyleChain};
11use crate::introspection::Locator;
12use crate::layout::{Abs, Axes, Frame, Length, Region, Size};
13use crate::visualize::RelativeTo;
14use crate::World;
15
16/// A repeating tiling fill.
17///
18/// Typst supports the most common type of tilings, where a pattern is repeated
19/// in a grid-like fashion, covering the entire area of an element that is
20/// filled or stroked. The pattern is defined by a tile size and a body defining
21/// the content of each cell. You can also add horizontal or vertical spacing
22/// between the cells of the tiling.
23///
24/// # Examples
25///
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 are also supported on text, but only when setting the
36/// [relativeness]($tiling.relative) to either `{auto}` (the default value) or
37/// `{"parent"}`. To create word-by-word or glyph-by-glyph tilings, you can
38/// wrap the words or characters of your text in [boxes]($box) manually or
39/// through a [show rule]($styling/#show-rules).
40///
41/// ```example
42/// #let pat = tiling(
43///   size: (30pt, 30pt),
44///   relative: "parent",
45///   square(
46///     size: 30pt,
47///     fill: gradient
48///       .conic(..color.map.rainbow),
49///   )
50/// )
51///
52/// #set text(fill: pat)
53/// #lorem(10)
54/// ```
55///
56/// You can also space the elements further or closer apart using the
57/// [`spacing`]($tiling.spacing) feature of the tiling. If the spacing
58/// is lower than the size of the tiling, the tiling will overlap.
59/// If it is higher, the tiling will have gaps of the same color as the
60/// background of the tiling.
61///
62/// ```example
63/// #let pat = tiling(
64///   size: (30pt, 30pt),
65///   spacing: (10pt, 10pt),
66///   relative: "parent",
67///   square(
68///     size: 30pt,
69///     fill: gradient
70///      .conic(..color.map.rainbow),
71///   ),
72/// )
73///
74/// #rect(
75///   width: 100%,
76///   height: 60pt,
77///   fill: pat,
78/// )
79/// ```
80///
81/// # Relativeness
82/// The location of the starting point of the tiling is dependent on the
83/// dimensions of a container. This container can either be the shape that it is
84/// being painted on, or the closest surrounding container. This is controlled
85/// by the `relative` argument of a tiling constructor. By default, tilings
86/// are relative to the shape they are being painted on, unless the tiling is
87/// applied on text, in which case they are relative to the closest ancestor
88/// container.
89///
90/// Typst determines the ancestor container as follows:
91/// - For shapes that are placed at the root/top level of the document, the
92///   closest ancestor is the page itself.
93/// - For other shapes, the ancestor is the innermost [`block`] or [`box`] that
94///   contains the shape. This includes the boxes and blocks that are implicitly
95///   created by show rules and elements. For example, a [`rotate`] will not
96///   affect the parent of a gradient, but a [`grid`] will.
97///
98/// # Compatibility
99/// This type used to be called `pattern`. The name remains as an alias, but is
100/// deprecated since Typst 0.13.
101#[ty(scope, cast, keywords = ["pattern"])]
102#[derive(Debug, Clone, Eq, PartialEq, Hash)]
103pub struct Tiling(Arc<Repr>);
104
105/// Internal representation of [`Tiling`].
106#[derive(Debug, Clone, Eq, PartialEq, Hash)]
107struct Repr {
108    /// The tiling's rendered content.
109    frame: LazyHash<Frame>,
110    /// The tiling's tile size.
111    size: Size,
112    /// The tiling's tile spacing.
113    spacing: Size,
114    /// The tiling's relative transform.
115    relative: Smart<RelativeTo>,
116}
117
118#[scope]
119impl Tiling {
120    /// Construct a new tiling.
121    ///
122    /// ```example
123    /// #let pat = tiling(
124    ///   size: (20pt, 20pt),
125    ///   relative: "parent",
126    ///   place(
127    ///     dx: 5pt,
128    ///     dy: 5pt,
129    ///     rotate(45deg, square(
130    ///       size: 5pt,
131    ///       fill: black,
132    ///     )),
133    ///   ),
134    /// )
135    ///
136    /// #rect(width: 100%, height: 60pt, fill: pat)
137    /// ```
138    #[func(constructor)]
139    pub fn construct(
140        engine: &mut Engine,
141        span: Span,
142        /// The bounding box of each cell of the tiling.
143        #[named]
144        #[default(Spanned::new(Smart::Auto, Span::detached()))]
145        size: Spanned<Smart<Axes<Length>>>,
146        /// The spacing between cells of the tiling.
147        #[named]
148        #[default(Spanned::new(Axes::splat(Length::zero()), Span::detached()))]
149        spacing: Spanned<Axes<Length>>,
150        /// The [relative placement](#relativeness) of the tiling.
151        ///
152        /// For an element placed at the root/top level of the document, the
153        /// parent is the page itself. For other elements, the parent is the
154        /// innermost block, box, column, grid, or stack that contains the
155        /// element.
156        #[named]
157        #[default(Smart::Auto)]
158        relative: Smart<RelativeTo>,
159        /// The content of each cell of the tiling.
160        body: Content,
161    ) -> SourceResult<Tiling> {
162        let size_span = size.span;
163        if let Smart::Custom(size) = size.v {
164            // Ensure that sizes are absolute.
165            if !size.x.em.is_zero() || !size.y.em.is_zero() {
166                bail!(size_span, "tile size must be absolute");
167            }
168
169            // Ensure that sizes are non-zero and finite.
170            if size.x.is_zero()
171                || size.y.is_zero()
172                || !size.x.is_finite()
173                || !size.y.is_finite()
174            {
175                bail!(size_span, "tile size must be non-zero and non-infinite");
176            }
177        }
178
179        // Ensure that spacing is absolute.
180        if !spacing.v.x.em.is_zero() || !spacing.v.y.em.is_zero() {
181            bail!(spacing.span, "tile spacing must be absolute");
182        }
183
184        // Ensure that spacing is finite.
185        if !spacing.v.x.is_finite() || !spacing.v.y.is_finite() {
186            bail!(spacing.span, "tile spacing must be finite");
187        }
188
189        // The size of the frame
190        let size = size.v.map(|l| l.map(|a| a.abs));
191        let region = size.unwrap_or_else(|| Axes::splat(Abs::inf()));
192
193        // Layout the tiling.
194        let world = engine.world;
195        let library = world.library();
196        let locator = Locator::root();
197        let styles = StyleChain::new(&library.styles);
198        let pod = Region::new(region, Axes::splat(false));
199        let mut frame =
200            (engine.routines.layout_frame)(engine, &body, locator, styles, pod)?;
201
202        // Set the size of the frame if the size is enforced.
203        if let Smart::Custom(size) = size {
204            frame.set_size(size);
205        }
206
207        // Check that the frame is non-zero.
208        if frame.width().is_zero() || frame.height().is_zero() {
209            bail!(
210                span, "tile size must be non-zero";
211                hint: "try setting the size manually"
212            );
213        }
214
215        Ok(Self(Arc::new(Repr {
216            size: frame.size(),
217            frame: LazyHash::new(frame),
218            spacing: spacing.v.map(|l| l.abs),
219            relative,
220        })))
221    }
222}
223
224impl Tiling {
225    /// Set the relative placement of the tiling.
226    pub fn with_relative(mut self, relative: RelativeTo) -> Self {
227        if let Some(this) = Arc::get_mut(&mut self.0) {
228            this.relative = Smart::Custom(relative);
229        } else {
230            self.0 = Arc::new(Repr {
231                relative: Smart::Custom(relative),
232                ..self.0.as_ref().clone()
233            });
234        }
235
236        self
237    }
238
239    /// Return the frame of the tiling.
240    pub fn frame(&self) -> &Frame {
241        &self.0.frame
242    }
243
244    /// Return the size of the tiling in absolute units.
245    pub fn size(&self) -> Size {
246        self.0.size
247    }
248
249    /// Return the spacing of the tiling in absolute units.
250    pub fn spacing(&self) -> Size {
251        self.0.spacing
252    }
253
254    /// Returns the relative placement of the tiling.
255    pub fn relative(&self) -> Smart<RelativeTo> {
256        self.0.relative
257    }
258
259    /// Returns the relative placement of the tiling.
260    pub fn unwrap_relative(&self, on_text: bool) -> RelativeTo {
261        self.0.relative.unwrap_or_else(|| {
262            if on_text {
263                RelativeTo::Parent
264            } else {
265                RelativeTo::Self_
266            }
267        })
268    }
269}
270
271impl repr::Repr for Tiling {
272    fn repr(&self) -> EcoString {
273        let mut out =
274            eco_format!("tiling(({}, {})", self.0.size.x.repr(), self.0.size.y.repr());
275
276        if self.0.spacing.is_zero() {
277            out.push_str(", spacing: (");
278            out.push_str(&self.0.spacing.x.repr());
279            out.push_str(", ");
280            out.push_str(&self.0.spacing.y.repr());
281            out.push(')');
282        }
283
284        out.push_str(", ..)");
285
286        out
287    }
288}