2d alignments with plus operator

This commit is contained in:
Laurenz 2021-11-23 12:45:20 +01:00
parent d3f6040ced
commit 4f9e5819bb
22 changed files with 113 additions and 81 deletions

View File

@ -422,7 +422,7 @@ impl Eval for CallArgs {
} }
v => { v => {
if let Value::Dyn(dynamic) = &v { if let Value::Dyn(dynamic) = &v {
if let Some(args) = dynamic.downcast_ref::<Args>() { if let Some(args) = dynamic.downcast::<Args>() {
items.extend(args.items.iter().cloned()); items.extend(args.items.iter().cloned());
continue; continue;
} }

View File

@ -1,8 +1,9 @@
use std::cmp::Ordering; use std::cmp::Ordering;
use std::convert::TryFrom; use std::convert::TryFrom;
use super::Value; use super::{Dynamic, Value};
use crate::diag::StrResult; use crate::diag::StrResult;
use crate::geom::{Align, Get, Spec};
use crate::util::EcoString; use crate::util::EcoString;
use Value::*; use Value::*;
@ -87,7 +88,25 @@ pub fn add(lhs: Value, rhs: Value) -> StrResult<Value> {
(Template(a), Str(b)) => Template(a + b), (Template(a), Str(b)) => Template(a + b),
(Str(a), Template(b)) => Template(a + b), (Str(a), Template(b)) => Template(a + b),
(a, b) => mismatch!("cannot add {} and {}", a, b), (a, b) => {
if let (Dyn(a), Dyn(b)) = (&a, &b) {
// 1D alignments can be summed into 2D alignments.
if let (Some(&a), Some(&b)) =
(a.downcast::<Align>(), b.downcast::<Align>())
{
if a.axis() == b.axis() {
return Err(format!("cannot add two {:?} alignments", a.axis()));
}
let mut aligns = Spec::default();
aligns.set(a.axis(), Some(a));
aligns.set(b.axis(), Some(b));
return Ok(Dyn(Dynamic::new(aligns)));
}
}
mismatch!("cannot add {} and {}", a, b);
}
}) })
} }

View File

