typst_library/visualize/
tiling.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
use std::hash::Hash;
use std::sync::Arc;

use ecow::{eco_format, EcoString};
use typst_syntax::{Span, Spanned};
use typst_utils::{LazyHash, Numeric};

use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
use crate::foundations::{func, repr, scope, ty, Content, Smart, StyleChain};
use crate::introspection::Locator;
use crate::layout::{Abs, Axes, Frame, Length, Region, Size};
use crate::visualize::RelativeTo;
use crate::World;

/// A repeating tiling fill.
///
/// Typst supports the most common type of tilings, where a pattern is repeated
/// in a grid-like fashion, covering the entire area of an element that is
/// filled or stroked. The pattern is defined by a tile size and a body defining
/// the content of each cell. You can also add horizontal or vertical spacing
/// between the cells of the tiling.
///
/// # Examples
///
/// ```example
/// #let pat = tiling(size: (30pt, 30pt))[
///   #place(line(start: (0%, 0%), end: (100%, 100%)))
///   #place(line(start: (0%, 100%), end: (100%, 0%)))
/// ]
///
/// #rect(fill: pat, width: 100%, height: 60pt, stroke: 1pt)
/// ```
///
/// Tilings are also supported on text, but only when setting the
/// [relativeness]($tiling.relative) to either `{auto}` (the default value) or
/// `{"parent"}`. To create word-by-word or glyph-by-glyph tilings, you can
/// wrap the words or characters of your text in [boxes]($box) manually or
/// through a [show rule]($styling/#show-rules).
///
/// ```example
/// #let pat = tiling(
///   size: (30pt, 30pt),
///   relative: "parent",
///   square(
///     size: 30pt,
///     fill: gradient
///       .conic(..color.map.rainbow),
///   )
/// )
///
/// #set text(fill: pat)
/// #lorem(10)
/// ```
///
/// You can also space the elements further or closer apart using the
/// [`spacing`]($tiling.spacing) feature of the tiling. If the spacing
/// is lower than the size of the tiling, the tiling will overlap.
/// If it is higher, the tiling will have gaps of the same color as the
/// background of the tiling.
///
/// ```example
/// #let pat = tiling(
///   size: (30pt, 30pt),
///   spacing: (10pt, 10pt),
///   relative: "parent",
///   square(
///     size: 30pt,
///     fill: gradient
///      .conic(..color.map.rainbow),
///   ),
/// )
///
/// #rect(
///   width: 100%,
///   height: 60pt,
///   fill: pat,
/// )
/// ```
///
/// # Relativeness
/// The location of the starting point of the tiling is dependent on the
/// dimensions of a container. This container can either be the shape that it is
/// being painted on, or the closest surrounding container. This is controlled
/// by the `relative` argument of a tiling constructor. By default, tilings
/// are relative to the shape they are being painted on, unless the tiling is
/// applied on text, in which case they are relative to the closest ancestor
/// container.
///
/// Typst determines the ancestor container as follows:
/// - For shapes that are placed at the root/top level of the document, the
///   closest ancestor is the page itself.
/// - For other shapes, the ancestor is the innermost [`block`] or [`box`] that
///   contains the shape. This includes the boxes and blocks that are implicitly
///   created by show rules and elements. For example, a [`rotate`] will not
///   affect the parent of a gradient, but a [`grid`] will.
///
/// # Compatibility
/// This type used to be called `pattern`. The name remains as an alias, but is
/// deprecated since Typst 0.13.
#[ty(scope, cast, keywords = ["pattern"])]
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Tiling(Arc<Repr>);

/// Internal representation of [`Tiling`].
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
struct Repr {
    /// The tiling's rendered content.
    frame: LazyHash<Frame>,
    /// The tiling's tile size.
    size: Size,
    /// The tiling's tile spacing.
    spacing: Size,
    /// The tiling's relative transform.
    relative: Smart<RelativeTo>,
}

