From 977ac77e6a3298be2644a8231e93acbef9f7f396 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Fri, 8 Apr 2022 15:01:55 +0200 Subject: [PATCH] Start & end alignment --- src/eval/layout.rs | 4 +-- src/eval/mod.rs | 2 ++ src/eval/ops.rs | 6 ++-- src/eval/raw.rs | 49 ++++++++++++++++++++++++++++++ src/frame.rs | 4 +-- src/geom/align.rs | 2 +- src/library/graphics/transform.rs | 5 +-- src/library/layout/align.rs | 28 ++++------------- src/library/layout/flow.rs | 5 +-- src/library/layout/place.rs | 2 +- src/library/layout/stack.rs | 3 +- src/library/mod.rs | 33 ++++++++++++++++---- src/library/prelude.rs | 3 +- src/library/text/par.rs | 21 ++++++------- tests/ref/layout/align.png | Bin 2388 -> 7832 bytes tests/typ/layout/align.typ | 13 ++++++++ 16 files changed, 126 insertions(+), 54 deletions(-) create mode 100644 src/eval/raw.rs 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 77619b32077dddd59d9137a64c873fe7050d229f..5dde0cef8986ff8d98ec2b9c61b178dca4bd1d80 100644 GIT binary patch literal 7832 zcmb_>2UL^Wwl#2N)8}!0F@#|q=zCM1nEr$F@l09MGQ&}2uM40 zA_NSG(v=dap-Au2TX>7}-hJbaciw&X{A2u#gnZ*G*?aB1)?9PWJk`9Z!pd@-g^rGn zRrU94+H`dEd*Qv#ya&#VTT|Z9(eat8UX$1H>YnNIy?tbP&-{WZX{7i;-d-o~;M<35 z)mhGtUQtULwTO!IQc{nw>269%ee@>!M4I3otT(E)RmPxyLBct*7tc?&$yZ_W<*7kJLdd9ghA4 z;{zn!zdcC2SJ&#=L82ulnOC+3_U?$~ety2OBfy9~@i$%r9dOs~KWj`bBl^Oo_hsC} zu9hr$L}P#nE7ojf(T;_;{U$qe|#AS1Ns3HM-SuBq3LSqILOZPqz&HnkSlQE0=)mf zzjg;<_{V7f%fUaU^Ovw;CVzdl*Tw+DhHc&5wZE}*e`k09`5u=^_2iHJ_kMz?3G)SV*Hn1^xwPvmxB=QYATha zpKB7nEE%{y5*tFyGg2oZ zU?YPATm5L?d1B4HP_4_x(!^!^&eMW|qQ>P*VPci7RQHkkX!ohk?M>>A-jNYF^`p?X zEdoP(->++ENc7)ax#Bi@T0p?Dr#L$;O?js(+puJ|x2&MN+-G^R<5v1jW1@$nfkB*) zzLV4J(nQ}34B&oR#;d=rfyb%J%Svj77gc`6}b68_-ZEY_T6aKnjL03E;ua0OwOor(UPfTzfKI}HzdqqhrGb?MX zG452b*YI=6C3K-Pl{WZXV0Az(s*~CU{vW>R$QtsjM*4JwLCgVenNDgO54t$a0PYPH8nTuIp-J@D64TD zJ0{^a+IUVs=l$j?Jw1I=Qc}%;4Eh;ArXQRCzK8Bje}!*}LzgyxP=K&LrBR?hJ(fGKH@JPn3^g?tPr&x?OpN!W$30M6>Ggj32PN7W_&3CBg zX@7sezHjr?v%!ZR6)p(cFp+_@sOVX;G`_UO-_ zTby{+5aEt&DR+&5B8tgjh3l^`ROjt1;<2q%og)NOQb0++PgX!h?uL~%!zVsh!G)SLdFsTO_=YE zx1!u7J-5~t1YfB#o9Ex&Se{y4>2cm^Ml!PT*x*pIvc_r|e0NST-o+(B&Ten6N3p$&{s`zJ6*X{}c zgEEds$m8k=3H(gg4z=8Us@)>P+}s?J^8lx4Z;?Ia&1L)9(qT4JQz}%m@hL_ntr#&= zQNpvQPro)d_d_Q5`F+gFntFj!bZUPy*jwgiYioNe?fOB5r8z1_MC69yG+@o>@UZy( z@z%n^!jB(6E-ft`6S-4LB-*FXD~Iz46L=(@dPke^>^xEz>^sj36IQ-Kjb}2HTo0$vUzXwuP zRaMnJVdc4GKQkMfUQ5mT#>P#�^=-To!hA5@f?t8{nOfukVz_mxhMLm6d?8x1N)7 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