@ -188,7 +188,7 @@ impl Dynamic {
} }
/// Try to downcast to a reference to a specific type. /// Try to downcast to a reference to a specific type.
pub fn downcast_ref<T: 'static>(&self) -> Option<&T> { pub fn downcast<T: 'static>(&self) -> Option<&T> {
self.0.as_any().downcast_ref() self.0.as_any().downcast_ref()
} }
@ -225,7 +225,7 @@ where
} }
fn dyn_eq(&self, other: &Dynamic) -> bool { fn dyn_eq(&self, other: &Dynamic) -> bool {
if let Some(other) = other.downcast_ref::<Self>() { if let Some(other) = other.downcast::<Self>() {
self == other self == other
} else { } else {
false false
@ -334,7 +334,7 @@ macro_rules! castable {
let found = match value { let found = match value {
$($pattern => return Ok($out),)* $($pattern => return Ok($out),)*
$crate::eval::Value::Dyn(dynamic) => { $crate::eval::Value::Dyn(dynamic) => {
$(if let Some($dyn_in) = dynamic.downcast_ref::<$dyn_type>() { $(if let Some($dyn_in) = dynamic.downcast::<$dyn_type>() {
return Ok($dyn_out); return Ok($dyn_out);
})* })*
dynamic.type_name() dynamic.type_name()

View File

@ -123,7 +123,7 @@ impl<T: Debug> Debug for Spec<T> {
} }
/// The two specific layouting axes. /// The two specific layouting axes.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub enum SpecAxis { pub enum SpecAxis {
/// The horizontal layouting axis. /// The horizontal layouting axis.
Horizontal, Horizontal,
@ -150,3 +150,12 @@ impl SpecAxis {
} }
} }
} }
impl Debug for SpecAxis {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.pad(match self {
Self::Horizontal => "horizontal",
Self::Vertical => "vertical",
})
}
}

View File

@ -104,27 +104,27 @@ impl PackedNode {
} }
/// Force a size for this node. /// Force a size for this node.
pub fn sized(self, w: Option<Linear>, h: Option<Linear>) -> Self { pub fn sized(self, sizing: Spec<Option<Linear>>) -> Self {
if w.is_some() || h.is_some() { if sizing.any(Option::is_some) {
SizedNode { child: self, sizing: Spec::new(w, h) }.pack() SizedNode { child: self, sizing }.pack()
} else { } else {
self self
} }
} }
/// Set alignments for this node. /// Set alignments for this node.
pub fn aligned(self, x: Option<Align>, y: Option<Align>) -> Self { pub fn aligned(self, aligns: Spec<Option<Align>>) -> Self {
if x.is_some() || y.is_some() { if aligns.any(Option::is_some) {
AlignNode { child: self, aligns: Spec::new(x, y) }.pack() AlignNode { child: self, aligns }.pack()
} else { } else {
self self
} }
} }
/// Move this node's contents without affecting layout. /// Move this node's contents without affecting layout.
pub fn moved(self, dx: Option<Linear>, dy: Option<Linear>) -> Self { pub fn moved(self, offset: Spec<Option<Linear>>) -> Self {
if dx.is_some() || dy.is_some() { if offset.any(Option::is_some) {
MoveNode { child: self, offset: Spec::new(dx, dy) }.pack() MoveNode { child: self, offset }.pack()
} else { } else {
self self
} }

View File

@ -2,32 +2,18 @@ use super::prelude::*;
/// `align`: Configure the alignment along the layouting axes. /// `align`: Configure the alignment along the layouting axes.
pub fn align(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { pub fn align(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let Spec { x, y } = parse_aligns(args)?; let aligns = args.expect::<Spec<_>>("alignment")?;
let body = args.expect::<Template>("body")?; let body = args.expect::<Template>("body")?;
Ok(Value::Template(Template::from_block(move |style| { Ok(Value::Template(Template::from_block(move |style| {
let mut style = style.clone(); let mut style = style.clone();
if let Some(x) = x { if let Some(x) = aligns.x {
style.par_mut().align = x; style.par_mut().align = x;
} }
body.pack(&style).aligned(x, y) body.pack(&style).aligned(aligns)
}))) })))
} }
/// Parse alignment arguments with shorthand.
pub(super) fn parse_aligns(args: &mut Args) -> TypResult<Spec<Option<Align>>> {
let mut x = args.named("horizontal")?;
let mut y = args.named("vertical")?;
for Spanned { v, span } in args.all::<Spanned<Align>>() {
match v.axis() {
SpecAxis::Horizontal if x.is_none() => x = Some(v),
SpecAxis::Vertical if y.is_none() => y = Some(v),
_ => bail!(span, "unexpected argument"),
}
}
Ok(Spec::new(x, y))
}
/// A node that aligns its child. /// A node that aligns its child.
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
pub struct AlignNode { pub struct AlignNode {

View File

@ -7,8 +7,7 @@ use crate::image::ImageId;
/// `image`: An image. /// `image`: An image.
pub fn image(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> { pub fn image(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let path = args.expect::<Spanned<EcoString>>("path to image file")?; let path = args.expect::<Spanned<EcoString>>("path to image file")?;
let width = args.named("width")?; let sizing = Spec::new(args.named("width")?, args.named("height")?);
let height = args.named("height")?;
let fit = args.named("fit")?.unwrap_or_default(); let fit = args.named("fit")?.unwrap_or_default();
// Load the image. // Load the image.
@ -21,7 +20,7 @@ pub fn image(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
})?; })?;
Ok(Value::Template(Template::from_inline(move |_| { Ok(Value::Template(Template::from_inline(move |_| {
ImageNode { id, fit }.pack().sized(width, height) ImageNode { id, fit }.pack().sized(sizing)
}))) })))
} }

View File

@ -141,6 +141,15 @@ dynamic! {
Align: "alignment", Align: "alignment",
} }
dynamic! {
Spec<Option<Align>>: "2d alignment",
@align: Align => {
let mut aligns = Spec::default();
aligns.set(align.axis(), Some(*align));
aligns
},
}
dynamic! { dynamic! {
FontFamily: "font family", FontFamily: "font family",
Value::Str(string) => Self::Named(string.to_lowercase()), Value::Str(string) => Self::Named(string.to_lowercase()),

View File

@ -24,20 +24,18 @@ pub fn par(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
}); });
if let Some(Spanned { v, span }) = args.named::<Spanned<Dir>>("dir")? { if let Some(Spanned { v, span }) = args.named::<Spanned<Dir>>("dir")? {
if v.axis() == SpecAxis::Horizontal { if v.axis() != SpecAxis::Horizontal {
dir = Some(v);
} else {
bail!(span, "must be horizontal"); bail!(span, "must be horizontal");
} }
dir = Some(v);
} }
let mut align = None; let mut align = None;
if let Some(Spanned { v, span }) = args.named::<Spanned<Align>>("align")? { if let Some(Spanned { v, span }) = args.named::<Spanned<Align>>("align")? {
if v.axis() == SpecAxis::Horizontal { if v.axis() != SpecAxis::Horizontal {
align = Some(v);
} else {
bail!(span, "must be horizontal"); bail!(span, "must be horizontal");
} }
align = Some(v);
} }
ctx.template.modify(move |style| { ctx.template.modify(move |style| {

View File

@ -1,18 +1,13 @@
use super::parse_aligns;
use super::prelude::*; use super::prelude::*;
/// `place`: Place content at an absolute position. /// `place`: Place content at an absolute position.
pub fn place(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { pub fn place(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let Spec { x, y } = parse_aligns(args)?; let aligns = args.find().unwrap_or(Spec::new(Some(Align::Left), None));
let dx = args.named("dx")?; let offset = Spec::new(args.named("dx")?, args.named("dy")?);
let dy = args.named("dy")?;
let body: Template = args.expect("body")?; let body: Template = args.expect("body")?;
Ok(Value::Template(Template::from_block(move |style| { Ok(Value::Template(Template::from_block(move |style| {
PlacedNode { PlacedNode {
child: body child: body.pack(style).moved(offset).aligned(aligns),
.pack(style)
.moved(dx, dy)
.aligned(Some(x.unwrap_or(Align::Left)), y),
} }
}))) })))
} }

View File

@ -5,9 +5,8 @@ use crate::util::RcExt;
/// `rect`: A rectangle with optional content. /// `rect`: A rectangle with optional content.
pub fn rect(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { pub fn rect(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let width = args.named("width")?; let sizing = Spec::new(args.named("width")?, args.named("height")?);
let height = args.named("height")?; shape_impl(args, ShapeKind::Rect, sizing)
shape_impl(args, ShapeKind::Rect, width, height)
} }
/// `square`: A square with optional content. /// `square`: A square with optional content.
@ -21,14 +20,14 @@ pub fn square(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
None => args.named("height")?, None => args.named("height")?,
size => size, size => size,
}; };
shape_impl(args, ShapeKind::Square, width, height) let sizing = Spec::new(width, height);
shape_impl(args, ShapeKind::Square, sizing)
} }
/// `ellipse`: An ellipse with optional content. /// `ellipse`: An ellipse with optional content.
pub fn ellipse(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { pub fn ellipse(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let width = args.named("width")?; let sizing = Spec::new(args.named("width")?, args.named("height")?);
let height = args.named("height")?; shape_impl(args, ShapeKind::Ellipse, sizing)
shape_impl(args, ShapeKind::Ellipse, width, height)
} }
/// `circle`: A circle with optional content. /// `circle`: A circle with optional content.
@ -42,14 +41,14 @@ pub fn circle(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
None => args.named("height")?, None => args.named("height")?,
diameter => diameter, diameter => diameter,
}; };
shape_impl(args, ShapeKind::Circle, width, height) let sizing = Spec::new(width, height);
shape_impl(args, ShapeKind::Circle, sizing)
} }
fn shape_impl( fn shape_impl(
args: &mut Args, args: &mut Args,
kind: ShapeKind, kind: ShapeKind,
width: Option<Linear>, sizing: Spec<Option<Linear>>,
height: Option<Linear>,
) -> TypResult<Value> { ) -> TypResult<Value> {
// The default appearance of a shape. // The default appearance of a shape.
let default = Stroke { let default = Stroke {
@ -67,7 +66,10 @@ fn shape_impl(
}), }),
}; };
// Shorthand for padding.
let padding = Sides::splat(args.named("padding")?.unwrap_or_default()); let padding = Sides::splat(args.named("padding")?.unwrap_or_default());
// The shape's contents.
let body = args.find::<Template>(); let body = args.find::<Template>();
Ok(Value::Template(Template::from_inline(move |style| { Ok(Value::Template(Template::from_inline(move |style| {
@ -78,7 +80,7 @@ fn shape_impl(
child: body.as_ref().map(|body| body.pack(style).padded(padding)), child: body.as_ref().map(|body| body.pack(style).padded(padding)),
} }
.pack() .pack()
.sized(width, height) .sized(sizing)
}))) })))
} }