#[scope]
impl Tiling {
    /// Construct a new tiling.
    ///
    /// ```example
    /// #let pat = tiling(
    ///   size: (20pt, 20pt),
    ///   relative: "parent",
    ///   place(
    ///     dx: 5pt,
    ///     dy: 5pt,
    ///     rotate(45deg, square(
    ///       size: 5pt,
    ///       fill: black,
    ///     )),
    ///   ),
    /// )
    ///
    /// #rect(width: 100%, height: 60pt, fill: pat)
    /// ```
    #[func(constructor)]
    pub fn construct(
        engine: &mut Engine,
        span: Span,
        /// The bounding box of each cell of the tiling.
        #[named]
        #[default(Spanned::new(Smart::Auto, Span::detached()))]
        size: Spanned<Smart<Axes<Length>>>,
        /// The spacing between cells of the tiling.
        #[named]
        #[default(Spanned::new(Axes::splat(Length::zero()), Span::detached()))]
        spacing: Spanned<Axes<Length>>,
        /// The [relative placement](#relativeness) of the tiling.
        ///
        /// For an element placed at the root/top level of the document, the
        /// parent is the page itself. For other elements, the parent is the
        /// innermost block, box, column, grid, or stack that contains the
        /// element.
        #[named]
        #[default(Smart::Auto)]
        relative: Smart<RelativeTo>,
        /// The content of each cell of the tiling.
        body: Content,
    ) -> SourceResult<Tiling> {
        let size_span = size.span;
        if let Smart::Custom(size) = size.v {
            // Ensure that sizes are absolute.
            if !size.x.em.is_zero() || !size.y.em.is_zero() {
                bail!(size_span, "tile size must be absolute");
            }

            // Ensure that sizes are non-zero and finite.
            if size.x.is_zero()
                || size.y.is_zero()
                || !size.x.is_finite()
                || !size.y.is_finite()
            {
                bail!(size_span, "tile size must be non-zero and non-infinite");
            }
        }

        // Ensure that spacing is absolute.
        if !spacing.v.x.em.is_zero() || !spacing.v.y.em.is_zero() {
            bail!(spacing.span, "tile spacing must be absolute");
        }

        // Ensure that spacing is finite.
        if !spacing.v.x.is_finite() || !spacing.v.y.is_finite() {
            bail!(spacing.span, "tile spacing must be finite");
        }

        // The size of the frame
        let size = size.v.map(|l| l.map(|a| a.abs));
        let region = size.unwrap_or_else(|| Axes::splat(Abs::inf()));

        // Layout the tiling.
        let world = engine.world;
        let library = world.library();
        let locator = Locator::root();
        let styles = StyleChain::new(&library.styles);
        let pod = Region::new(region, Axes::splat(false));
        let mut frame =
            (engine.routines.layout_frame)(engine, &body, locator, styles, pod)?;

        // Set the size of the frame if the size is enforced.
        if let Smart::Custom(size) = size {
            frame.set_size(size);
        }

        // Check that the frame is non-zero.
        if frame.width().is_zero() || frame.height().is_zero() {
            bail!(
                span, "tile size must be non-zero";
                hint: "try setting the size manually"
            );
        }

        Ok(Self(Arc::new(Repr {
            size: frame.size(),
            frame: LazyHash::new(frame),
            spacing: spacing.v.map(|l| l.abs),
            relative,
        })))
    }
}

impl Tiling {
    /// Set the relative placement of the tiling.
    pub fn with_relative(mut self, relative: RelativeTo) -> Self {
        if let Some(this) = Arc::get_mut(&mut self.0) {
            this.relative = Smart::Custom(relative);
        } else {
            self.0 = Arc::new(Repr {
                relative: Smart::Custom(relative),
                ..self.0.as_ref().clone()
            });
        }

        self
    }

    /// Return the frame of the tiling.
    pub fn frame(&self) -> &Frame {
        &self.0.frame
    }

    /// Return the size of the tiling in absolute units.
    pub fn size(&self) -> Size {
        self.0.size
    }

    /// Return the spacing of the tiling in absolute units.
    pub fn spacing(&self) -> Size {
        self.0.spacing
    }

    /// Returns the relative placement of the tiling.
    pub fn relative(&self) -> Smart<RelativeTo> {
        self.0.relative
    }

    /// Returns the relative placement of the tiling.
    pub fn unwrap_relative(&self, on_text: bool) -> RelativeTo {
        self.0.relative.unwrap_or_else(|| {
            if on_text {
                RelativeTo::Parent
            } else {
                RelativeTo::Self_
            }
        })
    }
}

impl repr::Repr for Tiling {
    fn repr(&self) -> EcoString {
        let mut out =
            eco_format!("tiling(({}, {})", self.0.size.x.repr(), self.0.size.y.repr());

        if self.0.spacing.is_zero() {
            out.push_str(", spacing: (");
            out.push_str(&self.0.spacing.x.repr());
            out.push_str(", ");
            out.push_str(&self.0.spacing.y.repr());
            out.push(')');
        }

        out.push_str(", ..)");

        out
    }
}