mirror of
https://github.com/typst/typst
synced 2025-05-13 20:46:23 +08:00
2d alignments with plus operator
This commit is contained in:
parent
d3f6040ced
commit
4f9e5819bb
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
})))
|
||||
}
|
||||
|
||||
|
@ -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()),
|
||||
|
@ -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| {
|
||||
|
@ -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),
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
@ -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)
|
||||
})))
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
})))
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
})))
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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!
|
||||
]
|
||||
)
|
||||
|
@ -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.
|
||||
|
@ -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])
|
||||
|
@ -13,6 +13,6 @@
|
||||
height: 100% + 20pt,
|
||||
)
|
||||
)
|
||||
#align(bottom, right)[
|
||||
#align(bottom + right)[
|
||||
_Welcome to_ #underline[*Tigerland*]
|
||||
]
|
||||
|
@ -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]
|
||||
}]
|
||||
|
@ -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.
|
||||
|
@ -1,5 +1,5 @@
|
||||
#page("a8")
|
||||
#place(bottom, center)[© Typst]
|
||||
#place(bottom + center)[© Typst]
|
||||
|
||||
= Placement
|
||||
#place(right, image("../../res/tiger.jpg", width: 1.8cm))
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user