use std::ops::Add; use ecow::{eco_format, EcoString}; use crate::diag::{bail, HintedStrResult, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ cast, elem, func, scope, ty, CastInfo, Content, Fold, FromValue, IntoValue, Packed, Reflect, Repr, Resolve, Show, StyleChain, Value, }; use crate::layout::{Abs, Axes, Axis, Dir, Side}; use crate::text::TextElem; /// Aligns content horizontally and vertically. /// /// # Example /// Let's start with centering our content horizontally: /// ```example /// #set page(height: 120pt) /// #set align(center) /// /// Centered text, a sight to see \ /// In perfect balance, visually \ /// Not left nor right, it stands alone \ /// A work of art, a visual throne /// ``` /// /// To center something vertically, use _horizon_ alignment: /// ```example /// #set page(height: 120pt) /// #set align(horizon) /// /// Vertically centered, \ /// the stage had entered, \ /// a new paragraph. /// ``` /// /// # Combining alignments /// You can combine two alignments with the `+` operator. Let's also only apply /// this to one piece of content by using the function form instead of a set /// rule: /// ```example /// #set page(height: 120pt) /// Though left in the beginning ... /// /// #align(right + bottom)[ /// ... they were right in the end, \ /// and with addition had gotten, \ /// the paragraph to the bottom! /// ] /// ``` /// /// # Nested alignment /// You can use varying alignments for layout containers and the elements within /// them. This way, you can create intricate layouts: /// /// ```example /// #align(center, block[ /// #set align(left) /// Though centered together \ /// alone \ /// we \ /// are \ /// left. /// ]) /// ``` /// /// # Alignment within the same line /// The `align` function performs block-level alignment and thus always /// interrupts the current paragraph. To have different alignment for parts /// of the same line, you should use [fractional spacing]($h) instead: /// /// ```example /// Start #h(1fr) End /// ``` #[elem(Show)] pub struct AlignElem { /// The [alignment] along both axes. /// /// ```example /// #set page(height: 6cm) /// #set text(lang: "ar") /// /// مثال /// #align( /// end + horizon, /// rect(inset: 12pt)[ركن] /// ) /// ``` #[positional] #[fold] #[default] pub alignment: Alignment, /// The content to align. #[required] pub body: Content, } impl Show for Packed { #[typst_macros::time(name = "align", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { Ok(self.body.clone().aligned(self.alignment(styles))) } } /// Where to [align] something along an axis. /// /// Possible values are: /// - `start`: Aligns at the [start]($direction.start) of the [text /// direction]($text.dir). /// - `end`: Aligns at the [end]($direction.end) of the [text /// direction]($text.dir). /// - `left`: Align at the left. /// - `center`: Aligns in the middle, horizontally. /// - `right`: Aligns at the right. /// - `top`: Aligns at the top. /// - `horizon`: Aligns in the middle, vertically. /// - `bottom`: Align at the bottom. /// /// These values are available globally and also in the alignment type's scope, /// so you can write either of the following two: /// /// ```example /// #align(center)[Hi] /// #align(alignment.center)[Hi] /// ``` /// /// # 2D alignments /// To align along both axes at the same time, add the two alignments using the /// `+` operator. For example, `top + right` aligns the content to the top right /// corner. /// /// ```example /// #set page(height: 3cm) /// #align(center + bottom)[Hi] /// ``` /// /// # Fields /// The `x` and `y` fields hold the alignment's horizontal and vertical /// components, respectively (as yet another `alignment`). They may be `{none}`. /// /// ```example /// #(top + right).x \ /// #left.x \ /// #left.y (none) /// ``` #[ty(scope)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum Alignment { H(HAlignment), V(VAlignment), Both(HAlignment, VAlignment), } impl Alignment { /// The horizontal component. pub const fn x(self) -> Option { match self { Self::H(h) | Self::Both(h, _) => Some(h), Self::V(_) => None, } } /// The vertical component. pub const fn y(self) -> Option { match self { Self::V(v) | Self::Both(_, v) => Some(v), Self::H(_) => None, } } /// Normalize the alignment to a LTR-TTB space. pub fn fix(self, text_dir: Dir) -> Axes { Axes::new( self.x().unwrap_or_default().fix(text_dir), self.y().unwrap_or_default().fix(text_dir), ) } } #[scope] impl Alignment { pub const START: Self = Alignment::H(HAlignment::Start); pub const LEFT: Self = Alignment::H(HAlignment::Left); pub const CENTER: Self = Alignment::H(HAlignment::Center); pub const RIGHT: Self = Alignment::H(HAlignment::Right); pub const END: Self = Alignment::H(HAlignment::End); pub const TOP: Self = Alignment::V(VAlignment::Top); pub const HORIZON: Self = Alignment::V(VAlignment::Horizon); pub const BOTTOM: Self = Alignment::V(VAlignment::Bottom); /// The axis this alignment belongs to. /// - `{"horizontal"}` for `start`, `left`, `center`, `right`, and `end` /// - `{"vertical"}` for `top`, `horizon`, and `bottom` /// - `{none}` for 2-dimensional alignments /// /// ```example /// #left.axis() \ /// #bottom.axis() /// ``` #[func] pub const fn axis(self) -> Option { match self { Self::H(_) => Some(Axis::X), Self::V(_) => Some(Axis::Y), Self::Both(..) => None, } } /// The inverse alignment. /// /// ```example /// #top.inv() \ /// #left.inv() \ /// #center.inv() \ /// #(left + bottom).inv() /// ``` #[func(title = "Inverse")] pub const fn inv(self) -> Alignment { match self { Self::H(h) => Self::H(h.inv()), Self::V(v) => Self::V(v.inv()), Self::Both(h, v) => Self::Both(h.inv(), v.inv()), } } } impl Default for Alignment { fn default() -> Self { HAlignment::default() + VAlignment::default() } } impl Add for Alignment { type Output = StrResult; fn add(self, rhs: Self) -> Self::Output { match (self, rhs) { (Self::H(h), Self::V(v)) | (Self::V(v), Self::H(h)) => Ok(h + v), (Self::H(_), Self::H(_)) => bail!("cannot add two horizontal alignments"), (Self::V(_), Self::V(_)) => bail!("cannot add two vertical alignments"), (Self::H(_), Self::Both(..)) | (Self::Both(..), Self::H(_)) => { bail!("cannot add a horizontal and a 2D alignment") } (Self::V(_), Self::Both(..)) | (Self::Both(..), Self::V(_)) => { bail!("cannot add a vertical and a 2D alignment") } (Self::Both(..), Self::Both(..)) => { bail!("cannot add two 2D alignments") } } } } impl Repr for Alignment { fn repr(&self) -> EcoString { match self { Self::H(h) => h.repr(), Self::V(v) => v.repr(), Self::Both(h, v) => eco_format!("{} + {}", h.repr(), v.repr()), } } } impl Fold for Alignment { fn fold(self, outer: Self) -> Self { match (self, outer) { (Self::H(h), Self::V(v) | Self::Both(_, v)) => Self::Both(h, v), (Self::V(v), Self::H(h) | Self::Both(h, _)) => Self::Both(h, v), _ => self, } } } impl Resolve for Alignment { type Output = Axes; fn resolve(self, styles: StyleChain) -> Self::Output { self.fix(TextElem::dir_in(styles)) } } impl From for Alignment { fn from(side: Side) -> Self { match side { Side::Left => Self::LEFT, Side::Top => Self::TOP, Side::Right => Self::RIGHT, Side::Bottom => Self::BOTTOM, } } } /// Alignment on this axis can be fixed to an absolute direction. pub trait FixAlignment { /// Resolve to the absolute alignment. fn fix(self, dir: Dir) -> FixedAlignment; } /// Where to align something horizontally. #[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] pub enum HAlignment { #[default] Start, Left, Center, Right, End, } impl HAlignment { /// The inverse horizontal alignment. pub const fn inv(self) -> Self { match self { Self::Start => Self::End, Self::Left => Self::Right, Self::Center => Self::Center, Self::Right => Self::Left, Self::End => Self::Start, } } } impl FixAlignment for HAlignment { fn fix(self, dir: Dir) -> FixedAlignment { match (self, dir.is_positive()) { (Self::Start, true) | (Self::End, false) => FixedAlignment::Start, (Self::Left, _) => FixedAlignment::Start, (Self::Center, _) => FixedAlignment::Center, (Self::Right, _) => FixedAlignment::End, (Self::End, true) | (Self::Start, false) => FixedAlignment::End, } } } impl Repr for HAlignment { fn repr(&self) -> EcoString { match self { Self::Start => "start".into(), Self::Left => "left".into(), Self::Center => "center".into(), Self::Right => "right".into(), Self::End => "end".into(), } } } impl Add for HAlignment { type Output = Alignment; fn add(self, rhs: VAlignment) -> Self::Output { Alignment::Both(self, rhs) } } impl From for Alignment { fn from(align: HAlignment) -> Self { Self::H(align) } } impl TryFrom for HAlignment { type Error = EcoString; fn try_from(value: Alignment) -> StrResult { match value { Alignment::H(h) => Ok(h), v => bail!( "expected `start`, `left`, `center`, `right`, or `end`, found {}", v.repr() ), } } } impl Resolve for HAlignment { type Output = FixedAlignment; fn resolve(self, styles: StyleChain) -> Self::Output { self.fix(TextElem::dir_in(styles)) } } cast! { HAlignment, self => Alignment::H(self).into_value(), align: Alignment => Self::try_from(align)?, } /// A horizontal alignment which only allows `left`/`right` and `start`/`end`, /// thus excluding `center`. #[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] pub enum OuterHAlignment { #[default] Start, Left, Right, End, } impl FixAlignment for OuterHAlignment { fn fix(self, dir: Dir) -> FixedAlignment { match (self, dir.is_positive()) { (Self::Start, true) | (Self::End, false) => FixedAlignment::Start, (Self::Left, _) => FixedAlignment::Start, (Self::Right, _) => FixedAlignment::End, (Self::End, true) | (Self::Start, false) => FixedAlignment::End, } } } impl Resolve for OuterHAlignment { type Output = FixedAlignment; fn resolve(self, styles: StyleChain) -> Self::Output { self.fix(TextElem::dir_in(styles)) } } impl From for HAlignment { fn from(value: OuterHAlignment) -> Self { match value { OuterHAlignment::Start => Self::Start, OuterHAlignment::Left => Self::Left, OuterHAlignment::Right => Self::Right, OuterHAlignment::End => Self::End, } } } impl TryFrom for OuterHAlignment { type Error = EcoString; fn try_from(value: Alignment) -> StrResult { match value { Alignment::H(HAlignment::Start) => Ok(Self::Start), Alignment::H(HAlignment::Left) => Ok(Self::Left), Alignment::H(HAlignment::Right) => Ok(Self::Right), Alignment::H(HAlignment::End) => Ok(Self::End), v => bail!("expected `start`, `left`, `right`, or `end`, found {}", v.repr()), } } } cast! { OuterHAlignment, self => HAlignment::from(self).into_value(), align: Alignment => Self::try_from(align)?, } /// Where to align something vertically. #[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum VAlignment { #[default] Top, Horizon, Bottom, } impl VAlignment { /// The inverse vertical alignment. pub const fn inv(self) -> Self { match self { Self::Top => Self::Bottom, Self::Horizon => Self::Horizon, Self::Bottom => Self::Top, } } /// Returns the position of this alignment in a container with the given /// extent. pub fn position(self, extent: Abs) -> Abs { match self { Self::Top => Abs::zero(), Self::Horizon => extent / 2.0, Self::Bottom => extent, } } } impl FixAlignment for VAlignment { fn fix(self, _: Dir) -> FixedAlignment { // The vertical alignment does not depend on text direction. match self { Self::Top => FixedAlignment::Start, Self::Horizon => FixedAlignment::Center, Self::Bottom => FixedAlignment::End, } } } impl Repr for VAlignment { fn repr(&self) -> EcoString { match self { Self::Top => "top".into(), Self::Horizon => "horizon".into(), Self::Bottom => "bottom".into(), } } } impl Add for VAlignment { type Output = Alignment; fn add(self, rhs: HAlignment) -> Self::Output { Alignment::Both(rhs, self) } } impl Resolve for VAlignment { type Output = FixedAlignment; fn resolve(self, _: StyleChain) -> Self::Output { self.fix(Dir::TTB) } } impl From for Alignment { fn from(align: VAlignment) -> Self { Self::V(align) } } impl TryFrom for VAlignment { type Error = EcoString; fn try_from(value: Alignment) -> StrResult { match value { Alignment::V(v) => Ok(v), v => bail!("expected `top`, `horizon`, or `bottom`, found {}", v.repr()), } } } cast! { VAlignment, self => Alignment::V(self).into_value(), align: Alignment => Self::try_from(align)?, } /// A vertical alignment which only allows `top` and `bottom`, thus excluding /// `horizon`. #[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum OuterVAlignment { #[default] Top, Bottom, } impl FixAlignment for OuterVAlignment { fn fix(self, _: Dir) -> FixedAlignment { // The vertical alignment does not depend on text direction. match self { Self::Top => FixedAlignment::Start, Self::Bottom => FixedAlignment::End, } } } impl From for VAlignment { fn from(value: OuterVAlignment) -> Self { match value { OuterVAlignment::Top => Self::Top, OuterVAlignment::Bottom => Self::Bottom, } } } impl TryFrom for OuterVAlignment { type Error = EcoString; fn try_from(value: Alignment) -> StrResult { match value { Alignment::V(VAlignment::Top) => Ok(Self::Top), Alignment::V(VAlignment::Bottom) => Ok(Self::Bottom), v => bail!("expected `top` or `bottom`, found {}", v.repr()), } } } cast! { OuterVAlignment, self => VAlignment::from(self).into_value(), align: Alignment => Self::try_from(align)?, } /// An internal representation that combines horizontal or vertical alignments. The /// allowed alignment positions are designated by the type parameter `H` and `V`. /// /// This is not user-visible, but an internal type to impose type safety. For example, /// `SpecificAlignment` does not allow vertical alignment /// position "center", because `V = OuterVAlignment` doesn't have it. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum SpecificAlignment { H(H), V(V), Both(H, V), } impl SpecificAlignment where H: Default + Copy + FixAlignment, V: Default + Copy + FixAlignment, { /// The horizontal component. pub const fn x(self) -> Option { match self { Self::H(h) | Self::Both(h, _) => Some(h), Self::V(_) => None, } } /// The vertical component. pub const fn y(self) -> Option { match self { Self::V(v) | Self::Both(_, v) => Some(v), Self::H(_) => None, } } /// Normalize the alignment to a LTR-TTB space. pub fn fix(self, text_dir: Dir) -> Axes { Axes::new( self.x().unwrap_or_default().fix(text_dir), self.y().unwrap_or_default().fix(text_dir), ) } } impl Resolve for SpecificAlignment where H: Default + Copy + FixAlignment, V: Default + Copy + FixAlignment, { type Output = Axes; fn resolve(self, styles: StyleChain) -> Self::Output { self.fix(TextElem::dir_in(styles)) } } impl From> for Alignment where HAlignment: From, VAlignment: From, { fn from(value: SpecificAlignment) -> Self { type FromType = SpecificAlignment; match value { FromType::H(h) => Self::H(HAlignment::from(h)), FromType::V(v) => Self::V(VAlignment::from(v)), FromType::Both(h, v) => Self::Both(HAlignment::from(h), VAlignment::from(v)), } } } impl Reflect for SpecificAlignment where H: Reflect, V: Reflect, { fn input() -> CastInfo { Alignment::input() } fn output() -> CastInfo { Alignment::output() } fn castable(value: &Value) -> bool { H::castable(value) || V::castable(value) } } impl IntoValue for SpecificAlignment where HAlignment: From, VAlignment: From, { fn into_value(self) -> Value { Alignment::from(self).into_value() } } impl FromValue for SpecificAlignment where H: Reflect + TryFrom, V: Reflect + TryFrom, { fn from_value(value: Value) -> HintedStrResult { if Alignment::castable(&value) { let align = Alignment::from_value(value)?; let result = match align { Alignment::H(_) => Self::H(H::try_from(align)?), Alignment::V(_) => Self::V(V::try_from(align)?), Alignment::Both(h, v) => { Self::Both(H::try_from(h.into())?, V::try_from(v.into())?) } }; return Ok(result); } Err(Self::error(&value)) } } /// A fixed alignment in the global coordinate space. /// /// For horizontal alignment, start is globally left and for vertical alignment /// it is globally top. #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum FixedAlignment { Start, Center, End, } impl FixedAlignment { /// Returns the position of this alignment in a container with the given /// extent. pub fn position(self, extent: Abs) -> Abs { match self { Self::Start => Abs::zero(), Self::Center => extent / 2.0, Self::End => extent, } } /// The inverse alignment. pub const fn inv(self) -> Self { match self { Self::Start => Self::End, Self::Center => Self::Center, Self::End => Self::Start, } } } impl From for FixedAlignment { fn from(side: Side) -> Self { match side { Side::Left => Self::Start, Side::Top => Self::Start, Side::Right => Self::End, Side::Bottom => Self::End, } } }