diff --git a/src/eval/layout.rs b/src/eval/layout.rs index 9bf441947..09b692539 100644 --- a/src/eval/layout.rs +++ b/src/eval/layout.rs @@ -5,7 +5,7 @@ use std::fmt::{self, Debug, Formatter}; use std::hash::Hash; use std::sync::Arc; -use super::{Barrier, StyleChain}; +use super::{Barrier, RawAlign, StyleChain}; use crate::diag::TypResult; use crate::frame::{Element, Frame, Geometry, Shape, Stroke}; use crate::geom::{ @@ -182,7 +182,7 @@ impl LayoutNode { } /// Set alignments for this node. - pub fn aligned(self, aligns: Spec>) -> Self { + pub fn aligned(self, aligns: Spec>) -> Self { if aligns.any(Option::is_some) { AlignNode { aligns, child: self }.pack() } else { diff --git a/src/eval/mod.rs b/src/eval/mod.rs index e83c81590..8b777a649 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -17,6 +17,7 @@ mod func; mod layout; mod module; mod ops; +mod raw; mod scope; mod show; mod str; @@ -32,6 +33,7 @@ pub use dict::*; pub use func::*; pub use layout::*; pub use module::*; +pub use raw::*; pub use scope::*; pub use show::*; pub use styles::*; diff --git a/src/eval/ops.rs b/src/eval/ops.rs index ff21d93f9..0ba4320ec 100644 --- a/src/eval/ops.rs +++ b/src/eval/ops.rs @@ -1,8 +1,8 @@ use std::cmp::Ordering; -use super::{Dynamic, StrExt, Value}; +use super::{Dynamic, RawAlign, StrExt, Value}; use crate::diag::StrResult; -use crate::geom::{Align, Numeric, Spec, SpecAxis}; +use crate::geom::{Numeric, Spec, SpecAxis}; use Value::*; /// Bail with a type mismatch error. @@ -94,7 +94,7 @@ pub fn add(lhs: Value, rhs: Value) -> StrResult { if let (Dyn(a), Dyn(b)) = (&a, &b) { // 1D alignments can be summed into 2D alignments. if let (Some(&a), Some(&b)) = - (a.downcast::(), b.downcast::()) + (a.downcast::(), b.downcast::()) { return if a.axis() != b.axis() { Ok(Dyn(Dynamic::new(match a.axis() { diff --git a/src/eval/raw.rs b/src/eval/raw.rs new file mode 100644 index 000000000..337638f99 --- /dev/null +++ b/src/eval/raw.rs @@ -0,0 +1,49 @@ +use std::fmt::{self, Debug, Formatter}; + +use super::{Resolve, StyleChain}; +use crate::geom::{Align, SpecAxis}; +use crate::library::text::ParNode; + +/// The unresolved alignment representation. +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum RawAlign { + /// Align at the start side of the text direction. + Start, + /// Align at the end side of the text direction. + End, + /// Align at a specific alignment. + Specific(Align), +} + +impl Resolve for RawAlign { + type Output = Align; + + fn resolve(self, styles: StyleChain) -> Self::Output { + let dir = styles.get(ParNode::DIR); + match self { + Self::Start => dir.start().into(), + Self::End => dir.end().into(), + Self::Specific(align) => align, + } + } +} + +impl RawAlign { + /// The axis this alignment belongs to. + pub const fn axis(self) -> SpecAxis { + match self { + Self::Start | Self::End => SpecAxis::Horizontal, + Self::Specific(align) => align.axis(), + } + } +} + +impl Debug for RawAlign { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Start => f.pad("left"), + Self::End => f.pad("center"), + Self::Specific(align) => align.fmt(f), + } + } +} diff --git a/src/frame.rs b/src/frame.rs index 3c747f0d2..5698560f1 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -79,8 +79,8 @@ impl Frame { pub fn resize(&mut self, target: Size, aligns: Spec) { if self.size != target { let offset = Point::new( - aligns.x.resolve(target.x - self.size.x), - aligns.y.resolve(target.y - self.size.y), + aligns.x.position(target.x - self.size.x), + aligns.y.position(target.y - self.size.y), ); self.size = target; self.translate(offset); diff --git a/src/geom/align.rs b/src/geom/align.rs index be2eac96a..3d5f96e5f 100644 --- a/src/geom/align.rs +++ b/src/geom/align.rs @@ -45,7 +45,7 @@ impl Align { } /// Returns the position of this alignment in the given length. - pub fn resolve(self, length: Length) -> Length { + pub fn position(self, length: Length) -> Length { match self { Self::Left | Self::Top => Length::zero(), Self::Center | Self::Horizon => length / 2.0, diff --git a/src/library/graphics/transform.rs b/src/library/graphics/transform.rs index eb419a7e3..67f9cad97 100644 --- a/src/library/graphics/transform.rs +++ b/src/library/graphics/transform.rs @@ -22,7 +22,8 @@ pub type ScaleNode = TransformNode; #[node] impl TransformNode { /// The origin of the transformation. - pub const ORIGIN: Spec> = Spec::default(); + #[property(resolve)] + pub const ORIGIN: Spec> = Spec::default(); fn construct(_: &mut Context, args: &mut Args) -> TypResult { let transform = match T { @@ -61,7 +62,7 @@ impl Layout for TransformNode { let mut frames = self.child.layout(ctx, regions, styles)?; for frame in &mut frames { - let Spec { x, y } = origin.zip(frame.size).map(|(o, s)| o.resolve(s)); + let Spec { x, y } = origin.zip(frame.size).map(|(o, s)| o.position(s)); let transform = Transform::translate(x, y) .pre_concat(self.transform) .pre_concat(Transform::translate(-x, -y)); diff --git a/src/library/layout/align.rs b/src/library/layout/align.rs index b08e5fcee..699a908c4 100644 --- a/src/library/layout/align.rs +++ b/src/library/layout/align.rs @@ -5,7 +5,7 @@ use crate::library::text::ParNode; #[derive(Debug, Hash)] pub struct AlignNode { /// How to align the node horizontally and vertically. - pub aligns: Spec>, + pub aligns: Spec>, /// The node to be aligned. pub child: LayoutNode, } @@ -42,30 +42,14 @@ impl Layout for AlignNode { // Align in the target size. The target size depends on whether we // should expand. let target = regions.expand.select(region, frame.size); - let default = Spec::new(Align::Left, Align::Top); - let aligns = self.aligns.unwrap_or(default); + let aligns = self + .aligns + .map(|align| align.resolve(styles)) + .unwrap_or(Spec::new(Align::Left, Align::Top)); + Arc::make_mut(frame).resize(target, aligns); } Ok(frames) } } - -dynamic! { - Align: "alignment", -} - -dynamic! { - Spec: "2d alignment", -} - -castable! { - Spec>, - Expected: "1d or 2d alignment", - @align: Align => { - let mut aligns = Spec::default(); - aligns.set(align.axis(), Some(*align)); - aligns - }, - @aligns: Spec => aligns.map(Some), -} diff --git a/src/library/layout/flow.rs b/src/library/layout/flow.rs index ffda69256..a53b03041 100644 --- a/src/library/layout/flow.rs +++ b/src/library/layout/flow.rs @@ -189,6 +189,7 @@ impl FlowLayouter { // Vertical align node alignment is respected by the flow node. node.downcast::() .and_then(|aligned| aligned.aligns.y) + .map(|align| align.resolve(styles)) .unwrap_or(Align::Top), ); @@ -238,8 +239,8 @@ impl FlowLayouter { } FlowItem::Frame(frame, aligns) => { ruler = ruler.max(aligns.y); - let x = aligns.x.resolve(size.x - frame.size.x); - let y = offset + ruler.resolve(size.y - self.used.y); + let x = aligns.x.position(size.x - frame.size.x); + let y = offset + ruler.position(size.y - self.used.y); let pos = Point::new(x, y); offset += frame.size.y; output.push_frame(pos, frame); diff --git a/src/library/layout/place.rs b/src/library/layout/place.rs index 2d4ebc4d3..eefa6a9b0 100644 --- a/src/library/layout/place.rs +++ b/src/library/layout/place.rs @@ -8,9 +8,9 @@ pub struct PlaceNode(pub LayoutNode); #[node] impl PlaceNode { fn construct(_: &mut Context, args: &mut Args) -> TypResult { - let aligns = args.find()?.unwrap_or(Spec::with_x(Some(Align::Left))); let tx = args.named("dx")?.unwrap_or_default(); let ty = args.named("dy")?.unwrap_or_default(); + let aligns = args.find()?.unwrap_or(Spec::with_x(Some(RawAlign::Start))); let body: LayoutNode = args.expect("body")?; Ok(Content::block(Self( body.moved(Point::new(tx, ty)).aligned(aligns), diff --git a/src/library/layout/stack.rs b/src/library/layout/stack.rs index b0e2e1607..312757f37 100644 --- a/src/library/layout/stack.rs +++ b/src/library/layout/stack.rs @@ -175,6 +175,7 @@ impl StackLayouter { let align = node .downcast::() .and_then(|node| node.aligns.get(self.axis)) + .map(|align| align.resolve(styles)) .unwrap_or(self.dir.start().into()); let frames = node.layout(ctx, &self.regions, styles)?; @@ -229,7 +230,7 @@ impl StackLayouter { // Align along the block axis. let parent = size.get(self.axis); let child = frame.size.get(self.axis); - let block = ruler.resolve(parent - self.used.main) + let block = ruler.position(parent - self.used.main) + if self.dir.is_positive() { cursor } else { diff --git a/src/library/mod.rs b/src/library/mod.rs index 7c5a519f7..358c2204b 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -113,12 +113,14 @@ pub fn new() -> Scope { std.def_const("rtl", Dir::RTL); std.def_const("ttb", Dir::TTB); std.def_const("btt", Dir::BTT); - std.def_const("left", Align::Left); - std.def_const("center", Align::Center); - std.def_const("right", Align::Right); - std.def_const("top", Align::Top); - std.def_const("horizon", Align::Horizon); - std.def_const("bottom", Align::Bottom); + std.def_const("start", RawAlign::Start); + std.def_const("end", RawAlign::End); + std.def_const("left", RawAlign::Specific(Align::Left)); + std.def_const("center", RawAlign::Specific(Align::Center)); + std.def_const("right", RawAlign::Specific(Align::Right)); + std.def_const("top", RawAlign::Specific(Align::Top)); + std.def_const("horizon", RawAlign::Specific(Align::Horizon)); + std.def_const("bottom", RawAlign::Specific(Align::Bottom)); std } @@ -127,6 +129,25 @@ dynamic! { Dir: "direction", } +dynamic! { + RawAlign: "alignment", +} + +dynamic! { + Spec: "2d alignment", +} + +castable! { + Spec>, + Expected: "1d or 2d alignment", + @align: RawAlign => { + let mut aligns = Spec::default(); + aligns.set(align.axis(), Some(*align)); + aligns + }, + @aligns: Spec => aligns.map(Some), +} + castable! { usize, Expected: "non-negative integer", diff --git a/src/library/prelude.rs b/src/library/prelude.rs index a2e296fa7..f052a43a7 100644 --- a/src/library/prelude.rs +++ b/src/library/prelude.rs @@ -10,7 +10,8 @@ pub use typst_macros::node; pub use crate::diag::{with_alternative, At, Error, StrResult, TypError, TypResult}; pub use crate::eval::{ Arg, Args, Array, Cast, Content, Dict, Fold, Func, Key, Layout, LayoutNode, Merge, - Node, Regions, Scope, Show, ShowNode, Smart, StyleChain, StyleMap, StyleVec, Value, + Node, RawAlign, Regions, Resolve, Scope, Show, ShowNode, Smart, StyleChain, StyleMap, + StyleVec, Value, }; pub use crate::frame::*; pub use crate::geom::*; diff --git a/src/library/text/par.rs b/src/library/text/par.rs index 2d31cd116..dc7c9dcfd 100644 --- a/src/library/text/par.rs +++ b/src/library/text/par.rs @@ -33,7 +33,8 @@ impl ParNode { /// The direction for text and inline objects. pub const DIR: Dir = Dir::LTR; /// How to align text and inline objects in their line. - pub const ALIGN: Align = Align::Left; + #[property(resolve)] + pub const ALIGN: RawAlign = RawAlign::Start; /// Whether to justify text in its line. pub const JUSTIFY: bool = false; /// How to determine line breaks. @@ -74,15 +75,13 @@ impl ParNode { dir = Some(v); } - let align = - if let Some(Spanned { v, span }) = args.named::>("align")? { - if v.axis() != SpecAxis::Horizontal { - bail!(span, "must be horizontal"); - } - Some(v) - } else { - dir.map(|dir| dir.start().into()) - }; + let mut align = None; + if let Some(Spanned { v, span }) = args.named::>("align")? { + if v.axis() != SpecAxis::Horizontal { + bail!(span, "must be horizontal"); + } + align = Some(v); + }; styles.set_opt(Self::LANG, lang); styles.set_opt(Self::DIR, dir); @@ -880,7 +879,7 @@ fn commit( // Construct the line's frame from left to right. for item in reordered { let mut position = |frame: Frame| { - let x = offset + align.resolve(remaining); + let x = offset + align.position(remaining); let y = line.baseline - frame.baseline(); offset += frame.size.x; output.merge_frame(Point::new(x, y), frame); diff --git a/tests/ref/layout/align.png b/tests/ref/layout/align.png index 77619b320..5dde0cef8 100644 Binary files a/tests/ref/layout/align.png and b/tests/ref/layout/align.png differ diff --git a/tests/typ/layout/align.typ b/tests/typ/layout/align.typ index 09c4dee56..f0603b46c 100644 --- a/tests/typ/layout/align.typ +++ b/tests/typ/layout/align.typ @@ -14,12 +14,25 @@ )) --- +// Test that multiple paragraphs in subflow also respect alignment. #align(center)[ Lorem Ipsum Dolor ] +--- +// Test start and end alignment. +#rotate(-30deg, origin: end + horizon)[Hello] + +#set par(lang: "de") +#align(start)[Start] +#align(end)[Ende] + +#set par(lang: "ar") +#align(start)[يبدأ] +#align(end)[نهاية] + --- // Ref: false #test(type(center), "alignment")