typst_library/layout/
align.rs

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