typst_library/layout/
align.rs

1use std::ops::Add;
2
3use ecow::{EcoString, eco_format};
4
5use crate::diag::{HintedStrResult, StrResult, bail};
6use crate::foundations::{
7    CastInfo, Content, Fold, FromValue, IntoValue, Reflect, Repr, Resolve, StyleChain,
8    Value, cast, elem, func, scope, ty,
9};
10use crate::layout::{Abs, Axes, Axis, Dir, Side};
11use crate::text::TextElem;
12
13/// Aligns content horizontally and vertically.
14///
15/// # Example
16/// Let's start with centering our content horizontally:
17/// ```example
18/// #set page(height: 120pt)
19/// #set align(center)
20///
21/// Centered text, a sight to see \
22/// In perfect balance, visually \
23/// Not left nor right, it stands alone \
24/// A work of art, a visual throne
25/// ```
26///
27/// To center something vertically, use _horizon_ alignment:
28/// ```example
29/// #set page(height: 120pt)
30/// #set align(horizon)
31///
32/// Vertically centered, \
33/// the stage had entered, \
34/// a new paragraph.
35/// ```
36///
37/// # Combining alignments
38/// You can combine two alignments with the `+` operator. Let's also only apply
39/// this to one piece of content by using the function form instead of a set
40/// rule:
41/// ```example
42/// #set page(height: 120pt)
43/// Though left in the beginning ...
44///
45/// #align(right + bottom)[
46///   ... they were right in the end, \
47///   and with addition had gotten, \
48///   the paragraph to the bottom!
49/// ]
50/// ```
51///
52/// # Nested alignment
53/// You can use varying alignments for layout containers and the elements within
54/// them. This way, you can create intricate layouts:
55///
56/// ```example
57/// #align(center, block[
58///   #set align(left)
59///   Though centered together \
60///   alone \
61///   we \
62///   are \
63///   left.
64/// ])
65/// ```
66///
67/// # Alignment within the same line
68/// The `align` function performs block-level alignment and thus always
69/// interrupts the current paragraph. To have different alignment for parts
70/// of the same line, you should use [fractional spacing]($h) instead:
71///
72/// ```example
73/// Start #h(1fr) End
74/// ```
75#[elem]
76pub struct AlignElem {
77    /// The [alignment] along both axes.
78    ///
79    /// ```example
80    /// #set page(height: 6cm)
81    /// #set text(lang: "ar")
82    ///
83    /// مثال
84    /// #align(
85    ///   end + horizon,
86    ///   rect(inset: 12pt)[ركن]
87    /// )
88    /// ```
89    #[positional]
90    #[fold]
91    #[default]
92    pub alignment: Alignment,
93
94    /// The content to align.
95    #[required]
96    pub body: Content,
97}
98
99/// Where to align something along an axis.
100///
101/// Possible values are:
102/// - `start`: Aligns at the [start]($direction.start) of the [text
103///   direction]($text.dir).
104/// - `end`: Aligns at the [end]($direction.end) of the [text
105///   direction]($text.dir).
106/// - `left`: Align at the left.
107/// - `center`: Aligns in the middle, horizontally.
108/// - `right`: Aligns at the right.
109/// - `top`: Aligns at the top.
110/// - `horizon`: Aligns in the middle, vertically.
111/// - `bottom`: Align at the bottom.
112///
113/// These values are available globally and also in the alignment type's scope,
114/// so you can write either of the following two:
115///
116/// ```example
117/// #align(center)[Hi]
118/// #align(alignment.center)[Hi]
119/// ```
120///
121/// # 2D alignments
122/// To align along both axes at the same time, add the two alignments using the
123/// `+` operator. For example, `top + right` aligns the content to the top right
124/// corner.
125///
126/// ```example
127/// #set page(height: 3cm)
128/// #align(center + bottom)[Hi]
129/// ```
130///
131/// # Fields
132/// The `x` and `y` fields hold the alignment's horizontal and vertical
133/// components, respectively (as yet another `alignment`). They may be `{none}`.
134///
135/// ```example
136/// #(top + right).x \
137/// #left.x \
138/// #left.y (none)
139/// ```
140#[ty(scope)]
141#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
142pub enum Alignment {
143    H(HAlignment),
144    V(VAlignment),
145    Both(HAlignment, VAlignment),
146}
147
148impl Alignment {
149    /// The horizontal component.
150    pub const fn x(self) -> Option<HAlignment> {
151        match self {
152            Self::H(h) | Self::Both(h, _) => Some(h),
153            Self::V(_) => None,
154        }
155    }
156
157    /// The vertical component.
158    pub const fn y(self) -> Option<VAlignment> {
159        match self {
160            Self::V(v) | Self::Both(_, v) => Some(v),
161            Self::H(_) => None,
162        }
163    }
164
165    /// Normalize the alignment to a LTR-TTB space.
166    pub fn fix(self, text_dir: Dir) -> Axes<FixedAlignment> {
167        Axes::new(
168            self.x().unwrap_or_default().fix(text_dir),
169            self.y().unwrap_or_default().fix(text_dir),
170        )
171    }
172}
173
174#[scope]
175impl Alignment {
176    pub const START: Self = Alignment::H(HAlignment::Start);
177    pub const LEFT: Self = Alignment::H(HAlignment::Left);
178    pub const CENTER: Self = Alignment::H(HAlignment::Center);
179    pub const RIGHT: Self = Alignment::H(HAlignment::Right);
180    pub const END: Self = Alignment::H(HAlignment::End);
181    pub const TOP: Self = Alignment::V(VAlignment::Top);
182    pub const HORIZON: Self = Alignment::V(VAlignment::Horizon);
183    pub const BOTTOM: Self = Alignment::V(VAlignment::Bottom);
184
185    /// The axis this alignment belongs to.
186    /// - `{"horizontal"}` for `start`, `left`, `center`, `right`, and `end`
187    /// - `{"vertical"}` for `top`, `horizon`, and `bottom`
188    /// - `{none}` for 2-dimensional alignments
189    ///
190    /// ```example
191    /// #left.axis() \
192    /// #bottom.axis()
193    /// ```
194    #[func]
195    pub const fn axis(self) -> Option<Axis> {
196        match self {
197            Self::H(_) => Some(Axis::X),
198            Self::V(_) => Some(Axis::Y),
199            Self::Both(..) => None,
200        }
201    }
202
203    /// The inverse alignment.
204    ///
205    /// ```example
206    /// #top.inv() \
207    /// #left.inv() \
208    /// #center.inv() \
209    /// #(left + bottom).inv()
210    /// ```
211    #[func(title = "Inverse")]
212    pub const fn inv(self) -> Alignment {
213        match self {
214            Self::H(h) => Self::H(h.inv()),
215            Self::V(v) => Self::V(v.inv()),
216            Self::Both(h, v) => Self::Both(h.inv(), v.inv()),
217        }
218    }
219}
220
221impl Default for Alignment {
222    fn default() -> Self {
223        HAlignment::default() + VAlignment::default()
224    }
225}
226
227impl Add for Alignment {
228    type Output = StrResult<Self>;
229
230    fn add(self, rhs: Self) -> Self::Output {
231        match (self, rhs) {
232            (Self::H(h), Self::V(v)) | (Self::V(v), Self::H(h)) => Ok(h + v),
233            (Self::H(_), Self::H(_)) => bail!("cannot add two horizontal alignments"),
234            (Self::V(_), Self::V(_)) => bail!("cannot add two vertical alignments"),
235            (Self::H(_), Self::Both(..)) | (Self::Both(..), Self::H(_)) => {
236                bail!("cannot add a horizontal and a 2D alignment")
237            }
238            (Self::V(_), Self::Both(..)) | (Self::Both(..), Self::V(_)) => {
239                bail!("cannot add a vertical and a 2D alignment")
240            }
241            (Self::Both(..), Self::Both(..)) => {
242                bail!("cannot add two 2D alignments")
243            }
244        }
245    }
246}
247
248impl Repr for Alignment {
249    fn repr(&self) -> EcoString {
250        match self {
251            Self::H(h) => h.repr(),
252            Self::V(v) => v.repr(),
253            Self::Both(h, v) => eco_format!("{} + {}", h.repr(), v.repr()),
254        }
255    }
256}
257
258impl Fold for Alignment {
259    fn fold(self, outer: Self) -> Self {
260        match (self, outer) {
261            (Self::H(h), Self::V(v) | Self::Both(_, v)) => Self::Both(h, v),
262            (Self::V(v), Self::H(h) | Self::Both(h, _)) => Self::Both(h, v),
263            _ => self,
264        }
265    }
266}
267
268impl Resolve for Alignment {
269    type Output = Axes<FixedAlignment>;
270
271    fn resolve(self, styles: StyleChain) -> Self::Output {
272        self.fix(styles.resolve(TextElem::dir))
273    }
274}
275
276impl From<Side> for Alignment {
277    fn from(side: Side) -> Self {
278        match side {
279            Side::Left => Self::LEFT,
280            Side::Top => Self::TOP,
281            Side::Right => Self::RIGHT,
282            Side::Bottom => Self::BOTTOM,
283        }
284    }
285}
286
287/// Alignment on this axis can be fixed to an absolute direction.
288pub trait FixAlignment {
289    /// Resolve to the absolute alignment.
290    fn fix(self, dir: Dir) -> FixedAlignment;
291}
292
293/// Where to align something horizontally.
294#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
295pub enum HAlignment {
296    #[default]
297    Start,
298    Left,
299    Center,
300    Right,
301    End,
302}
303
304impl HAlignment {
305    /// The inverse horizontal alignment.
306    pub const fn inv(self) -> Self {
307        match self {
308            Self::Start => Self::End,
309            Self::Left => Self::Right,
310            Self::Center => Self::Center,
311            Self::Right => Self::Left,
312            Self::End => Self::Start,
313        }
314    }
315}
316
317impl FixAlignment for HAlignment {
318    fn fix(self, dir: Dir) -> FixedAlignment {
319        match (self, dir.is_positive()) {
320            (Self::Start, true) | (Self::End, false) => FixedAlignment::Start,
321            (Self::Left, _) => FixedAlignment::Start,
322            (Self::Center, _) => FixedAlignment::Center,
323            (Self::Right, _) => FixedAlignment::End,
324            (Self::End, true) | (Self::Start, false) => FixedAlignment::End,
325        }
326    }
327}
328
329impl Repr for HAlignment {
330    fn repr(&self) -> EcoString {
331        match self {
332            Self::Start => "start".into(),
333            Self::Left => "left".into(),
334            Self::Center => "center".into(),
335            Self::Right => "right".into(),
336            Self::End => "end".into(),
337        }
338    }
339}
340
341impl Add<VAlignment> for HAlignment {
342    type Output = Alignment;
343
344    fn add(self, rhs: VAlignment) -> Self::Output {
345        Alignment::Both(self, rhs)
346    }
347}
348
349impl From<HAlignment> for Alignment {
350    fn from(align: HAlignment) -> Self {
351        Self::H(align)
352    }
353}
354
355impl TryFrom<Alignment> for HAlignment {
356    type Error = EcoString;
357
358    fn try_from(value: Alignment) -> StrResult<Self> {
359        match value {
360            Alignment::H(h) => Ok(h),
361            v => bail!(
362                "expected `start`, `left`, `center`, `right`, or `end`, found {}",
363                v.repr()
364            ),
365        }
366    }
367}
368
369impl Resolve for HAlignment {
370    type Output = FixedAlignment;
371
372    fn resolve(self, styles: StyleChain) -> Self::Output {
373        self.fix(styles.resolve(TextElem::dir))
374    }
375}
376
377cast! {
378    HAlignment,
379    self => Alignment::H(self).into_value(),
380    align: Alignment => Self::try_from(align)?,
381}
382
383/// A horizontal alignment which only allows `left`/`right` and `start`/`end`,
384/// thus excluding `center`.
385#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
386pub enum OuterHAlignment {
387    #[default]
388    Start,
389    Left,
390    Right,
391    End,
392}
393
394impl FixAlignment for OuterHAlignment {
395    fn fix(self, dir: Dir) -> FixedAlignment {
396        match (self, dir.is_positive()) {
397            (Self::Start, true) | (Self::End, false) => FixedAlignment::Start,
398            (Self::Left, _) => FixedAlignment::Start,
399            (Self::Right, _) => FixedAlignment::End,
400            (Self::End, true) | (Self::Start, false) => FixedAlignment::End,
401        }
402    }
403}
404
405impl Resolve for OuterHAlignment {
406    type Output = FixedAlignment;
407
408    fn resolve(self, styles: StyleChain) -> Self::Output {
409        self.fix(styles.resolve(TextElem::dir))
410    }
411}
412
413impl From<OuterHAlignment> for HAlignment {
414    fn from(value: OuterHAlignment) -> Self {
415        match value {
416            OuterHAlignment::Start => Self::Start,
417            OuterHAlignment::Left => Self::Left,
418            OuterHAlignment::Right => Self::Right,
419            OuterHAlignment::End => Self::End,
420        }
421    }
422}
423
424impl TryFrom<Alignment> for OuterHAlignment {
425    type Error = EcoString;
426
427    fn try_from(value: Alignment) -> StrResult<Self> {
428        match value {
429            Alignment::H(HAlignment::Start) => Ok(Self::Start),
430            Alignment::H(HAlignment::Left) => Ok(Self::Left),
431            Alignment::H(HAlignment::Right) => Ok(Self::Right),
432            Alignment::H(HAlignment::End) => Ok(Self::End),
433            v => bail!("expected `start`, `left`, `right`, or `end`, found {}", v.repr()),
434        }
435    }
436}
437
438cast! {
439    OuterHAlignment,
440    self => HAlignment::from(self).into_value(),
441    align: Alignment => Self::try_from(align)?,
442}
443
444/// Where to align something vertically.
445#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
446pub enum VAlignment {
447    #[default]
448    Top,
449    Horizon,
450    Bottom,
451}
452
453impl VAlignment {
454    /// The inverse vertical alignment.
455    pub const fn inv(self) -> Self {
456        match self {
457            Self::Top => Self::Bottom,
458            Self::Horizon => Self::Horizon,
459            Self::Bottom => Self::Top,
460        }
461    }
462
463    /// Returns the position of this alignment in a container with the given
464    /// extent.
465    pub fn position(self, extent: Abs) -> Abs {
466        match self {
467            Self::Top => Abs::zero(),
468            Self::Horizon => extent / 2.0,
469            Self::Bottom => extent,
470        }
471    }
472}
473
474impl FixAlignment for VAlignment {
475    fn fix(self, _: Dir) -> FixedAlignment {
476        // The vertical alignment does not depend on text direction.
477        match self {
478            Self::Top => FixedAlignment::Start,
479            Self::Horizon => FixedAlignment::Center,
480            Self::Bottom => FixedAlignment::End,
481        }
482    }
483}
484
485impl Repr for VAlignment {
486    fn repr(&self) -> EcoString {
487        match self {
488            Self::Top => "top".into(),
489            Self::Horizon => "horizon".into(),
490            Self::Bottom => "bottom".into(),
491        }
492    }
493}
494
495impl Add<HAlignment> for VAlignment {
496    type Output = Alignment;
497
498    fn add(self, rhs: HAlignment) -> Self::Output {
499        Alignment::Both(rhs, self)
500    }
501}
502
503impl Resolve for VAlignment {
504    type Output = FixedAlignment;
505
506    fn resolve(self, _: StyleChain) -> Self::Output {
507        self.fix(Dir::TTB)
508    }
509}
510
511impl From<VAlignment> for Alignment {
512    fn from(align: VAlignment) -> Self {
513        Self::V(align)
514    }
515}
516
517impl TryFrom<Alignment> for VAlignment {
518    type Error = EcoString;
519
520    fn try_from(value: Alignment) -> StrResult<Self> {
521        match value {
522            Alignment::V(v) => Ok(v),
523            v => bail!("expected `top`, `horizon`, or `bottom`, found {}", v.repr()),
524        }
525    }
526}
527
528cast! {
529    VAlignment,
530    self => Alignment::V(self).into_value(),
531    align: Alignment => Self::try_from(align)?,
532}
533
534/// A vertical alignment which only allows `top` and `bottom`, thus excluding
535/// `horizon`.
536#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
537pub enum OuterVAlignment {
538    #[default]
539    Top,
540    Bottom,
541}
542
543impl FixAlignment for OuterVAlignment {
544    fn fix(self, _: Dir) -> FixedAlignment {
545        // The vertical alignment does not depend on text direction.
546        match self {
547            Self::Top => FixedAlignment::Start,
548            Self::Bottom => FixedAlignment::End,
549        }
550    }
551}
552
553impl From<OuterVAlignment> for VAlignment {
554    fn from(value: OuterVAlignment) -> Self {
555        match value {
556            OuterVAlignment::Top => Self::Top,
557            OuterVAlignment::Bottom => Self::Bottom,
558        }
559    }
560}
561
562impl TryFrom<Alignment> for OuterVAlignment {
563    type Error = EcoString;
564
565    fn try_from(value: Alignment) -> StrResult<Self> {
566        match value {
567            Alignment::V(VAlignment::Top) => Ok(Self::Top),
568            Alignment::V(VAlignment::Bottom) => Ok(Self::Bottom),
569            v => bail!("expected `top` or `bottom`, found {}", v.repr()),
570        }
571    }
572}
573
574cast! {
575    OuterVAlignment,
576    self => VAlignment::from(self).into_value(),
577    align: Alignment => Self::try_from(align)?,
578}
579
580/// An internal representation that combines horizontal or vertical alignments. The
581/// allowed alignment positions are designated by the type parameter `H` and `V`.
582///
583/// This is not user-visible, but an internal type to impose type safety. For example,
584/// `SpecificAlignment<HAlignment, OuterVAlignment>` does not allow vertical alignment
585/// position "center", because `V = OuterVAlignment` doesn't have it.
586#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
587pub enum SpecificAlignment<H, V> {
588    H(H),
589    V(V),
590    Both(H, V),
591}
592
593impl<H, V> SpecificAlignment<H, V>
594where
595    H: Default + Copy + FixAlignment,
596    V: Default + Copy + FixAlignment,
597{
598    /// The horizontal component.
599    pub const fn x(self) -> Option<H> {
600        match self {
601            Self::H(h) | Self::Both(h, _) => Some(h),
602            Self::V(_) => None,
603        }
604    }
605
606    /// The vertical component.
607    pub const fn y(self) -> Option<V> {
608        match self {
609            Self::V(v) | Self::Both(_, v) => Some(v),
610            Self::H(_) => None,
611        }
612    }
613
614    /// Normalize the alignment to a LTR-TTB space.
615    pub fn fix(self, text_dir: Dir) -> Axes<FixedAlignment> {
616        Axes::new(
617            self.x().unwrap_or_default().fix(text_dir),
618            self.y().unwrap_or_default().fix(text_dir),
619        )
620    }
621}
622
623impl<H, V> Resolve for SpecificAlignment<H, V>
624where
625    H: Default + Copy + FixAlignment,
626    V: Default + Copy + FixAlignment,
627{
628    type Output = Axes<FixedAlignment>;
629
630    fn resolve(self, styles: StyleChain) -> Self::Output {
631        self.fix(styles.resolve(TextElem::dir))
632    }
633}
634
635impl<H, V> From<SpecificAlignment<H, V>> for Alignment
636where
637    HAlignment: From<H>,
638    VAlignment: From<V>,
639{
640    fn from(value: SpecificAlignment<H, V>) -> Self {
641        type FromType<H, V> = SpecificAlignment<H, V>;
642        match value {
643            FromType::H(h) => Self::H(HAlignment::from(h)),
644            FromType::V(v) => Self::V(VAlignment::from(v)),
645            FromType::Both(h, v) => Self::Both(HAlignment::from(h), VAlignment::from(v)),
646        }
647    }
648}
649
650impl<H, V> Reflect for SpecificAlignment<H, V>
651where
652    H: Reflect,
653    V: Reflect,
654{
655    fn input() -> CastInfo {
656        Alignment::input()
657    }
658
659    fn output() -> CastInfo {
660        Alignment::output()
661    }
662
663    fn castable(value: &Value) -> bool {
664        H::castable(value) || V::castable(value)
665    }
666}
667
668impl<H, V> IntoValue for SpecificAlignment<H, V>
669where
670    HAlignment: From<H>,
671    VAlignment: From<V>,
672{
673    fn into_value(self) -> Value {
674        Alignment::from(self).into_value()
675    }
676}
677
678impl<H, V> FromValue for SpecificAlignment<H, V>
679where
680    H: Reflect + TryFrom<Alignment, Error = EcoString>,
681    V: Reflect + TryFrom<Alignment, Error = EcoString>,
682{
683    fn from_value(value: Value) -> HintedStrResult<Self> {
684        if Alignment::castable(&value) {
685            let align = Alignment::from_value(value)?;
686            let result = match align {
687                Alignment::H(_) => Self::H(H::try_from(align)?),
688                Alignment::V(_) => Self::V(V::try_from(align)?),
689                Alignment::Both(h, v) => {
690                    Self::Both(H::try_from(h.into())?, V::try_from(v.into())?)
691                }
692            };
693            return Ok(result);
694        }
695        Err(Self::error(&value))
696    }
697}
698
699/// A fixed alignment in the global coordinate space.
700///
701/// For horizontal alignment, start is globally left and for vertical alignment
702/// it is globally top.
703#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
704pub enum FixedAlignment {
705    Start,
706    Center,
707    End,
708}
709
710impl FixedAlignment {
711    /// Returns the position of this alignment in a container with the given
712    /// extent.
713    pub fn position(self, extent: Abs) -> Abs {
714        match self {
715            Self::Start => Abs::zero(),
716            Self::Center => extent / 2.0,
717            Self::End => extent,
718        }
719    }
720
721    /// The inverse alignment.
722    pub const fn inv(self) -> Self {
723        match self {
724            Self::Start => Self::End,
725            Self::Center => Self::Center,
726            Self::End => Self::Start,
727        }
728    }
729}
730
731impl From<Side> for FixedAlignment {
732    fn from(side: Side) -> Self {
733        match side {
734            Side::Left => Self::Start,
735            Side::Top => Self::Start,
736            Side::Right => Self::End,
737            Side::Bottom => Self::End,
738        }
739    }
740}