summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--crates/typst/src/layout/align.rs213
-rw-r--r--crates/typst/src/layout/page.rs19
-rw-r--r--crates/typst/src/model/figure.rs24
-rw-r--r--tests/typ/layout/page-number-align.typ2
-rw-r--r--tests/typ/meta/figure-caption.typ8
5 files changed, 206 insertions, 60 deletions
diff --git a/crates/typst/src/layout/align.rs b/crates/typst/src/layout/align.rs
index c108ec89..58dc7589 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<HAlignment> {
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<VAlignment> {
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<HAlignment> for Alignment {
}
}
+impl TryFrom<Alignment> for HAlignment {
+ type Error = EcoString;
+
+ fn try_from(value: Alignment) -> StrResult<Self> {
+ 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<OuterHAlignment> for HAlignment {
fn from(value: OuterHAlignment) -> Self {
match value {
@@ -343,16 +367,24 @@ impl From<OuterHAlignment> for HAlignment {
}
}
+impl TryFrom<Alignment> for OuterHAlignment {
+ type Error = EcoString;
+
+ fn try_from(value: Alignment) -> StrResult<Self> {
+ 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<VAlignment> for Alignment {
}
}
+impl TryFrom<Alignment> for VAlignment {
+ type Error = EcoString;
+
+ fn try_from(value: Alignment) -> StrResult<Self> {
+ 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<OuterVAlignment> for VAlignment {
fn from(value: OuterVAlignment) -> Self {
match value {
@@ -435,13 +485,120 @@ impl From<OuterVAlignment> for VAlignment {
}
}
+impl TryFrom<Alignment> for OuterVAlignment {
+ type Error = EcoString;
+
+ fn try_from(value: Alignment) -> StrResult<Self> {
+ 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<HAlignment, OuterVAlignment>` 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, V> {
+ H(H),
+ V(V),
+ Both(H, V),
+}
+
+impl<H, V> SpecificAlignment<H, V>
+where
+ H: Copy,
+ V: Copy,
+{
+ /// The horizontal component.
+ pub const fn x(self) -> Option<H> {
+ match self {
+ Self::H(h) | Self::Both(h, _) => Some(h),
+ Self::V(_) => None,
+ }
+ }
+
+ /// The vertical component.
+ pub const fn y(self) -> Option<V> {
+ match self {
+ Self::V(v) | Self::Both(_, v) => Some(v),
+ Self::H(_) => None,
+ }
+ }
+}
+
+impl<H, V> From<SpecificAlignment<H, V>> for Alignment
+where
+ HAlignment: From<H>,
+ VAlignment: From<V>,
+{
+ fn from(value: SpecificAlignment<H, V>) -> Self {
+ type FromType<H, V> = SpecificAlignment<H, V>;
+ 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<H, V> Reflect for SpecificAlignment<H, V>
+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<H, V> IntoValue for SpecificAlignment<H, V>
+where
+ HAlignment: From<H>,
+ VAlignment: From<V>,
+{
+ fn into_value(self) -> Value {
+ Alignment::from(self).into_value()
+ }
+}
+
+impl<H, V> FromValue for SpecificAlignment<H, V>
+where
+ H: Reflect + TryFrom<Alignment, Error = EcoString>,
+ V: Reflect + TryFrom<Alignment, Error = EcoString>,
+{
+ fn from_value(value: Value) -> StrResult<Self> {
+ 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 e73a11aa..081415c0 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<Spanned<Alignment>> = 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<HAlignment, OuterVAlignment>,
/// The page's header. Fills the top margin of each page.
///
@@ -440,7 +431,7 @@ impl Packed<PageElem> {
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 d30af5b8..950934d9 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<FigureElem> {
// 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<Spanned<VAlignment>> = 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 0e9b2bc9..7559dd65 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 2a12cc22..0188ebca 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)