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
}
}