View File

@ -2,21 +2,19 @@ use super::prelude::*;
/// `box`: Size content and place it into a paragraph. /// `box`: Size content and place it into a paragraph.
pub fn box_(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { pub fn box_(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let width = args.named("width")?; let sizing = Spec::new(args.named("width")?, args.named("height")?);
let height = args.named("height")?;
let body: Template = args.find().unwrap_or_default(); let body: Template = args.find().unwrap_or_default();
Ok(Value::Template(Template::from_inline(move |style| { Ok(Value::Template(Template::from_inline(move |style| {
body.pack(style).sized(width, height) body.pack(style).sized(sizing)
}))) })))
} }
/// `block`: Size content and place it into the flow. /// `block`: Size content and place it into the flow.
pub fn block(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { pub fn block(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let width = args.named("width")?; let sizing = Spec::new(args.named("width")?, args.named("height")?);
let height = args.named("height")?;
let body: Template = args.find().unwrap_or_default(); let body: Template = args.find().unwrap_or_default();
Ok(Value::Template(Template::from_block(move |style| { Ok(Value::Template(Template::from_block(move |style| {
body.pack(style).sized(width, height) body.pack(style).sized(sizing)
}))) })))
} }

View File

@ -2,11 +2,10 @@ use super::prelude::*;
/// `move`: Move content without affecting layout. /// `move`: Move content without affecting layout.
pub fn move_(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { pub fn move_(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let dx = args.named("dx")?; let offset = Spec::new(args.named("x")?, args.named("y")?);
let dy = args.named("dy")?;
let body: Template = args.expect("body")?; let body: Template = args.expect("body")?;
Ok(Value::Template(Template::from_inline(move |style| { Ok(Value::Template(Template::from_inline(move |style| {
body.pack(style).moved(dx, dy) body.pack(style).moved(offset)
}))) })))
} }

View File

@ -10,12 +10,12 @@
Auto-sized circle. \ Auto-sized circle. \
#circle(fill: rgb("eb5278"), thickness: 2pt, #circle(fill: rgb("eb5278"), thickness: 2pt,
align(center, horizon)[But, soft!] align(center + horizon)[But, soft!]
) )
Center-aligned rect in auto-sized circle. Center-aligned rect in auto-sized circle.
#circle(fill: forest, stroke: conifer, #circle(fill: forest, stroke: conifer,
align(center, horizon, align(center + horizon,
rect(fill: conifer, pad(5pt)[But, soft!]) rect(fill: conifer, pad(5pt)[But, soft!])
) )
) )
@ -37,7 +37,7 @@ Expanded by height.
--- ---
// Test relative sizing. // Test relative sizing.
#let centered(body) = align(center, horizon, body) #let centered(body) = align(center + horizon, body)
#font(fill: white) #font(fill: white)
#rect(width: 100pt, height: 50pt, fill: rgb("aaa"), centered[ #rect(width: 100pt, height: 50pt, fill: rgb("aaa"), centered[
#circle(radius: 10pt, fill: eastern, centered[A]) // D=20pt #circle(radius: 10pt, fill: eastern, centered[A]) // D=20pt

