Skip to main content

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