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 => {
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());
continue;
}

View File

@ -1,8 +1,9 @@
use std::cmp::Ordering;
use std::convert::TryFrom;
use super::Value;
use super::{Dynamic, Value};
use crate::diag::StrResult;
use crate::geom::{Align, Get, Spec};
use crate::util::EcoString;
use Value::*;
@ -87,7 +88,25 @@ pub fn add(lhs: Value, rhs: Value) -> StrResult<Value> {
(Template(a), Str(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.
pub fn downcast_ref<T: 'static>(&self) -> Option<&T> {
pub fn downcast<T: 'static>(&self) -> Option<&T> {
self.0.as_any().downcast_ref()
}
@ -225,7 +225,7 @@ where
}
fn dyn_eq(&self, other: &Dynamic) -> bool {
if let Some(other) = other.downcast_ref::<Self>() {
if let Some(other) = other.downcast::<Self>() {
self == other
} else {
false
@ -334,7 +334,7 @@ macro_rules! castable {
let found = match value {
$($pattern => return Ok($out),)*
$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);
})*
dynamic.type_name()

View File

@ -123,7 +123,7 @@ impl<T: Debug> Debug for Spec<T> {
}
/// The two specific layouting axes.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub enum SpecAxis {
/// The horizontal layouting axis.
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.
pub fn sized(self, w: Option<Linear>, h: Option<Linear>) -> Self {
if w.is_some() || h.is_some() {
SizedNode { child: self, sizing: Spec::new(w, h) }.pack()
pub fn sized(self, sizing: Spec<Option<Linear>>) -> Self {
if sizing.any(Option::is_some) {
SizedNode { child: self, sizing }.pack()
} else {
self
}
}
/// Set alignments for this node.
pub fn aligned(self, x: Option<Align>, y: Option<Align>) -> Self {
if x.is_some() || y.is_some() {
AlignNode { child: self, aligns: Spec::new(x, y) }.pack()
pub fn aligned(self, aligns: Spec<Option<Align>>) -> Self {
if aligns.any(Option::is_some) {
AlignNode { child: self, aligns }.pack()
} else {
self
}
}
/// Move this node's contents without affecting layout.
pub fn moved(self, dx: Option<Linear>, dy: Option<Linear>) -> Self {
if dx.is_some() || dy.is_some() {
MoveNode { child: self, offset: Spec::new(dx, dy) }.pack()
pub fn moved(self, offset: Spec<Option<Linear>>) -> Self {
if offset.any(Option::is_some) {
MoveNode { child: self, offset }.pack()
} else {
self
}

View File

@ -2,32 +2,18 @@ use super::prelude::*;
/// `align`: Configure the alignment along the layouting axes.
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")?;
Ok(Value::Template(Template::from_block(move |style| {
let mut style = style.clone();
if let Some(x) = x {
if let Some(x) = aligns.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.
#[derive(Debug, Hash)]
pub struct AlignNode {

View File

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

View File

@ -141,6 +141,15 @@ dynamic! {
Align: "alignment",
}
dynamic! {
Spec<Option<Align>>: "2d alignment",
@align: Align => {
let mut aligns = Spec::default();
aligns.set(align.axis(), Some(*align));
aligns
},
}
dynamic! {
FontFamily: "font family",
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 v.axis() == SpecAxis::Horizontal {
dir = Some(v);
} else {
if v.axis() != SpecAxis::Horizontal {
bail!(span, "must be horizontal");
}
dir = Some(v);
}
let mut align = None;
if let Some(Spanned { v, span }) = args.named::<Spanned<Align>>("align")? {
if v.axis() == SpecAxis::Horizontal {
align = Some(v);
} else {
if v.axis() != SpecAxis::Horizontal {
bail!(span, "must be horizontal");
}
align = Some(v);
}
ctx.template.modify(move |style| {

View File

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

View File

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

View File

@ -2,21 +2,19 @@ use super::prelude::*;
/// `box`: Size content and place it into a paragraph.
pub fn box_(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let width = args.named("width")?;
let height = args.named("height")?;
let sizing = Spec::new(args.named("width")?, args.named("height")?);
let body: Template = args.find().unwrap_or_default();
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.
pub fn block(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let width = args.named("width")?;
let height = args.named("height")?;
let sizing = Spec::new(args.named("width")?, args.named("height")?);
let body: Template = args.find().unwrap_or_default();
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.
pub fn move_(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
let dx = args.named("dx")?;
let dy = args.named("dy")?;
let offset = Spec::new(args.named("x")?, args.named("y")?);
let body: Template = args.expect("body")?;
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. \
#circle(fill: rgb("eb5278"), thickness: 2pt,
align(center, horizon)[But, soft!]
align(center + horizon)[But, soft!]
)
Center-aligned rect in auto-sized circle.
#circle(fill: forest, stroke: conifer,
align(center, horizon,
align(center + horizon,
rect(fill: conifer, pad(5pt)[But, soft!])
)
)
@ -37,7 +37,7 @@ Expanded by height.
---
// Test relative sizing.
#let centered(body) = align(center, horizon, body)
#let centered(body) = align(center + horizon, body)
#font(fill: white)
#rect(width: 100pt, height: 50pt, fill: rgb("aaa"), centered[
#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"),
ellipse(fill: forest,
rect(fill: conifer,
align(center, horizon)[
align(center + horizon)[
Stuff inside an ellipse!
]
)

View File

@ -21,7 +21,7 @@
#image("../../res/tiger.jpg", width: 100%, height: 20pt, fit: "stretch")
// 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.

View File

@ -7,7 +7,7 @@
align(center, square(size: 20pt, 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(center, rect(fill: conifer, height: 10pt)),
rect(fill: forest, height: 10pt),
@ -19,3 +19,17 @@
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,
)
)
#align(bottom, right)[
#align(bottom + right)[
_Welcome to_ #underline[*Tigerland*]
]

View File

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

View File

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

View File

@ -1,5 +1,5 @@
#page("a8")
#place(bottom, center)[© Typst]
#place(bottom + center)[© Typst]
= Placement
#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
#par(align: top)
---
// Error: 13-29 expected alignment, found 2d alignment
#par(align: horizon + center)