View File

@ -9,7 +9,7 @@ Rect in ellipse in fixed rect. \
#rect(width: 3cm, height: 2cm, fill: rgb("2a631a"), #rect(width: 3cm, height: 2cm, fill: rgb("2a631a"),
ellipse(fill: forest, ellipse(fill: forest,
rect(fill: conifer, rect(fill: conifer,
align(center, horizon)[ align(center + horizon)[
Stuff inside an ellipse! Stuff inside an ellipse!
] ]
) )

View File

@ -21,7 +21,7 @@
#image("../../res/tiger.jpg", width: 100%, height: 20pt, fit: "stretch") #image("../../res/tiger.jpg", width: 100%, height: 20pt, fit: "stretch")
// Make sure the bounding-box of the image is correct. // Make sure the bounding-box of the image is correct.
#align(bottom, right, image("../../res/tiger.jpg", width: 40pt)) #align(bottom + right, image("../../res/tiger.jpg", width: 40pt))
--- ---
// Test all three fit modes. // Test all three fit modes.

View File

@ -7,7 +7,7 @@
align(center, square(size: 20pt, fill: eastern)), align(center, square(size: 20pt, fill: eastern)),
align(right, square(size: 15pt, fill: eastern)), align(right, square(size: 15pt, fill: eastern)),
) )
#align(center, horizon, rect(fill: eastern, height: 10pt)) #align(center + horizon, rect(fill: eastern, height: 10pt))
#align(bottom, stack( #align(bottom, stack(
align(center, rect(fill: conifer, height: 10pt)), align(center, rect(fill: conifer, height: 10pt)),
rect(fill: forest, height: 10pt), rect(fill: forest, height: 10pt),
@ -19,3 +19,17 @@
Dolor Dolor
] ]
---
// Ref: false
#test(type(center), "alignment")
#test(type(horizon), "alignment")
#test(type(center + horizon), "2d alignment")
---
// Error: 8-22 cannot add two horizontal alignments
#align(center + right, [A])
---
// Error: 8-20 cannot add two vertical alignments
#align(top + bottom, [A])

