Align set rule

This commit is contained in:
Laurenz 2022-12-09 10:21:11 +01:00
parent 495b525694
commit cd089b6194
27 changed files with 90 additions and 154 deletions

View File

@ -1,62 +1,22 @@
use super::{HorizontalAlign, ParNode};
use crate::prelude::*;
/// Align content along the layouting axes.
/// Just an empty shell to scope styles.
#[derive(Debug, Hash)]
pub struct AlignNode {
/// How to align the content horizontally and vertically.
pub aligns: Axes<Option<GenAlign>>,
/// The content to be aligned.
pub body: Content,
}
pub enum AlignNode {}
#[node(Layout)]
#[node]
impl AlignNode {
/// The alignment.
#[property(fold, skip)]
pub const ALIGNS: Axes<Option<GenAlign>> =
Axes::new(GenAlign::Start, GenAlign::Specific(Align::Top));
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
args.expect("body")
}
fn set(...) {
let aligns: Axes<Option<GenAlign>> = args.find()?.unwrap_or_default();
let body: Content = args.expect("body")?;
if let Axes { x: Some(x), y: None } = aligns {
if !body.has::<dyn Layout>() || body.has::<dyn Inline>() {
return Ok(body.styled(ParNode::ALIGN, HorizontalAlign(x)));
}
}
Ok(Self { aligns, body }.pack())
}
}
impl Layout for AlignNode {
fn layout(
&self,
vt: &mut Vt,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
// The child only needs to expand along an axis if there's no alignment.
let mut pod = regions.clone();
pod.expand &= self.aligns.as_ref().map(Option::is_none);
// Align paragraphs inside the child.
let mut map = StyleMap::new();
if let Some(align) = self.aligns.x {
map.set(ParNode::ALIGN, HorizontalAlign(align));
}
// Layout the child.
let mut fragment = self.body.layout(vt, styles.chain(&map), pod)?;
for (region, frame) in regions.iter().zip(&mut fragment) {
// Align in the target size. The target size depends on whether we
// should expand.
let target = regions.expand.select(region, frame.size());
let aligns = self
.aligns
.map(|align| align.resolve(styles))
.unwrap_or(Axes::new(Align::Left, Align::Top));
frame.resize(target, aligns);
}
Ok(fragment)
styles.set(Self::ALIGNS, aligns);
}
}

View File

@ -125,7 +125,7 @@ impl<'a> FlowLayouter<'a> {
par: &ParNode,
styles: StyleChain,
) -> SourceResult<()> {
let aligns = Axes::new(styles.get(ParNode::ALIGN), Align::Top);
let aligns = styles.get(AlignNode::ALIGNS).resolve(styles);
let leading = styles.get(ParNode::LEADING);
let consecutive = self.last_was_par;
let fragment = par.layout(
@ -172,17 +172,7 @@ impl<'a> FlowLayouter<'a> {
}
// How to align the block.
let aligns = Axes::new(
// For non-expanding paragraphs it is crucial that we align the
// whole paragraph as it is itself aligned.
styles.get(ParNode::ALIGN),
// Vertical align node alignment is respected by the flow.
block
.to::<AlignNode>()
.and_then(|aligned| aligned.aligns.y)
.map(|align| align.resolve(styles))
.unwrap_or(Align::Top),
);
let aligns = styles.get(AlignNode::ALIGNS).resolve(styles);
// Layout the block itself.
let sticky = styles.get(BlockNode::STICKY);

View File

@ -412,7 +412,9 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> {
bail!(span, "not allowed here");
}
self.interrupt_page(styles)?;
} else if map.interruption::<ParNode>().is_some() {
} else if map.interruption::<ParNode>().is_some()
|| map.interruption::<AlignNode>().is_some()
{
self.interrupt_par()?;
} else if map.interruption::<ListNode>().is_some()
|| map.interruption::<EnumNode>().is_some()

View File

@ -5,6 +5,7 @@ use xi_unicode::LineBreakIterator;
use typst::model::Key;
use super::{HNode, RepeatNode, Spacing};
use crate::layout::AlignNode;
use crate::prelude::*;
use crate::text::{
shape, LinebreakNode, Quoter, Quotes, ShapedText, SmartQuoteNode, SpaceNode, TextNode,
@ -22,9 +23,6 @@ impl ParNode {
/// The spacing between lines.
#[property(resolve)]
pub const LEADING: Length = Em::new(0.65).into();
/// How to align text and inline objects in their line.
#[property(resolve)]
pub const ALIGN: HorizontalAlign = HorizontalAlign(GenAlign::Start);
/// Whether to justify text in its line.
pub const JUSTIFY: bool = false;
/// How to determine line breaks.
@ -554,7 +552,7 @@ fn prepare<'a>(
styles,
hyphenate: shared_get(styles, &par.0, TextNode::HYPHENATE),
lang: shared_get(styles, &par.0, TextNode::LANG),
align: styles.get(ParNode::ALIGN),
align: styles.get(AlignNode::ALIGNS).x.resolve(styles),
justify: styles.get(ParNode::JUSTIFY),
})
}

