diff --git a/crates/typst/src/layout/align.rs b/crates/typst/src/layout/align.rs index c108ec898..58dc7589d 100644 --- a/crates/typst/src/layout/align.rs +++ b/crates/typst/src/layout/align.rs @@ -5,7 +5,8 @@ use ecow::{eco_format, EcoString}; use crate::diag::{bail, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, func, scope, ty, Content, Fold, Packed, Repr, Resolve, Show, StyleChain, + 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; @@ -108,7 +109,7 @@ impl Alignment { /// The horizontal component. pub const fn x(self) -> Option { match self { - Self::H(x) | Self::Both(x, _) => Some(x), + Self::H(h) | Self::Both(h, _) => Some(h), Self::V(_) => None, } } @@ -116,7 +117,7 @@ impl Alignment { /// The vertical component. pub const fn y(self) -> Option { match self { - Self::V(y) | Self::Both(_, y) => Some(y), + Self::V(v) | Self::Both(_, v) => Some(v), Self::H(_) => None, } } @@ -188,7 +189,7 @@ impl Add for Alignment { fn add(self, rhs: Self) -> Self::Output { match (self, rhs) { - (Self::H(x), Self::V(y)) | (Self::V(y), Self::H(x)) => Ok(x + y), + (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(_)) => { @@ -207,9 +208,9 @@ impl Add for Alignment { impl Repr for Alignment { fn repr(&self) -> EcoString { match self { - Self::H(x) => x.repr(), - Self::V(y) => y.repr(), - Self::Both(x, y) => eco_format!("{} + {}", x.repr(), y.repr()), + Self::H(h) => h.repr(), + Self::V(v) => v.repr(), + Self::Both(h, v) => eco_format!("{} + {}", h.repr(), v.repr()), } } } @@ -217,8 +218,8 @@ impl Repr for Alignment { impl Fold for Alignment { fn fold(self, outer: Self) -> Self { match (self, outer) { - (Self::H(x), Self::V(y) | Self::Both(_, y)) => Self::Both(x, y), - (Self::V(y), Self::H(x) | Self::Both(x, _)) => Self::Both(x, y), + (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, } } @@ -304,6 +305,20 @@ impl From for Alignment { } } +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; @@ -315,10 +330,7 @@ impl Resolve for HAlignment { cast! { HAlignment, self => Alignment::H(self).into_value(), - align: Alignment => match align { - Alignment::H(v) => v, - v => bail!("expected `start`, `left`, `center`, `right`, or `end`, found {}", v.repr()), - } + align: Alignment => Self::try_from(align)?, } /// A horizontal alignment which only allows `left`/`right` and `start`/`end`, @@ -332,6 +344,18 @@ pub enum OuterHAlignment { End, } +impl OuterHAlignment { + /// Resolve the axis alignment based on the horizontal direction. + pub const 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 From for HAlignment { fn from(value: OuterHAlignment) -> Self { match value { @@ -343,16 +367,24 @@ impl From for HAlignment { } } +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 => match align { - Alignment::H(HAlignment::Start) => Self::Start, - Alignment::H(HAlignment::Left) => Self::Left, - Alignment::H(HAlignment::Right) => Self::Right, - Alignment::H(HAlignment::End) => Self::End, - v => bail!("expected `start`, `left`, `right`, or `end`, found {}", v.repr()), - } + align: Alignment => Self::try_from(align)?, } /// Where to align something vertically. @@ -408,13 +440,21 @@ impl From for Alignment { } } +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 => match align { - Alignment::V(v) => v, - v => bail!("expected `top`, `horizon`, or `bottom`, found {}", v.repr()), - } + align: Alignment => Self::try_from(align)?, } /// A vertical alignment which only allows `top` and `bottom`, thus excluding @@ -426,6 +466,16 @@ pub enum OuterVAlignment { Bottom, } +impl OuterVAlignment { + /// Resolve the axis alignment based on the vertical direction. + pub const fn fix(self) -> FixedAlignment { + match self { + Self::Top => FixedAlignment::Start, + Self::Bottom => FixedAlignment::End, + } + } +} + impl From for VAlignment { fn from(value: OuterVAlignment) -> Self { match value { @@ -435,13 +485,120 @@ impl From for VAlignment { } } +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 => match align { - Alignment::V(VAlignment::Top) => Self::Top, - Alignment::V(VAlignment::Bottom) => Self::Bottom, - v => bail!("expected `top` or `bottom`, found {}", v.repr()), + 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: Copy, + V: Copy, +{ + /// 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, + } + } +} + +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 { + H::input() + V::input() + } + + fn output() -> CastInfo { + H::output() + V::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) -> StrResult { + 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)) } } diff --git a/crates/typst/src/layout/page.rs b/crates/typst/src/layout/page.rs index e73a11aae..081415c01 100644 --- a/crates/typst/src/layout/page.rs +++ b/crates/typst/src/layout/page.rs @@ -12,11 +12,11 @@ use crate::foundations::{ use crate::introspection::{Counter, CounterKey, ManualPageCounter}; use crate::layout::{ Abs, AlignElem, Alignment, Axes, ColumnsElem, Dir, Frame, HAlignment, LayoutMultiple, - Length, Point, Ratio, Regions, Rel, Sides, Size, VAlignment, + Length, OuterVAlignment, Point, Ratio, Regions, Rel, Sides, Size, SpecificAlignment, + VAlignment, }; use crate::model::Numbering; -use crate::syntax::Spanned; use crate::text::TextElem; use crate::util::{NonZeroExt, Numeric, Scalar}; use crate::visualize::Paint; @@ -221,17 +221,8 @@ pub struct PageElem { /// /// #lorem(30) /// ``` - #[default(HAlignment::Center + VAlignment::Bottom)] - #[parse({ - let option: Option> = args.named("number-align")?; - if let Some(Spanned { v: align, span }) = option { - if align.y() == Some(VAlignment::Horizon) { - bail!(span, "page number cannot be `horizon`-aligned"); - } - } - option.map(|spanned| spanned.v) - })] - pub number_align: Alignment, + #[default(SpecificAlignment::Both(HAlignment::Center, OuterVAlignment::Bottom))] + pub number_align: SpecificAlignment, /// The page's header. Fills the top margin of each page. /// @@ -440,7 +431,7 @@ impl Packed { counter })); - if matches!(number_align.y(), Some(VAlignment::Top)) { + if matches!(number_align.y(), Some(OuterVAlignment::Top)) { header = if header.is_some() { header } else { numbering_marginal }; } else { footer = if footer.is_some() { footer } else { numbering_marginal }; diff --git a/crates/typst/src/model/figure.rs b/crates/typst/src/model/figure.rs index d30af5b80..950934d92 100644 --- a/crates/typst/src/model/figure.rs +++ b/crates/typst/src/model/figure.rs @@ -14,10 +14,10 @@ use crate::introspection::{ Count, Counter, CounterKey, CounterUpdate, Locatable, Location, }; use crate::layout::{ - Alignment, BlockElem, Em, HAlignment, Length, PlaceElem, VAlignment, VElem, + Alignment, BlockElem, Em, HAlignment, Length, OuterVAlignment, PlaceElem, VAlignment, + VElem, }; use crate::model::{Numbering, NumberingPattern, Outlinable, Refable, Supplement}; -use crate::syntax::Spanned; use crate::text::{Lang, Region, TextElem}; use crate::util::NonZeroExt; use crate::visualize::ImageElem; @@ -310,10 +310,9 @@ impl Show for Packed { // Build the caption, if any. if let Some(caption) = self.caption(styles) { let v = VElem::weak(self.gap(styles).into()).pack(); - realized = if caption.position(styles) == VAlignment::Bottom { - realized + v + caption.pack() - } else { - caption.pack() + v + realized + realized = match caption.position(styles) { + OuterVAlignment::Top => caption.pack() + v + realized, + OuterVAlignment::Bottom => realized + v + caption.pack(), }; } @@ -458,17 +457,8 @@ pub struct FigureCaption { /// ) /// ) /// ``` - #[default(VAlignment::Bottom)] - #[parse({ - let option: Option> = args.named("position")?; - if let Some(Spanned { v: align, span }) = option { - if align == VAlignment::Horizon { - bail!(span, "expected `top` or `bottom`"); - } - } - option.map(|spanned| spanned.v) - })] - pub position: VAlignment, + #[default(OuterVAlignment::Bottom)] + pub position: OuterVAlignment, /// The separator which will appear between the number and body. /// diff --git a/tests/typ/layout/page-number-align.typ b/tests/typ/layout/page-number-align.typ index 0e9b2bc94..7559dd659 100644 --- a/tests/typ/layout/page-number-align.typ +++ b/tests/typ/layout/page-number-align.typ @@ -21,5 +21,5 @@ #block(width: 100%, height: 100%, fill: aqua.lighten(50%)) --- -// Error: 25-39 page number cannot be `horizon`-aligned +// Error: 25-39 expected `top` or `bottom`, found horizon #set page(number-align: left + horizon) diff --git a/tests/typ/meta/figure-caption.typ b/tests/typ/meta/figure-caption.typ index 2a12cc22b..0188ebcaf 100644 --- a/tests/typ/meta/figure-caption.typ +++ b/tests/typ/meta/figure-caption.typ @@ -54,3 +54,11 @@ caption: [Hi], supplement: [B], ) + +--- +// Ref: false +#set figure.caption(position: top) + +--- +// Error: 31-38 expected `top` or `bottom`, found horizon +#set figure.caption(position: horizon)