typst_library/layout/container.rs
1use crate::diag::{HintedStrResult, SourceResult, bail};
2use crate::engine::Engine;
3use crate::foundations::{
4 Args, AutoValue, Construct, Content, Dict, Fold, FromValue, IntoValue, NativeElement,
5 Packed, Smart, StyleChain, Value, cast, elem,
6};
7use crate::introspection::Locator;
8use crate::layout::{
9 Abs, Corners, Em, Fr, Fragment, Frame, Length, Region, Regions, Rel, Sides, Size,
10 Spacing, VAlignment,
11};
12use crate::visualize::{Paint, Stroke};
13
14/// An inline-level container that sizes content.
15///
16/// All elements except inline math, text, and boxes are block-level and cannot
17/// occur inside of a @par[paragraph]. The box function can be used to integrate
18/// such elements into a paragraph. Boxes take the size of their contents by
19/// default but can also be sized explicitly.
20///
21/// = Example <example>
22/// ```example
23/// Refer to the docs
24/// #box(
25/// height: 9pt,
26/// image("docs.svg")
27/// )
28/// for more information.
29/// ```
30#[elem]
31pub struct BoxElem {
32 /// The width of the box.
33 ///
34 /// Boxes can have @fraction[fractional] widths, as the example below
35 /// demonstrates.
36 ///
37 /// _Note:_ Currently, only boxes and only their widths might be
38 /// fractionally sized within paragraphs. Support for fractionally sized
39 /// images, shapes, and more might be added in the future.
40 ///
41 /// ```example
42 /// Line in #box(width: 1fr, line(length: 100%)) between.
43 /// ```
44 pub width: Sizing,
45
46 /// The height of the box.
47 pub height: Smart<Rel<Length>>,
48
49 /// The vertical position of the box's baseline. This is used to align the
50 /// box with the text surrounding it in a paragraph, as the baseline is
51 /// meant to go right below text by default.
52 ///
53 /// By default, the box's baseline will match the baseline of its contents
54 /// (for example, of the text or equation inside it) - this is the `{auto}`
55 /// option. However, the baseline can be adjusted in two ways. The first one
56 /// is to simply pick a vertical @alignment[alignment], such as `{top}`,
57 /// `{horizon}` or `{bottom}`, to place the baseline at that position,
58 /// relative to the total height of the box (including inset).
59 ///
60 /// The other way to adjust it is to shift the default baseline vertically
61 /// by some amount, specified as a @relative[relative length]. For example,
62 /// a value of `{2pt}` will move it up by that exact length (causing the
63 /// contents to go _down_, as the alignment point moves _up_), whereas
64 /// `{-40%}` will shift the baseline _down_ by 40% of the box's total
65 /// height, including inset (thus causing the contents to move _up_).
66 ///
67 /// Both options can be specified at the same time through a dictionary with
68 /// the keys `at` and `shift`, respectively. For example, when specifying
69 /// `{(at: bottom, shift: 10pt)}`, the box's baseline will be set to the
70 /// height exactly `{10pt}` above the bottom of its contents.
71 ///
72 /// ```example
73 /// Image: #box(baseline: 40%, image("tiger.jpg", width: 2cm)).
74 /// ```
75 pub baseline: BaselinePos,
76
77 /// The box's background color. See the
78 /// @rect.fill[rectangle's documentation] for more details.
79 pub fill: Option<Paint>,
80
81 /// The box's border color. See the @rect.stroke[rectangle's documentation]
82 /// for more details.
83 #[fold]
84 pub stroke: Sides<Option<Option<Stroke>>>,
85
86 /// How much to round the box's corners. See the
87 /// @rect.radius[rectangle's documentation] for more details.
88 #[fold]
89 pub radius: Corners<Option<Rel<Length>>>,
90
91 /// How much to pad the box's content.
92 ///
93 /// This can be a single length for all sides or a dictionary of lengths for
94 /// individual sides. When passing a dictionary, it can contain the
95 /// following keys in order of precedence: `top`, `right`, `bottom`, `left`
96 /// (controlling the respective cell sides), `x`, `y` (controlling vertical
97 /// and horizontal insets), and `rest` (covers all insets not styled by
98 /// other dictionary entries). All keys are optional; omitted keys will use
99 /// their previously set value, or the default value if never set.
100 ///
101 /// @relative[Relative lengths] for this parameter are relative to the box
102 /// size excluding @box.outset[outset]. Note that relative insets and
103 /// outsets are different from relative @box.width[widths] and
104 /// @box.height[heights], which are relative to the container.
105 ///
106 /// _Note:_ When the box contains text, its exact size depends on the
107 /// current @text.top-edge[text edges].
108 ///
109 /// ```example
110 /// #rect(inset: 0pt)[Tight]
111 /// ```
112 #[fold]
113 pub inset: Sides<Option<Rel<Length>>>,
114
115 /// How much to expand the box's size without affecting the layout.
116 ///
117 /// This can be a single length for all sides or a dictionary of lengths for
118 /// individual sides. @relative[Relative lengths] for this parameter are
119 /// relative to the box size excluding outset. See the documentation for
120 /// @box.inset[inset] above for further details.
121 ///
122 /// This is useful to prevent padding from affecting line layout. For a
123 /// generalized version of the example below, see the documentation for the
124 /// @raw.block[raw text's block parameter].
125 ///
126 /// ```example
127 /// An inline
128 /// #box(
129 /// fill: luma(235),
130 /// inset: (x: 3pt, y: 0pt),
131 /// outset: (y: 3pt),
132 /// radius: 2pt,
133 /// )[rectangle].
134 /// ```
135 #[fold]
136 pub outset: Sides<Option<Rel<Length>>>,
137
138 /// Whether to clip the content inside the box.
139 ///
140 /// Clipping is useful when the box's content is larger than the box itself,
141 /// as any content that exceeds the box's bounds will be hidden.
142 ///
143 /// ```example
144 /// #box(
145 /// width: 50pt,
146 /// height: 50pt,
147 /// clip: true,
148 /// image("tiger.jpg", width: 100pt, height: 100pt)
149 /// )
150 /// ```
151 #[default(false)]
152 pub clip: bool,
153
154 /// The contents of the box.
155 #[positional]
156 pub body: Option<Content>,
157}
158
159/// An inline-level container that can produce arbitrary items that can break
160/// across lines.
161#[elem(Construct)]
162pub struct InlineElem {
163 /// A callback that is invoked with the regions to produce arbitrary inline
164 /// items.
165 #[required]
166 #[internal]
167 body: callbacks::InlineCallback,
168}
169
170impl Construct for InlineElem {
171 fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> {
172 bail!(args.span, "cannot be constructed manually");
173 }
174}
175
176impl InlineElem {
177 /// Create an inline-level item with a custom layouter.
178 #[allow(clippy::type_complexity)]
179 pub fn layouter<T: NativeElement>(
180 captured: Packed<T>,
181 callback: fn(
182 content: &Packed<T>,
183 engine: &mut Engine,
184 locator: Locator,
185 styles: StyleChain,
186 region: Size,
187 ) -> SourceResult<Vec<InlineItem>>,
188 ) -> Self {
189 Self::new(callbacks::InlineCallback::new(captured, callback))
190 }
191}
192
193impl Packed<InlineElem> {
194 /// Layout the element.
195 pub fn layout(
196 &self,
197 engine: &mut Engine,
198 locator: Locator,
199 styles: StyleChain,
200 region: Size,
201 ) -> SourceResult<Vec<InlineItem>> {
202 self.body.call(engine, locator, styles, region)
203 }
204}
205
206/// Layouted items suitable for placing in a paragraph.
207#[derive(Debug, Clone)]
208pub enum InlineItem {
209 /// Absolute spacing between other items, and whether it is weak.
210 Space(Abs, bool),
211 /// Layouted inline-level content.
212 Frame(Frame),
213}
214
215/// A block-level container.
216///
217/// Such a container can be used to separate content, size it, and give it a
218/// background or border.
219///
220/// Blocks are also the primary way to control whether text becomes part of a
221/// paragraph or not. See
222/// @par:what-becomes-a-paragraph[the paragraph documentation] for more details.
223///
224/// = Examples <examples>
225/// With a block, you can give a background to content while still allowing it
226/// to break across multiple pages.
227///
228/// ```example
229/// #set page(height: 100pt)
230/// #block(
231/// fill: luma(230),
232/// inset: 8pt,
233/// radius: 4pt,
234/// lorem(30),
235/// )
236/// ```
237///
238/// Blocks are also useful to force elements that would otherwise be inline to
239/// become block-level, especially when writing show rules.
240///
241/// ```example
242/// #show heading: it => it.body
243/// = Blockless
244/// More text.
245///
246/// #show heading: it => block(it.body)
247/// = Blocky
248/// More text.
249/// ```
250#[elem]
251pub struct BlockElem {
252 /// The block's width.
253 ///
254 /// ```example
255 /// #set align(center)
256 /// #block(
257 /// width: 60%,
258 /// inset: 8pt,
259 /// fill: silver,
260 /// lorem(10),
261 /// )
262 /// ```
263 pub width: Smart<Rel<Length>>,
264
265 /// The block's height. When the height is larger than the remaining space
266 /// on a page and @block.breakable[`breakable`] is `{true}`, the block will
267 /// continue on the next page with the remaining height.
268 ///
269 /// ```example
270 /// #set page(height: 80pt)
271 /// #set align(center)
272 /// #block(
273 /// width: 80%,
274 /// height: 150%,
275 /// fill: aqua,
276 /// )
277 /// ```
278 pub height: Sizing,
279
280 /// Whether the block can be broken and continue on the next page.
281 ///
282 /// ```example
283 /// #set page(height: 80pt)
284 /// The following block will
285 /// jump to its own page.
286 /// #block(
287 /// breakable: false,
288 /// lorem(15),
289 /// )
290 /// ```
291 #[default(true)]
292 pub breakable: bool,
293
294 /// The block's background color. See the
295 /// @rect.fill[rectangle's documentation] for more details.
296 pub fill: Option<Paint>,
297
298 /// The block's border color. See the
299 /// @rect.stroke[rectangle's documentation] for more details.
300 #[fold]
301 pub stroke: Sides<Option<Option<Stroke>>>,
302
303 /// How much to round the block's corners. See the
304 /// @rect.radius[rectangle's documentation] for more details.
305 #[fold]
306 pub radius: Corners<Option<Rel<Length>>>,
307
308 /// How much to pad the block's content. See the
309 /// @box.inset[box's documentation] for more details.
310 #[fold]
311 pub inset: Sides<Option<Rel<Length>>>,
312
313 /// How much to expand the block's size without affecting the layout. See
314 /// the @box.outset[box's documentation] for more details.
315 #[fold]
316 pub outset: Sides<Option<Rel<Length>>>,
317
318 /// The spacing around the block. When `{auto}`, inherits the paragraph
319 /// @par.spacing[`spacing`].
320 ///
321 /// For two adjacent blocks, the larger of the first block's `below` and the
322 /// second block's `above` spacing wins. Moreover, block spacing takes
323 /// precedence over paragraph @par.spacing[`spacing`].
324 ///
325 /// Note that this is only a shorthand to set `above` and `below` to the
326 /// same value. Since the values for `above` and `below` might differ, a
327 /// @reference:context[context] block only provides access to
328 /// `{block.above}` and `{block.below}`, not to `{block.spacing}` directly.
329 ///
330 /// This property can be used in combination with a show rule to adjust the
331 /// spacing around arbitrary block-level elements.
332 ///
333 /// ```example
334 /// #set align(center)
335 /// #show math.equation: set block(above: 8pt, below: 16pt)
336 ///
337 /// This sum of $x$ and $y$:
338 /// $ x + y = z $
339 /// A second paragraph.
340 /// ```
341 #[external]
342 #[default(Smart::Custom(Em::new(1.2).into()))]
343 pub spacing: Smart<Spacing>,
344
345 /// The spacing between this block and its predecessor.
346 #[parse(
347 let spacing = args.named("spacing")?;
348 args.named("above")?.or(spacing)
349 )]
350 pub above: Smart<Spacing>,
351
352 /// The spacing between this block and its successor.
353 #[parse(args.named("below")?.or(spacing))]
354 pub below: Smart<Spacing>,
355
356 /// Whether to clip the content inside the block.
357 ///
358 /// Clipping is useful when the block's content is larger than the block
359 /// itself, as any content that exceeds the block's bounds will be hidden.
360 ///
361 /// ```example
362 /// #block(
363 /// width: 50pt,
364 /// height: 50pt,
365 /// clip: true,
366 /// image("tiger.jpg", width: 100pt, height: 100pt)
367 /// )
368 /// ```
369 #[default(false)]
370 pub clip: bool,
371
372 /// Whether this block must stick to the following one, with no break in
373 /// between.
374 ///
375 /// This is, by default, set on heading blocks to prevent orphaned headings
376 /// at the bottom of the page.
377 ///
378 /// ```example
379 /// >>> #set page(height: 140pt)
380 /// // Disable stickiness of headings.
381 /// #show heading: set block(sticky: false)
382 /// #lorem(20)
383 ///
384 /// = Chapter
385 /// #lorem(10)
386 /// ```
387 #[default(false)]
388 pub sticky: bool,
389
390 /// The contents of the block.
391 #[positional]
392 pub body: Option<BlockBody>,
393}
394
395impl BlockElem {
396 /// Creates a new block element with a normal content body and directly
397 /// packs it into type-erased content.
398 pub fn packed(body: Content) -> Content {
399 Self::new().with_body(Some(BlockBody::Content(body))).pack()
400 }
401
402 /// Create a block with a custom single-region layouter.
403 ///
404 /// Such a block must have `breakable: false` (which is set by this
405 /// constructor).
406 pub fn single_layouter<T: NativeElement>(
407 captured: Packed<T>,
408 f: fn(
409 content: &Packed<T>,
410 engine: &mut Engine,
411 locator: Locator,
412 styles: StyleChain,
413 region: Region,
414 ) -> SourceResult<Frame>,
415 ) -> Self {
416 Self::new()
417 .with_breakable(false)
418 .with_body(Some(BlockBody::SingleLayouter(
419 callbacks::BlockSingleCallback::new(captured, f),
420 )))
421 }
422
423 /// Create a block with a custom multi-region layouter.
424 pub fn multi_layouter<T: NativeElement>(
425 captured: Packed<T>,
426 f: fn(
427 content: &Packed<T>,
428 engine: &mut Engine,
429 locator: Locator,
430 styles: StyleChain,
431 regions: Regions,
432 ) -> SourceResult<Fragment>,
433 ) -> Self {
434 Self::new().with_body(Some(BlockBody::MultiLayouter(
435 callbacks::BlockMultiCallback::new(captured, f),
436 )))
437 }
438}
439
440/// The contents of a block.
441#[derive(Debug, Clone, PartialEq, Hash)]
442pub enum BlockBody {
443 /// The block contains normal content.
444 Content(Content),
445 /// The block contains a layout callback that needs access to just one
446 /// base region.
447 SingleLayouter(callbacks::BlockSingleCallback),
448 /// The block contains a layout callback that needs access to the exact
449 /// regions.
450 MultiLayouter(callbacks::BlockMultiCallback),
451}
452
453impl Default for BlockBody {
454 fn default() -> Self {
455 Self::Content(Content::default())
456 }
457}
458
459cast! {
460 BlockBody,
461 self => match self {
462 Self::Content(content) => content.into_value(),
463 _ => Value::Auto,
464 },
465 v: Content => Self::Content(v),
466}
467
468/// Defines how to size something along an axis.
469#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
470pub enum Sizing {
471 /// A track that fits its item's contents.
472 #[default]
473 Auto,
474 /// A size specified in absolute terms and relative to the parent's size.
475 Rel(Rel),
476 /// A size specified as a fraction of the remaining free space in the
477 /// parent.
478 Fr(Fr),
479}
480
481impl Sizing {
482 /// Whether this is an automatic sizing.
483 pub fn is_auto(self) -> bool {
484 matches!(self, Self::Auto)
485 }
486
487 /// Whether this is fractional sizing.
488 pub fn is_fractional(self) -> bool {
489 matches!(self, Self::Fr(_))
490 }
491}
492
493impl From<Smart<Rel>> for Sizing {
494 fn from(smart: Smart<Rel>) -> Self {
495 match smart {
496 Smart::Auto => Self::Auto,
497 Smart::Custom(rel) => Self::Rel(rel),
498 }
499 }
500}
501
502impl<T: Into<Spacing>> From<T> for Sizing {
503 fn from(spacing: T) -> Self {
504 match spacing.into() {
505 Spacing::Rel(rel) => Self::Rel(rel),
506 Spacing::Fr(fr) => Self::Fr(fr),
507 }
508 }
509}
510
511cast! {
512 Sizing,
513 self => match self {
514 Self::Auto => Value::Auto,
515 Self::Rel(rel) => rel.into_value(),
516 Self::Fr(fr) => fr.into_value(),
517 },
518 _: AutoValue => Self::Auto,
519 v: Rel<Length> => Self::Rel(v),
520 v: Fr => Self::Fr(v),
521}
522
523/// Configuration for a box's baseline.
524///
525/// Here `None` means unspecified and fields set to it are inherited.
526#[derive(Debug, Copy, Clone, PartialEq, Hash)]
527pub struct BaselinePos {
528 /// Baseline's reference position relative to the height of the box's
529 /// contents.
530 at: Option<Smart<VAlignment>>,
531 /// Amount to shift the baseline by.
532 shift: Option<Rel<Length>>,
533}
534
535cast! {
536 BaselinePos,
537 self => Value::Dict(self.into()),
538 at: Smart<VAlignment> => Self { at: Some(at), shift: None, },
539 shift: Rel<Length> => Self { at: None, shift: Some(shift) },
540 mut dict: Dict => {
541 // Get a value by key, accepting either non-existence or something
542 // convertible to type T.
543 fn take<T: FromValue>(dict: &mut Dict, key: &str) -> HintedStrResult<Option<T>> {
544 dict.take(key).ok().map(|v| v.cast()).transpose()
545 }
546
547 let at = take(&mut dict, "at")?;
548 let shift = take(&mut dict, "shift")?;
549 dict.finish(&["at", "shift"])?;
550 Self { at, shift }
551 },
552}
553
554impl From<BaselinePos> for Dict {
555 fn from(baseline: BaselinePos) -> Self {
556 let mut dict = Dict::new();
557 if let Some(at) = baseline.at {
558 dict.insert("at".into(), at.into_value());
559 }
560 if let Some(shift) = baseline.shift {
561 dict.insert("shift".into(), shift.into_value());
562 }
563 dict
564 }
565}
566
567impl BaselinePos {
568 /// Baseline position relative to frame height, or `auto` to align with the
569 /// inner baseline.
570 pub fn at(&self) -> Smart<VAlignment> {
571 self.at.unwrap_or_default()
572 }
573
574 /// Amount to shift by, with default resolved.
575 pub fn shift(&self) -> Rel<Length> {
576 self.shift.unwrap_or_default()
577 }
578}
579
580impl Default for BaselinePos {
581 fn default() -> Self {
582 Self {
583 at: Some(Default::default()),
584 shift: Some(Default::default()),
585 }
586 }
587}
588
589impl Fold for BaselinePos {
590 fn fold(self, outer: Self) -> Self {
591 Self {
592 at: self.at.or(outer.at),
593 shift: self.shift.or(outer.shift),
594 }
595 }
596}
597
598/// Manual closure implementations for layout callbacks.
599///
600/// Normal closures are not `Hash`, so we can't use them.
601mod callbacks {
602 use super::*;
603
604 macro_rules! callback {
605 ($name:ident = ($($param:ident: $param_ty:ty),* $(,)?) -> $ret:ty) => {
606 #[derive(Debug, Clone, Hash)]
607 pub struct $name {
608 captured: Content,
609 f: fn(&Content, $($param_ty),*) -> $ret,
610 }
611
612 impl $name {
613 pub fn new<T: NativeElement>(
614 captured: Packed<T>,
615 f: fn(&Packed<T>, $($param_ty),*) -> $ret,
616 ) -> Self {
617 Self {
618 // Type-erased the content.
619 captured: captured.pack(),
620 // Safety: The only difference between the two function
621 // pointer types is the type of the first parameter,
622 // which changes from `&Packed<T>` to `&Content`. This
623 // is safe because:
624 // - `Packed<T>` is a transparent wrapper around
625 // `Content`, so for any `T` it has the same memory
626 // representation as `Content`.
627 // - While `Packed<T>` imposes the additional constraint
628 // that the content is of type `T`, this constraint is
629 // upheld: It is initially the case because we store a
630 // `Packed<T>` above. It keeps being the case over the
631 // lifetime of the closure because `capture` is a
632 // private field and `Content`'s `Clone` impl is
633 // guaranteed to retain the type (if it didn't,
634 // literally everything would break).
635 #[allow(clippy::missing_transmute_annotations)]
636 f: unsafe { std::mem::transmute(f) },
637 }
638 }
639
640 pub fn call(&self, $($param: $param_ty),*) -> $ret {
641 (self.f)(&self.captured, $($param),*)
642 }
643 }
644
645 impl PartialEq for $name {
646 fn eq(&self, other: &Self) -> bool {
647 // Comparing function pointers is problematic. Since for
648 // each type of content, there is typically just one
649 // callback, we skip it. It barely matters anyway since
650 // getting into a comparison codepath for inline & block
651 // elements containing callback bodies is close to
652 // impossible (as these are generally generated in show
653 // rules).
654 self.captured.eq(&other.captured)
655 }
656 }
657 };
658 }
659
660 callback! {
661 InlineCallback = (
662 engine: &mut Engine,
663 locator: Locator,
664 styles: StyleChain,
665 region: Size,
666 ) -> SourceResult<Vec<InlineItem>>
667 }
668
669 callback! {
670 BlockSingleCallback = (
671 engine: &mut Engine,
672 locator: Locator,
673 styles: StyleChain,
674 region: Region,
675 ) -> SourceResult<Frame>
676 }
677
678 callback! {
679 BlockMultiCallback = (
680 engine: &mut Engine,
681 locator: Locator,
682 styles: StyleChain,
683 regions: Regions,
684 ) -> SourceResult<Fragment>
685 }
686}