View File

@ -1,9 +1,8 @@
use super::AlignNode;
use crate::prelude::*;
/// Place content at an absolute position.
#[derive(Debug, Hash)]
pub struct PlaceNode(pub Content);
pub struct PlaceNode(pub Content, bool);
#[node(Layout, Behave)]
impl PlaceNode {
@ -12,7 +11,8 @@ impl PlaceNode {
let dx = args.named("dx")?.unwrap_or_default();
let dy = args.named("dy")?.unwrap_or_default();
let body = args.expect::<Content>("body")?;
Ok(Self(body.moved(Axes::new(dx, dy)).aligned(aligns)).pack())
let out_of_flow = aligns.y.is_some();
Ok(Self(body.moved(Axes::new(dx, dy)).aligned(aligns), out_of_flow).pack())
}
}
@ -49,7 +49,7 @@ impl PlaceNode {
/// origin. Instead of relative to the parent's current flow/cursor
/// position.
pub fn out_of_flow(&self) -> bool {
self.0.to::<AlignNode>().map_or(false, |node| node.aligns.y.is_some())
self.1
}
}

View File

@ -1,6 +1,6 @@
use typst::model::StyledNode;
use super::{AlignNode, ParNode, Spacing};
use super::{AlignNode, Spacing};
use crate::prelude::*;
/// Arrange content and spacing along an axis.
@ -180,21 +180,13 @@ impl<'a> StackLayouter<'a> {
// Block-axis alignment of the `AlignNode` is respected
// by the stack node.
let align = block
.to::<AlignNode>()
.and_then(|node| node.aligns.get(self.axis))
.map(|align| align.resolve(styles))
.unwrap_or_else(|| {
if let Some(styled) = block.to::<StyledNode>() {
let map = &styled.map;
if map.contains(ParNode::ALIGN) {
return StyleChain::new(map).get(ParNode::ALIGN);
}
}
self.dir.start().into()
});
let aligns = if let Some(styled) = block.to::<StyledNode>() {
styles.chain(&styled.map).get(AlignNode::ALIGNS)
} else {
styles.get(AlignNode::ALIGNS)
};
let align = aligns.get(self.axis).resolve(styles);
let fragment = block.layout(vt, styles, self.regions)?;
let len = fragment.len();
for (i, frame) in fragment.into_iter().enumerate() {

View File

@ -208,6 +208,6 @@ fn items() -> LangItems {
math_atom: |atom| math::AtomNode(atom).pack(),
math_script: |base, sub, sup| math::ScriptNode { base, sub, sup }.pack(),
math_frac: |num, denom| math::FracNode { num, denom }.pack(),
math_align: |count| math::AlignNode(count).pack(),
math_align_point: |count| math::AlignPointNode(count).pack(),
}
}

View File

@ -442,14 +442,14 @@ impl Texify for ScriptNode {
}
}
/// A math alignment indicator: `&`, `&&`.
/// A math alignment point: `&`, `&&`.
#[derive(Debug, Hash)]
pub struct AlignNode(pub usize);
pub struct AlignPointNode(pub usize);
#[node(Texify)]
impl AlignNode {}
impl AlignPointNode {}
impl Texify for AlignNode {
impl Texify for AlignPointNode {
fn texify(&self, _: &mut Texifier) -> SourceResult<()> {
Ok(())
}

View File

@ -57,7 +57,7 @@ impl ContentExt for Content {
}
fn aligned(self, aligns: Axes<Option<GenAlign>>) -> Self {
crate::layout::AlignNode { aligns, body: self }.pack()
self.styled(crate::layout::AlignNode::ALIGNS, aligns)
}
fn padded(self, padding: Sides<Rel<Length>>) -> Self {

View File

@ -45,16 +45,13 @@ impl<T> Corners<T> {
}
}
/// Zip two instances into an instance.
pub fn zip<F, V, W>(self, other: Corners<V>, mut f: F) -> Corners<W>
where
F: FnMut(T, V) -> W,
{
/// Zip two instances into one.
pub fn zip<U>(self, other: Corners<U>) -> Corners<(T, U)> {
Corners {
top_left: f(self.top_left, other.top_left),
top_right: f(self.top_right, other.top_right),
bottom_right: f(self.bottom_right, other.bottom_right),
bottom_left: f(self.bottom_left, other.bottom_left),
top_left: (self.top_left, other.top_left),
top_right: (self.top_right, other.top_right),
bottom_right: (self.bottom_right, other.bottom_right),
bottom_left: (self.bottom_left, other.bottom_left),
}
}

View File

@ -45,16 +45,13 @@ impl<T> Sides<T> {
}
}
/// Zip two instances into an instance.
pub fn zip<F, V, W>(self, other: Sides<V>, mut f: F) -> Sides<W>
where
F: FnMut(T, V) -> W,
{
/// Zip two instances into one.
pub fn zip<U>(self, other: Sides<U>) -> Sides<(T, U)> {
Sides {
left: f(self.left, other.left),
top: f(self.top, other.top),
right: f(self.right, other.right),
bottom: f(self.bottom, other.bottom),
left: (self.left, other.left),
top: (self.top, other.top),
right: (self.right, other.right),
bottom: (self.bottom, other.bottom),
}
}

View File

@ -429,7 +429,7 @@ impl Eval for ast::MathNode {
Self::Symbol(v) => (vm.items.symbol)(v.get().clone() + ":op".into()),
Self::Script(v) => v.eval(vm)?,
Self::Frac(v) => v.eval(vm)?,
Self::Align(v) => v.eval(vm)?,
Self::AlignPoint(v) => v.eval(vm)?,
Self::Group(v) => v.eval(vm)?,
Self::Expr(v) => {
if let ast::Expr::Ident(ident) = v {
@ -480,11 +480,11 @@ impl Eval for ast::Frac {
}
}
impl Eval for ast::Align {
impl Eval for ast::AlignPoint {
type Output = Content;
fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
Ok((vm.items.math_align)(self.count()))
Ok((vm.items.math_align_point)(self.count()))
}
}

View File

@ -74,8 +74,8 @@ pub struct LangItems {
fn(base: Content, sub: Option<Content>, sup: Option<Content>) -> Content,
/// A fraction in a formula: `x/2`.
pub math_frac: fn(num: Content, denom: Content) -> Content,
/// An alignment indicator in a formula: `&`, `&&`.
pub math_align: fn(count: usize) -> Content,
/// An alignment point in a formula: `&`, `&&`.
pub math_align_point: fn(count: usize) -> Content,
}
impl Debug for LangItems {
@ -107,7 +107,7 @@ impl Hash for LangItems {
self.math_atom.hash(state);
self.math_script.hash(state);
self.math_frac.hash(state);
self.math_align.hash(state);
self.math_align_point.hash(state);
}
}

View File

@ -913,6 +913,14 @@ where
}
}
impl Fold for Axes<Option<GenAlign>> {
type Output = Axes<GenAlign>;
fn fold(self, outer: Self::Output) -> Self::Output {
self.zip(outer).map(|(inner, outer)| inner.unwrap_or(outer))
}
}
impl<T> Fold for Sides<T>
where
T: Fold,
@ -920,7 +928,7 @@ where
type Output = Sides<T::Output>;
fn fold(self, outer: Self::Output) -> Self::Output {
self.zip(outer, |inner, outer| inner.fold(outer))
self.zip(outer).map(|(inner, outer)| inner.fold(outer))
}
}
@ -928,7 +936,7 @@ impl Fold for Sides<Option<Rel<Abs>>> {
type Output = Sides<Rel<Abs>>;
fn fold(self, outer: Self::Output) -> Self::Output {
self.zip(outer, |inner, outer| inner.unwrap_or(outer))
self.zip(outer).map(|(inner, outer)| inner.unwrap_or(outer))
}
}
@ -936,7 +944,7 @@ impl Fold for Sides<Option<Smart<Rel<Length>>>> {
type Output = Sides<Smart<Rel<Length>>>;
fn fold(self, outer: Self::Output) -> Self::Output {
self.zip(outer, |inner, outer| inner.unwrap_or(outer))
self.zip(outer).map(|(inner, outer)| inner.unwrap_or(outer))
}
}
@ -947,7 +955,7 @@ where
type Output = Corners<T::Output>;
fn fold(self, outer: Self::Output) -> Self::Output {
self.zip(outer, |inner, outer| inner.fold(outer))
self.zip(outer).map(|(inner, outer)| inner.fold(outer))
}
}
@ -955,7 +963,7 @@ impl Fold for Corners<Option<Rel<Abs>>> {
type Output = Corners<Rel<Abs>>;
fn fold(self, outer: Self::Output) -> Self::Output {
self.zip(outer, |inner, outer| inner.unwrap_or(outer))
self.zip(outer).map(|(inner, outer)| inner.unwrap_or(outer))
}
}

View File

@ -453,8 +453,8 @@ pub enum MathNode {
Script(Script),
/// A fraction: `x/2`.
Frac(Frac),
/// An alignment indicator: `&`, `&&`.
Align(Align),
/// An alignment point: `&`, `&&`.
AlignPoint(AlignPoint),
/// Grouped mathematical material.
Group(Math),
/// An expression.
@ -472,7 +472,7 @@ impl AstNode for MathNode {
SyntaxKind::Symbol(_) => node.cast().map(Self::Symbol),
SyntaxKind::Script => node.cast().map(Self::Script),
SyntaxKind::Frac => node.cast().map(Self::Frac),
SyntaxKind::Align => node.cast().map(Self::Align),
SyntaxKind::AlignPoint => node.cast().map(Self::AlignPoint),
SyntaxKind::Math => node.cast().map(Self::Group),
_ => node.cast().map(Self::Expr),
}
@ -488,7 +488,7 @@ impl AstNode for MathNode {
Self::Symbol(v) => v.as_untyped(),
Self::Script(v) => v.as_untyped(),
Self::Frac(v) => v.as_untyped(),
Self::Align(v) => v.as_untyped(),
Self::AlignPoint(v) => v.as_untyped(),
Self::Group(v) => v.as_untyped(),
Self::Expr(v) => v.as_untyped(),
}
@ -558,11 +558,11 @@ impl Frac {
}
node! {
/// An alignment indicator in a formula: `&`, `&&`.
Align
/// An alignment point in a formula: `&`, `&&`.
AlignPoint
}
impl Align {
impl AlignPoint {
/// The number of ampersands.
pub fn count(&self) -> usize {
self.0.children().filter(|n| n.kind() == &SyntaxKind::Amp).count()

View File

@ -302,7 +302,7 @@ impl Category {
SyntaxKind::Atom(_) => None,
SyntaxKind::Script => None,
SyntaxKind::Frac => None,
SyntaxKind::Align => None,
SyntaxKind::AlignPoint => None,
SyntaxKind::Ident(_) => match parent.kind() {
SyntaxKind::Markup { .. }

View File

@ -179,8 +179,8 @@ pub enum SyntaxKind {
Script,
/// A fraction in a formula: `x/2`.
Frac,
/// An alignment indicator in a formula: `&`, `&&`.
Align,
/// An alignment point in a formula: `&`, `&&`.
AlignPoint,
/// An identifier: `it`.
Ident(EcoString),
@ -408,7 +408,7 @@ impl SyntaxKind {
Self::Atom(_) => "math atom",
Self::Script => "script",
Self::Frac => "fraction",
Self::Align => "alignment indicator",
Self::AlignPoint => "alignment point",
Self::Ident(_) => "identifier",
Self::Bool(_) => "boolean",
Self::Int(_) => "integer",
@ -528,7 +528,7 @@ impl Hash for SyntaxKind {
Self::Atom(c) => c.hash(state),
Self::Script => {}
Self::Frac => {}
Self::Align => {}
Self::AlignPoint => {}
Self::Ident(v) => v.hash(state),
Self::Bool(v) => v.hash(state),
Self::Int(v) => v.hash(state),

View File

@ -499,7 +499,7 @@ fn math_group(p: &mut Parser, group: Group) {
}
fn math_align(p: &mut Parser) {
p.perform(SyntaxKind::Align, |p| {
p.perform(SyntaxKind::AlignPoint, |p| {
p.assert(SyntaxKind::Amp);
while p.eat_if(SyntaxKind::Amp) {}
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -10,9 +10,9 @@
// Ensure that constructor styles win, but not over outer styles.
// The outer paragraph should be right-aligned,
// but the B should be center-aligned.
#set par(align: center)
#par(align: right)[
A #rect(width: 2cm, fill: conifer, inset: 4pt)[B]
#set list(label: [>])
#list(label: [--])[
#rect(width: 2cm, fill: conifer, inset: 4pt, list[A])
]
---

View File

@ -2,7 +2,7 @@
---
// Test ragged-left.
#set par(align: right)
#set align(right)
To the right! Where the sunlight peeks behind the mountain.
---
@ -35,11 +35,3 @@ fn main() {}
- List
Paragraph
---
// Error: 17-20 must be horizontal
#set par(align: top)
---
// Error: 17-33 expected alignment, found 2d alignment
#set par(align: horizon + center)

View File

@ -32,7 +32,7 @@ A#repeat(rect(width: 2.5em, height: 1em))B
// Test single repeat in both directions.
A#repeat(rect(width: 6em, height: 0.7em))B
#set par(align: center)
#set align(center)
A#repeat(rect(width: 6em, height: 0.7em))B
#set text(dir: rtl)

View File

@ -19,7 +19,7 @@ Add #h(10pt) #h(10pt) up
---
// Test spacing collapsing before spacing.
#set par(align: right)
#set align(right)
A #h(0pt) B #h(0pt) \
A B \
A #h(-1fr) B

View File

@ -11,5 +11,5 @@
}
#set page(width: auto)
#set par(align: center)
#set align(center)
#table(columns: 1 + modifiers.len(), ..cells)

View File

@ -18,6 +18,6 @@
---
// Test that lone punctuation doesn't overhang into the margin.
#set page(margin: 0pt)
#set par(align: end)
#set align(end)
#set text(dir: rtl)
:

View File

@ -22,7 +22,7 @@
#let star(width, ..args) = box(width: width, height: width)[
#set text(spacing: 0%)
#set line(..args)
#set par(align: left)
#set align(left)
#line(length: +30%, origin: (09.0%, 02%))
#line(length: +30%, origin: (38.7%, 02%), angle: -72deg)
#line(length: +30%, origin: (57.5%, 02%), angle: 252deg)