View File

@ -13,6 +13,6 @@
height: 100% + 20pt, height: 100% + 20pt,
) )
) )
#align(bottom, right)[ #align(bottom + right)[
_Welcome to_ #underline[*Tigerland*] _Welcome to_ #underline[*Tigerland*]
] ]

View File

@ -2,7 +2,7 @@
#let tex = [{ #let tex = [{
[T] [T]
h(-0.14 * size) h(-0.14 * size)
move(dy: 0.22 * size)[E] move(y: 0.22 * size)[E]
h(-0.12 * size) h(-0.12 * size)
[X] [X]
}] }]

View File

@ -9,8 +9,8 @@
// Set all margins at once. // Set all margins at once.
[ [
#page(margins: 5pt) #page(margins: 5pt)
#place(top, left)[TL] #place(top + left)[TL]
#place(bottom, right)[BR] #place(bottom + right)[BR]
] ]
// Set individual margins. // Set individual margins.

View File

@ -1,5 +1,5 @@
#page("a8") #page("a8")
#place(bottom, center)[© Typst] #place(bottom + center)[© Typst]
= Placement = Placement
#place(right, image("../../res/tiger.jpg", width: 1.8cm)) #place(right, image("../../res/tiger.jpg", width: 1.8cm))

View File

@ -15,3 +15,7 @@ It is the east, and Juliet is the sun.
--- ---
// Error: 13-16 must be horizontal // Error: 13-16 must be horizontal
#par(align: top) #par(align: top)
---
// Error: 13-29 expected alignment, found 2d alignment
#par(align: horizon + center)