Use Outer(H|V)Alignment to constraint types (#3465)

This commit is contained in:
Leedehai 2024-02-21 04:05:40 -05:00 committed by GitHub
parent d0dd81cddf
commit f54d68daff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 206 additions and 60 deletions

View File

@ -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))
}
}

View File

@ -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 };

View File

@ -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.
///

View File

@ -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)

View File

@ -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)