summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/exec/mod.rs1
-rw-r--r--src/export/pdf.rs210
-rw-r--r--src/geom/length.rs4
-rw-r--r--src/geom/mod.rs2
-rw-r--r--src/geom/path.rs60
-rw-r--r--src/geom/relative.rs4
-rw-r--r--src/geom/sides.rs2
-rw-r--r--src/geom/size.rs8
-rw-r--r--src/layout/background.rs29
-rw-r--r--src/layout/fixed.rs16
-rw-r--r--src/layout/frame.rs8
-rw-r--r--src/layout/mod.rs47
-rw-r--r--src/layout/pad.rs35
-rw-r--r--src/layout/par.rs16
-rw-r--r--src/layout/stack.rs35
-rw-r--r--src/library/font.rs2
-rw-r--r--src/library/image.rs10
-rw-r--r--src/library/mod.rs3
-rw-r--r--src/library/shapes.rs155
-rw-r--r--src/library/spacing.rs7
-rw-r--r--tests/ref/library/circle.pngbin0 -> 13617 bytes
-rw-r--r--tests/ref/library/ellipse.pngbin0 -> 7638 bytes
-rw-r--r--tests/ref/library/pagebreak.pngbin803 -> 1357 bytes
-rw-r--r--tests/ref/library/rect.pngbin0 -> 2769 bytes
-rw-r--r--tests/ref/library/shapes.pngbin3136 -> 0 bytes
-rw-r--r--tests/ref/library/square.pngbin0 -> 7166 bytes
-rw-r--r--tests/typ/library/base.typ2
-rw-r--r--tests/typ/library/circle.typ41
-rw-r--r--tests/typ/library/ellipse.typ16
-rw-r--r--tests/typ/library/pagebreak.typ17
-rw-r--r--tests/typ/library/rect.typ27
-rw-r--r--tests/typ/library/shapes.typ42
-rw-r--r--tests/typ/library/square.typ31
-rw-r--r--tests/typeset.rs79
34 files changed, 664 insertions, 245 deletions
diff --git a/src/exec/mod.rs b/src/exec/mod.rs
index 90e5a225..5a2ff698 100644
--- a/src/exec/mod.rs
+++ b/src/exec/mod.rs
@@ -122,6 +122,7 @@ impl Exec for RawNode {
ctx.push(FixedNode {
width: None,
height: None,
+ aspect: None,
child: StackNode {
dirs: ctx.state.dirs,
aligns: ctx.state.aligns,
diff --git a/src/export/pdf.rs b/src/export/pdf.rs
index d8391e2d..6881188d 100644
--- a/src/export/pdf.rs
+++ b/src/export/pdf.rs
@@ -15,8 +15,8 @@ use ttf_parser::{name_id, GlyphId};
use crate::color::Color;
use crate::env::{Env, ImageResource, ResourceId};
-use crate::geom::Length;
-use crate::layout::{Element, Fill, Frame, Shape};
+use crate::geom::{self, Length, Size};
+use crate::layout::{Element, Fill, Frame, Image, Shape};
/// Export a collection of frames into a _PDF_ document.
///
@@ -134,63 +134,59 @@ impl<'a> PdfExporter<'a> {
let mut face = FaceId::MAX;
let mut size = Length::ZERO;
let mut fill: Option<Fill> = None;
- let mut change_color = |content: &mut Content, new_fill: Fill| {
- if fill != Some(new_fill) {
- match new_fill {
- Fill::Color(Color::Rgba(c)) => {
- content.fill_rgb(
- c.r as f32 / 255.0,
- c.g as f32 / 255.0,
- c.b as f32 / 255.0,
- );
- }
- Fill::Image(_) => todo!(),
- }
- fill = Some(new_fill);
- }
- };
for (pos, element) in &page.elements {
let x = pos.x.to_pt() as f32;
+ let y = (page.size.height - pos.y).to_pt() as f32;
+
match element {
- Element::Image(image) => {
- let name = format!("Im{}", self.images.map(image.res));
- let size = image.size;
- let y = (page.size.height - pos.y - size.height).to_pt() as f32;
- let w = size.width.to_pt() as f32;
- let h = size.height.to_pt() as f32;
+ &Element::Image(Image { res, size: Size { width, height } }) => {
+ let name = format!("Im{}", self.images.map(res));
+ let w = width.to_pt() as f32;
+ let h = height.to_pt() as f32;
content.save_state();
- content.matrix(w, 0.0, 0.0, h, x, y);
+ content.matrix(w, 0.0, 0.0, h, x, y - h);
content.x_object(Name(name.as_bytes()));
content.restore_state();
}
Element::Geometry(geometry) => {
content.save_state();
- change_color(&mut content, geometry.fill);
+ write_fill(&mut content, geometry.fill);
- match &geometry.shape {
- Shape::Rect(r) => {
- let w = r.width.to_pt() as f32;
- let h = r.height.to_pt() as f32;
- let y = (page.size.height - pos.y - r.height).to_pt() as f32;
+ match geometry.shape {
+ Shape::Rect(Size { width, height }) => {
+ let w = width.to_pt() as f32;
+ let h = height.to_pt() as f32;
if w > 0.0 && h > 0.0 {
- content.rect(x, y, w, h, false, true);
+ content.rect(x, y - h, w, h, false, true);
}
}
+
+ Shape::Ellipse(size) => {
+ let path = geom::ellipse_path(size);
+ write_path(&mut content, x, y, &path, false, true);
+ }
+
+ Shape::Path(ref path) => {
+ write_path(&mut content, x, y, path, false, true)
+ }
}
content.restore_state();
}
Element::Text(shaped) => {
- change_color(&mut content, shaped.color);
+ if fill != Some(shaped.color) {
+ write_fill(&mut content, shaped.color);
+ fill = Some(shaped.color);
+ }
let mut text = content.text();
- // Then, also check if we need to
- // issue a font switching action.
+ // Then, also check if we need to issue a font switching
+ // action.
if shaped.face != face || shaped.font_size != size {
face = shaped.face;
size = shaped.font_size;
@@ -199,8 +195,6 @@ impl<'a> PdfExporter<'a> {
text.font(Name(name.as_bytes()), size.to_pt() as f32);
}
- let x = pos.x.to_pt() as f32;
- let y = (page.size.height - pos.y).to_pt() as f32;
text.matrix(1.0, 0.0, 0.0, 1.0, x, y);
text.show(&shaped.encode_glyphs_be());
}
@@ -365,6 +359,97 @@ impl<'a> PdfExporter<'a> {
}
}
+/// Write a fill change into a content stream.
+fn write_fill(content: &mut Content, fill: Fill) {
+ match fill {
+ Fill::Color(Color::Rgba(c)) => {
+ content.fill_rgb(c.r as f32 / 255.0, c.g as f32 / 255.0, c.b as f32 / 255.0);
+ }
+ Fill::Image(_) => todo!(),
+ }
+}
+
+/// Write a path into a content stream.
+fn write_path(
+ content: &mut Content,
+ x: f32,
+ y: f32,
+ path: &geom::Path,
+ stroke: bool,
+ fill: bool,
+) {
+ let f = |length: Length| length.to_pt() as f32;
+ let mut builder = content.path(stroke, fill);
+ for elem in &path.0 {
+ match elem {
+ geom::PathElement::MoveTo(p) => builder.move_to(x + f(p.x), y + f(p.y)),
+ geom::PathElement::LineTo(p) => builder.line_to(x + f(p.x), y + f(p.y)),
+ geom::PathElement::CubicTo(p1, p2, p3) => builder.cubic_to(
+ x + f(p1.x),
+ y + f(p1.y),
+ x + f(p2.x),
+ y + f(p2.y),
+ x + f(p3.x),
+ y + f(p3.y),
+ ),
+ geom::PathElement::ClosePath => builder.close_path(),
+ };
+ }
+}
+
+/// The compression level for the deflating.
+const DEFLATE_LEVEL: u8 = 6;
+
+/// Encode an image with a suitable filter.
+///
+/// Skips the alpha channel as that's encoded separately.
+fn encode_image(img: &ImageResource) -> ImageResult<(Vec<u8>, Filter, ColorSpace)> {
+ let mut data = vec![];
+ let (filter, space) = match (img.format, &img.buf) {
+ // 8-bit gray JPEG.
+ (ImageFormat::Jpeg, DynamicImage::ImageLuma8(_)) => {
+ img.buf.write_to(&mut data, img.format)?;
+ (Filter::DctDecode, ColorSpace::DeviceGray)
+ }
+
+ // 8-bit Rgb JPEG (Cmyk JPEGs get converted to Rgb earlier).
+ (ImageFormat::Jpeg, DynamicImage::ImageRgb8(_)) => {
+ img.buf.write_to(&mut data, img.format)?;
+ (Filter::DctDecode, ColorSpace::DeviceRgb)
+ }
+
+ // TODO: Encode flate streams with PNG-predictor?
+
+ // 8-bit gray PNG.
+ (ImageFormat::Png, DynamicImage::ImageLuma8(luma)) => {
+ data = deflate::compress_to_vec_zlib(&luma.as_raw(), DEFLATE_LEVEL);
+ (Filter::FlateDecode, ColorSpace::DeviceGray)
+ }
+
+ // Anything else (including Rgb(a) PNGs).
+ (_, buf) => {
+ let (width, height) = buf.dimensions();
+ let mut pixels = Vec::with_capacity(3 * width as usize * height as usize);
+ for (_, _, Rgba([r, g, b, _])) in buf.pixels() {
+ pixels.push(r);
+ pixels.push(g);
+ pixels.push(b);
+ }
+
+ data = deflate::compress_to_vec_zlib(&pixels, DEFLATE_LEVEL);
+ (Filter::FlateDecode, ColorSpace::DeviceRgb)
+ }
+ };
+ Ok((data, filter, space))
+}
+
+/// Encode an image's alpha channel if present.
+fn encode_alpha(img: &ImageResource) -> (Vec<u8>, Filter) {
+ let pixels: Vec<_> = img.buf.pixels().map(|(_, _, Rgba([_, _, _, a]))| a).collect();
+ let data = deflate::compress_to_vec_zlib(&pixels, DEFLATE_LEVEL);
+ (data, Filter::FlateDecode)
+}
+
/// We need to know exactly which indirect reference id will be used for which
/// objects up-front to correctly declare the document catalogue, page tree and
/// so on. These offsets are computed in the beginning and stored here.
@@ -485,56 +570,3 @@ where
self.to_layout.iter().copied()
}
}
-
-/// The compression level for the deflating.
-const DEFLATE_LEVEL: u8 = 6;
-
-/// Encode an image with a suitable filter.
-///
-/// Skips the alpha channel as that's encoded separately.
-fn encode_image(img: &ImageResource) -> ImageResult<(Vec<u8>, Filter, ColorSpace)> {
- let mut data = vec![];
- let (filter, space) = match (img.format, &img.buf) {
- // 8-bit gray JPEG.
- (ImageFormat::Jpeg, DynamicImage::ImageLuma8(_)) => {
- img.buf.write_to(&mut data, img.format)?;
- (Filter::DctDecode, ColorSpace::DeviceGray)
- }
-
- // 8-bit Rgb JPEG (Cmyk JPEGs get converted to Rgb earlier).
- (ImageFormat::Jpeg, DynamicImage::ImageRgb8(_)) => {
- img.buf.write_to(&mut data, img.format)?;
- (Filter::DctDecode, ColorSpace::DeviceRgb)
- }
-
- // TODO: Encode flate streams with PNG-predictor?
-
- // 8-bit gray PNG.
- (ImageFormat::Png, DynamicImage::ImageLuma8(luma)) => {
- data = deflate::compress_to_vec_zlib(&luma.as_raw(), DEFLATE_LEVEL);
- (Filter::FlateDecode, ColorSpace::DeviceGray)
- }
-
- // Anything else (including Rgb(a) PNGs).
- (_, buf) => {
- let (width, height) = buf.dimensions();
- let mut pixels = Vec::with_capacity(3 * width as usize * height as usize);
- for (_, _, Rgba([r, g, b, _])) in buf.pixels() {
- pixels.push(r);
- pixels.push(g);
- pixels.push(b);
- }
-
- data = deflate::compress_to_vec_zlib(&pixels, DEFLATE_LEVEL);
- (Filter::FlateDecode, ColorSpace::DeviceRgb)
- }
- };
- Ok((data, filter, space))
-}
-
-/// Encode an image's alpha channel if present.
-fn encode_alpha(img: &ImageResource) -> (Vec<u8>, Filter) {
- let pixels: Vec<_> = img.buf.pixels().map(|(_, _, Rgba([_, _, _, a]))| a).collect();
- let data = deflate::compress_to_vec_zlib(&pixels, DEFLATE_LEVEL);
- (data, Filter::FlateDecode)
-}
diff --git a/src/geom/length.rs b/src/geom/length.rs
index 419da5c3..1175876c 100644
--- a/src/geom/length.rs
+++ b/src/geom/length.rs
@@ -32,7 +32,7 @@ impl Length {
}
/// Create a length from a number of raw units.
- pub fn raw(raw: f64) -> Self {
+ pub const fn raw(raw: f64) -> Self {
Self { raw }
}
@@ -57,7 +57,7 @@ impl Length {
}
/// Get the value of this length in raw units.
- pub fn to_raw(self) -> f64 {
+ pub const fn to_raw(self) -> f64 {
self.raw
}
diff --git a/src/geom/mod.rs b/src/geom/mod.rs
index 5d906834..5099c6b0 100644
--- a/src/geom/mod.rs
+++ b/src/geom/mod.rs
@@ -8,6 +8,7 @@ mod dir;
mod gen;
mod length;
mod linear;
+mod path;
mod point;
mod relative;
mod sides;
@@ -20,6 +21,7 @@ pub use dir::*;
pub use gen::*;
pub use length::*;
pub use linear::*;
+pub use path::*;
pub use point::*;
pub use relative::*;
pub use sides::*;
diff --git a/src/geom/path.rs b/src/geom/path.rs
new file mode 100644
index 00000000..c9fcf1c0
--- /dev/null
+++ b/src/geom/path.rs
@@ -0,0 +1,60 @@
+use super::*;
+
+/// A bezier path.
+#[derive(Default, Debug, Clone, PartialEq)]
+pub struct Path(pub Vec<PathElement>);
+
+/// An element in a bezier path.
+#[derive(Debug, Clone, PartialEq)]
+pub enum PathElement {
+ MoveTo(Point),
+ LineTo(Point),
+ CubicTo(Point, Point, Point),
+ ClosePath,
+}
+
+impl Path {
+ /// Create an empty path.
+ pub fn new() -> Self {
+ Self(vec![])
+ }
+
+ /// Push a [`MoveTo`](PathElement::MoveTo) element.
+ pub fn move_to(&mut self, p: Point) {
+ self.0.push(PathElement::MoveTo(p));
+ }
+
+ /// Push a [`LineTo`](PathElement::LineTo) element.
+ pub fn line_to(&mut self, p: Point) {
+ self.0.push(PathElement::LineTo(p));
+ }
+
+ /// Push a [`CubicTo`](PathElement::CubicTo) element.
+ pub fn cubic_to(&mut self, p1: Point, p2: Point, p3: Point) {
+ self.0.push(PathElement::CubicTo(p1, p2, p3));
+ }
+
+ /// Push a [`ClosePath`](PathElement::ClosePath) element.
+ pub fn close_path(&mut self) {
+ self.0.push(PathElement::ClosePath);
+ }
+}
+
+/// Create a path that approximates an axis-aligned ellipse.
+pub fn ellipse_path(size: Size) -> Path {
+ // https://stackoverflow.com/a/2007782
+ let rx = size.width / 2.0;
+ let ry = size.height / 2.0;
+ let m = 0.551784;
+ let mx = m * rx;
+ let my = m * ry;
+ let z = Length::ZERO;
+ let point = Point::new;
+ let mut path = Path::new();
+ path.move_to(point(-rx, z));
+ path.cubic_to(point(-rx, my), point(-mx, ry), point(z, ry));
+ path.cubic_to(point(mx, ry), point(rx, my), point(rx, z));
+ path.cubic_to(point(rx, -my), point(mx, -ry), point(z, -ry));
+ path.cubic_to(point(-mx, -ry), point(-rx, -my), point(z - rx, z));
+ path
+}
diff --git a/src/geom/relative.rs b/src/geom/relative.rs
index 9d7b3d3e..65312e99 100644
--- a/src/geom/relative.rs
+++ b/src/geom/relative.rs
@@ -26,8 +26,8 @@ impl Relative {
/// Resolve this relative to the given `length`.
pub fn resolve(self, length: Length) -> Length {
- // Zero wins over infinity.
- if self.is_zero() {
+ // We don't want NaNs.
+ if length.is_infinite() {
Length::ZERO
} else {
self.get() * length
diff --git a/src/geom/sides.rs b/src/geom/sides.rs
index 292f00c4..deeced45 100644
--- a/src/geom/sides.rs
+++ b/src/geom/sides.rs
@@ -34,7 +34,7 @@ impl<T> Sides<T> {
}
impl Sides<Linear> {
- /// Resolve the linear margins relative to the given `size`.
+ /// Resolve the linear sides relative to the given `size`.
pub fn resolve(self, size: Size) -> Sides<Length> {
Sides {
left: self.left.resolve(size.width),
diff --git a/src/geom/size.rs b/src/geom/size.rs
index 67e5643d..2feaa950 100644
--- a/src/geom/size.rs
+++ b/src/geom/size.rs
@@ -28,7 +28,8 @@ impl Size {
/// Whether the other size fits into this one (smaller width and height).
pub fn fits(self, other: Self) -> bool {
- self.width >= other.width && self.height >= other.height
+ const EPS: Length = Length::raw(1e-6);
+ self.width + EPS >= other.width && self.height + EPS >= other.height
}
/// Whether both components are finite.
@@ -45,6 +46,11 @@ impl Size {
pub fn is_nan(self) -> bool {
self.width.is_nan() || self.height.is_nan()
}
+
+ /// Convert to a point.
+ pub fn to_point(self) -> Point {
+ Point::new(self.width, self.height)
+ }
}
impl Get<SpecAxis> for Size {
diff --git a/src/layout/background.rs b/src/layout/background.rs
index bb155073..17280a86 100644
--- a/src/layout/background.rs
+++ b/src/layout/background.rs
@@ -3,25 +3,38 @@ use super::*;
/// A node that places a rectangular filled background behind its child.
#[derive(Debug, Clone, PartialEq)]
pub struct BackgroundNode {
+ /// The kind of shape to use as a background.
+ pub shape: BackgroundShape,
/// The background fill.
pub fill: Fill,
/// The child node to be filled.
pub child: Node,
}
+/// The kind of shape to use as a background.
+#[derive(Debug, Clone, PartialEq)]
+pub enum BackgroundShape {
+ Rect,
+ Ellipse,
+}
+
impl Layout for BackgroundNode {
fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Fragment {
- let mut layouted = self.child.layout(ctx, areas);
+ let mut fragment = self.child.layout(ctx, areas);
+
+ for frame in fragment.frames_mut() {
+ let (point, shape) = match self.shape {
+ BackgroundShape::Rect => (Point::ZERO, Shape::Rect(frame.size)),
+ BackgroundShape::Ellipse => {
+ (frame.size.to_point() / 2.0, Shape::Ellipse(frame.size))
+ }
+ };
- for frame in layouted.frames_mut() {
- let element = Element::Geometry(Geometry {
- shape: Shape::Rect(frame.size),
- fill: self.fill,
- });
- frame.elements.insert(0, (Point::ZERO, element));
+ let element = Element::Geometry(Geometry { shape, fill: self.fill });
+ frame.elements.insert(0, (point, element));
}
- layouted
+ fragment
}
}
diff --git a/src/layout/fixed.rs b/src/layout/fixed.rs
index e3365668..22c45ef1 100644
--- a/src/layout/fixed.rs
+++ b/src/layout/fixed.rs
@@ -7,6 +7,10 @@ pub struct FixedNode {
pub width: Option<Linear>,
/// The fixed height, if any.
pub height: Option<Linear>,
+ /// The fixed aspect ratio between width and height, if any.
+ ///
+ /// The resulting frame will satisfy `width = aspect * height`.
+ pub aspect: Option<f64>,
/// The child node whose size to fix.
pub child: Node,
}
@@ -14,18 +18,26 @@ pub struct FixedNode {
impl Layout for FixedNode {
fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Fragment {
let Areas { current, full, .. } = areas;
- let size = Size::new(
+
+ let full = Size::new(
self.width.map(|w| w.resolve(full.width)).unwrap_or(current.width),
self.height.map(|h| h.resolve(full.height)).unwrap_or(current.height),
);
+ let mut size = full;
+ if let Some(aspect) = self.aspect {
+ // Shrink the size to ensure that the aspect ratio can be satisfied.
+ let width = size.width.min(aspect * size.height);
+ size = Size::new(width, width / aspect);
+ }
+
let fill_if = |cond| if cond { Expand::Fill } else { Expand::Fit };
let expand = Spec::new(
fill_if(self.width.is_some()),
fill_if(self.height.is_some()),
);
- let areas = Areas::once(size, expand);
+ let areas = Areas::once(size, full, expand).with_aspect(self.aspect);
self.child.layout(ctx, &areas)
}
}
diff --git a/src/layout/frame.rs b/src/layout/frame.rs
index c85d7539..6e876151 100644
--- a/src/layout/frame.rs
+++ b/src/layout/frame.rs
@@ -1,7 +1,7 @@
use super::Shaped;
use crate::color::Color;
use crate::env::ResourceId;
-use crate::geom::{Point, Size};
+use crate::geom::{Path, Point, Size};
/// A finished layout with elements at fixed positions.
#[derive(Debug, Clone, PartialEq)]
@@ -59,8 +59,12 @@ pub struct Geometry {
/// Some shape.
#[derive(Debug, Clone, PartialEq)]
pub enum Shape {
- /// A rectangle.
+ /// A rectangle with its origin in the topleft corner.
Rect(Size),
+ /// An ellipse with its origin in the center.
+ Ellipse(Size),
+ /// A bezier path.
+ Path(Path),
}
/// The kind of graphic fill to be applied to a [`Shape`].
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index ae4ab89d..360c9d84 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -58,8 +58,7 @@ impl PageRun {
/// Layout the page run.
pub fn layout(&self, ctx: &mut LayoutContext) -> Vec<Frame> {
let areas = Areas::repeat(self.size, Spec::uniform(Expand::Fill));
- let layouted = self.child.layout(ctx, &areas);
- layouted.into_frames()
+ self.child.layout(ctx, &areas).into_frames()
}
}
@@ -89,29 +88,59 @@ pub struct Areas {
pub last: Option<Size>,
/// Whether the frames resulting from layouting into this areas should be
/// shrunk to fit their content or expanded to fill the area.
+ ///
+ /// This property is handled partially by the par layouter and fully by the
+ /// stack layouter.
pub expand: Spec<Expand>,
+ /// The aspect ratio the resulting frame should respect.
+ ///
+ /// This property is only handled by the stack layouter.
+ pub aspect: Option<f64>,
}
impl Areas {
- /// Create a new length-1 sequence of areas with just one `area`.
- pub fn once(size: Size, expand: Spec<Expand>) -> Self {
+ /// Create a new sequence of areas that repeats `area` indefinitely.
+ pub fn repeat(size: Size, expand: Spec<Expand>) -> Self {
Self {
current: size,
full: size,
backlog: vec![],
- last: None,
+ last: Some(size),
expand,
+ aspect: None,
}
}
- /// Create a new sequence of areas that repeats `area` indefinitely.
- pub fn repeat(size: Size, expand: Spec<Expand>) -> Self {
+ /// Create a new length-1 sequence of areas with just one `area`.
+ pub fn once(size: Size, full: Size, expand: Spec<Expand>) -> Self {
Self {
current: size,
- full: size,
+ full,
backlog: vec![],
- last: Some(size),
+ last: None,
expand,
+ aspect: None,
+ }
+ }
+
+ /// Builder-style method for setting the aspect ratio.
+ pub fn with_aspect(mut self, aspect: Option<f64>) -> Self {
+ self.aspect = aspect;
+ self
+ }
+
+ /// Map all areas.
+ pub fn map<F>(&self, mut f: F) -> Self
+ where
+ F: FnMut(Size) -> Size,
+ {
+ Self {
+ current: f(self.current),
+ full: f(self.full),
+ backlog: self.backlog.iter().copied().map(|s| f(s)).collect(),
+ last: self.last.map(f),
+ expand: self.expand,
+ aspect: self.aspect,
}
}
diff --git a/src/layout/pad.rs b/src/layout/pad.rs
index 33ce217d..fb038996 100644
--- a/src/layout/pad.rs
+++ b/src/layout/pad.rs
@@ -13,12 +13,12 @@ impl Layout for PadNode {
fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Fragment {
let areas = shrink(areas, self.padding);
- let mut layouted = self.child.layout(ctx, &areas);
- for frame in layouted.frames_mut() {
+ let mut fragment = self.child.layout(ctx, &areas);
+ for frame in fragment.frames_mut() {
pad(frame, self.padding);
}
- layouted
+ fragment
}
}
@@ -30,23 +30,30 @@ impl From<PadNode> for AnyNode {
/// Shrink all areas by the padding.
fn shrink(areas: &Areas, padding: Sides<Linear>) -> Areas {
- let shrink = |size| size - padding.resolve(size).size();
- Areas {
- current: shrink(areas.current),
- full: shrink(areas.full),
- backlog: areas.backlog.iter().copied().map(shrink).collect(),
- last: areas.last.map(shrink),
- expand: areas.expand,
- }
+ areas.map(|size| size - padding.resolve(size).size())
}
-/// Enlarge the frame and move all elements inwards.
+/// Pad the frame and move all elements inwards.
fn pad(frame: &mut Frame, padding: Sides<Linear>) {
- let padding = padding.resolve(frame.size);
+ let padded = solve(padding, frame.size);
+ let padding = padding.resolve(padded);
let origin = Point::new(padding.left, padding.top);
- frame.size += padding.size();
+ frame.size = padded;
for (point, _) in &mut frame.elements {
*point += origin;
}
}
+
+/// Solve for the size `padded` that satisfies (approximately):
+/// `padded - padding.resolve(padded).size() == size`
+fn solve(padding: Sides<Linear>, size: Size) -> Size {
+ fn solve_axis(length: Length, padding: Linear) -> Length {
+ (length + padding.abs) / (1.0 - padding.rel.get())
+ }
+
+ Size::new(
+ solve_axis(size.width, padding.left + padding.right),
+ solve_axis(size.height, padding.top + padding.bottom),
+ )
+}
diff --git a/src/layout/par.rs b/src/layout/par.rs
index e9fda015..0364a03a 100644
--- a/src/layout/par.rs
+++ b/src/layout/par.rs
@@ -18,7 +18,7 @@ pub struct ParNode {
impl Layout for ParNode {
fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Fragment {
- let mut layouter = ParLayouter::new(self, areas.clone());
+ let mut layouter = ParLayouter::new(self.dirs, self.line_spacing, areas.clone());
for child in &self.children {
match child.layout(ctx, &layouter.areas) {
Fragment::Spacing(spacing) => layouter.push_spacing(spacing),
@@ -57,12 +57,12 @@ struct ParLayouter {
}
impl ParLayouter {
- fn new(par: &ParNode, areas: Areas) -> Self {
+ fn new(dirs: LayoutDirs, line_spacing: Length, areas: Areas) -> Self {
Self {
- main: par.dirs.main.axis(),
- cross: par.dirs.cross.axis(),
- dirs: par.dirs,
- line_spacing: par.line_spacing,
+ main: dirs.main.axis(),
+ cross: dirs.cross.axis(),
+ dirs,
+ line_spacing,
areas,
finished: vec![],
lines: vec![],
@@ -134,12 +134,12 @@ impl ParLayouter {
}
fn finish_line(&mut self) {
- let expand = self.areas.expand.switch(self.dirs);
let full_size = {
+ let expand = self.areas.expand.switch(self.dirs);
let full = self.areas.full.switch(self.dirs);
Gen::new(
self.line_size.main,
- expand.cross.resolve(self.line_size.cross.min(full.cross), full.cross),
+ expand.cross.resolve(self.line_size.cross, full.cross),
)
};
diff --git a/src/layout/stack.rs b/src/layout/stack.rs
index 32eba676..6a87290e 100644
--- a/src/layout/stack.rs
+++ b/src/layout/stack.rs
@@ -16,7 +16,7 @@ pub struct StackNode {
impl Layout for StackNode {
fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Fragment {
- let mut layouter = StackLayouter::new(self, areas.clone());
+ let mut layouter = StackLayouter::new(self.dirs, areas.clone());
for child in &self.children {
match child.layout(ctx, &layouter.areas) {
Fragment::Spacing(spacing) => layouter.push_spacing(spacing),
@@ -49,10 +49,10 @@ struct StackLayouter {
}
impl StackLayouter {
- fn new(stack: &StackNode, areas: Areas) -> Self {
+ fn new(dirs: LayoutDirs, areas: Areas) -> Self {
Self {
- main: stack.dirs.main.axis(),
- dirs: stack.dirs,
+ main: dirs.main.axis(),
+ dirs,
areas,
finished: vec![],
frames: vec![],
@@ -93,12 +93,27 @@ impl StackLayouter {
fn finish_area(&mut self) {
let full_size = {
- let expand = self.areas.expand.switch(self.dirs);
- let full = self.areas.full.switch(self.dirs);
- Gen::new(
- expand.main.resolve(self.used.main.min(full.main), full.main),
- expand.cross.resolve(self.used.cross.min(full.cross), full.cross),
- )
+ let expand = self.areas.expand;
+ let full = self.areas.full;
+ let current = self.areas.current;
+ let used = self.used.switch(self.dirs).to_size();
+
+ let mut size = Size::new(
+ expand.horizontal.resolve(used.width, full.width),
+ expand.vertical.resolve(used.height, full.height),
+ );
+
+ if let Some(aspect) = self.areas.aspect {
+ let width = size
+ .width
+ .max(aspect * size.height)
+ .min(current.width)
+ .min((current.height + used.height) / aspect);
+
+ size = Size::new(width, width / aspect);
+ }
+
+ size.switch(self.dirs)
};
let mut output = Frame::new(full_size.switch(self.dirs).to_size());
diff --git a/src/library/font.rs b/src/library/font.rs
index 0993f7f0..ed2c0ef3 100644
--- a/src/library/font.rs
+++ b/src/library/font.rs
@@ -16,7 +16,7 @@ use super::*;
/// - Font Stretch: `stretch`, of type `relative`, between 0.5 and 2.0.
/// - Top edge of the font: `top-edge`, of type `vertical-font-metric`.
/// - Bottom edge of the font: `bottom-edge`, of type `vertical-font-metric`.
-/// - Fill color the glyphs: `color`, of type `color`.
+/// - Color the glyphs: `color`, of type `color`.
/// - Serif family definition: `serif`, of type `font-familiy-list`.
/// - Sans-serif family definition: `sans-serif`, of type `font-familiy-list`.
/// - Monospace family definition: `monospace`, of type `font-familiy-list`.
diff --git a/src/library/image.rs b/src/library/image.rs
index 8cb09463..10217e31 100644
--- a/src/library/image.rs
+++ b/src/library/image.rs
@@ -25,7 +25,7 @@ pub fn image(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
let loaded = ctx.env.resources.load(&path.v, ImageResource::parse);
if let Some((res, img)) = loaded {
let dimensions = img.buf.dimensions();
- ctx.push(NodeImage {
+ ctx.push(ImageNode {
res,
dimensions,
width,
@@ -41,7 +41,7 @@ pub fn image(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
/// An image node.
#[derive(Debug, Clone, PartialEq)]
-struct NodeImage {
+struct ImageNode {
/// How to align this image node in its parent.
aligns: LayoutAligns,
/// The resource id of the image file.
@@ -54,7 +54,7 @@ struct NodeImage {
height: Option<Linear>,
}
-impl Layout for NodeImage {
+impl Layout for ImageNode {
fn layout(&self, _: &mut LayoutContext, areas: &Areas) -> Fragment {
let Areas { current, full, .. } = areas;
@@ -90,8 +90,8 @@ impl Layout for NodeImage {
}
}
-impl From<NodeImage> for AnyNode {
- fn from(image: NodeImage) -> Self {
+impl From<ImageNode> for AnyNode {
+ fn from(image: ImageNode) -> Self {
Self::new(image)
}
}
diff --git a/src/library/mod.rs b/src/library/mod.rs
index d0920cf1..b09f94a0 100644
--- a/src/library/mod.rs
+++ b/src/library/mod.rs
@@ -51,6 +51,8 @@ pub fn new() -> Scope {
}
func!("align", align);
+ func!("circle", circle);
+ func!("ellipse", ellipse);
func!("font", font);
func!("h", h);
func!("image", image);
@@ -58,6 +60,7 @@ pub fn new() -> Scope {
func!("page", page);
func!("pagebreak", pagebreak);
func!("paragraph", par);
+ func!("square", square);
func!("rect", rect);
func!("repr", repr);
func!("rgb", rgb);
diff --git a/src/library/shapes.rs b/src/library/shapes.rs
index 211a4f2e..a5faf73e 100644
--- a/src/library/shapes.rs
+++ b/src/library/shapes.rs
@@ -1,49 +1,162 @@
+use std::f64::consts::SQRT_2;
+
use super::*;
-use crate::layout::{BackgroundNode, Fill, FixedNode};
+use crate::color::Color;
+use crate::layout::{BackgroundNode, BackgroundShape, Fill, FixedNode, PadNode};
-/// `rect`: Create a rectangular box.
+/// `rect`: Create a rectangle.
///
/// # Positional parameters
/// - Body: optional, of type `template`.
///
/// # Named parameters
-/// - Width of the box: `width`, of type `linear` relative to parent width.
-/// - Height of the box: `height`, of type `linear` relative to parent height.
-/// - Main layouting direction: `main-dir`, of type `direction`.
-/// - Cross layouting direction: `cross-dir`, of type `direction`.
-/// - Fill color of the box: `fill`, of type `color`.
+/// - Width: `width`, of type `linear` relative to parent width.
+/// - Height: `height`, of type `linear` relative to parent height.
+/// - Fill color: `fill`, of type `color`.
///
/// # Return value
/// A template that places the body into a rectangle.
-///
-/// # Relevant types and constants
-/// - Type `direction`
-/// - `ltr` (left to right)
-/// - `rtl` (right to left)
-/// - `ttb` (top to bottom)
-/// - `btt` (bottom to top)
pub fn rect(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
let width = args.get(ctx, "width");
let height = args.get(ctx, "height");
- let main = args.get(ctx, "main-dir");
- let cross = args.get(ctx, "cross-dir");
let fill = args.get(ctx, "fill");
let body = args.find::<TemplateValue>(ctx).unwrap_or_default();
+ rect_impl("rect", width, height, None, fill, body)
+}
- Value::template("box", move |ctx| {
+/// `square`: Create a square.
+///
+/// # Positional parameters
+/// - Body: optional, of type `template`.
+///
+/// # Named parameters
+/// - Side length: `length`, of type `length`.
+/// - Width: `width`, of type `linear` relative to parent width.
+/// - Height: `height`, of type `linear` relative to parent height.
+/// - Fill color: `fill`, of type `color`.
+///
+/// Note that you can specify only one of `length`, `width` and `height`. The
+/// width and height parameters exist so that you can size the square relative
+/// to its parent's size, which isn't possible by setting the side length.
+///
+/// # Return value
+/// A template that places the body into a square.
+pub fn square(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
+ let length = args.get::<Length>(ctx, "length").map(Linear::from);
+ let width = length.or_else(|| args.get(ctx, "width"));
+ let height = if width.is_none() { args.get(ctx, "height") } else { None };
+ let fill = args.get(ctx, "fill");
+ let body = args.find::<TemplateValue>(ctx).unwrap_or_default();
+ rect_impl("square", width, height, Some(1.0), fill, body)
+}
+
+fn rect_impl(
+ name: &str,
+ width: Option<Linear>,
+ height: Option<Linear>,
+ aspect: Option<f64>,
+ fill: Option<Color>,
+ body: TemplateValue,
+) -> Value {
+ Value::template(name, move |ctx| {
let snapshot = ctx.state.clone();
+ let child = ctx.exec(&body).into();
+ let node = FixedNode { width, height, aspect, child };
+
+ if let Some(color) = fill {
+ ctx.push(BackgroundNode {
+ shape: BackgroundShape::Rect,
+ fill: Fill::Color(color),
+ child: node.into(),
+ });
+ } else {
+ ctx.push(node);
+ }
+
+ ctx.state = snapshot;
+ })
+}
- ctx.set_dirs(Gen::new(main, cross));
+/// `ellipse`: Create an ellipse.
+///
+/// # Positional parameters
+/// - Body: optional, of type `template`.
+///
+/// # Named parameters
+/// - Width: `width`, of type `linear` relative to parent width.
+/// - Height: `height`, of type `linear` relative to parent height.
+/// - Fill color: `fill`, of type `color`.
+///
+/// # Return value
+/// A template that places the body into an ellipse.
+pub fn ellipse(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
+ let width = args.get(ctx, "width");
+ let height = args.get(ctx, "height");
+ let fill = args.get(ctx, "fill");
+ let body = args.find::<TemplateValue>(ctx).unwrap_or_default();
+ ellipse_impl("ellipse", width, height, None, fill, body)
+}
+/// `circle`: Create a circle.
+///
+/// # Positional parameters
+/// - Body: optional, of type `template`.
+///
+/// # Named parameters
+/// - Radius: `radius`, of type `length`.
+/// - Width: `width`, of type `linear` relative to parent width.
+/// - Height: `height`, of type `linear` relative to parent height.
+/// - Fill color: `fill`, of type `color`.
+///
+/// Note that you can specify only one of `radius`, `width` and `height`. The
+/// width and height parameters exist so that you can size the circle relative
+/// to its parent's size, which isn't possible by setting the radius.
+///
+/// # Return value
+/// A template that places the body into a circle.
+pub fn circle(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
+ let radius = args.get::<Length>(ctx, "radius").map(|r| 2.0 * Linear::from(r));
+ let width = radius.or_else(|| args.get(ctx, "width"));
+ let height = if width.is_none() { args.get(ctx, "height") } else { None };
+ let fill = args.get(ctx, "fill");
+ let body = args.find::<TemplateValue>(ctx).unwrap_or_default();
+ ellipse_impl("circle", width, height, Some(1.0), fill, body)
+}
+
+fn ellipse_impl(
+ name: &str,
+ width: Option<Linear>,
+ height: Option<Linear>,
+ aspect: Option<f64>,
+ fill: Option<Color>,
+ body: TemplateValue,
+) -> Value {
+ Value::template(name, move |ctx| {
+ // This padding ratio ensures that the rectangular padded area fits
+ // perfectly into the ellipse.
+ const PAD: f64 = 0.5 - SQRT_2 / 4.0;
+
+ let snapshot = ctx.state.clone();
let child = ctx.exec(&body).into();
- let fixed = FixedNode { width, height, child };
+ let node = FixedNode {
+ width,
+ height,
+ aspect,
+ child: PadNode {
+ padding: Sides::uniform(Relative::new(PAD).into()),
+ child,
+ }
+ .into(),
+ };
+
if let Some(color) = fill {
ctx.push(BackgroundNode {
+ shape: BackgroundShape::Ellipse,
fill: Fill::Color(color),
- child: fixed.into(),
+ child: node.into(),
});
} else {
- ctx.push(fixed);
+ ctx.push(node);
}
ctx.state = snapshot;
diff --git a/src/library/spacing.rs b/src/library/spacing.rs
index fee802fa..c96b8be4 100644
--- a/src/library/spacing.rs
+++ b/src/library/spacing.rs
@@ -9,7 +9,7 @@ use crate::layout::SpacingNode;
/// # Return value
/// A template that adds horizontal spacing.
pub fn h(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
- spacing(ctx, args, SpecAxis::Horizontal)
+ spacing_impl(ctx, args, SpecAxis::Horizontal)
}
/// `v`: Add vertical spacing.
@@ -20,11 +20,10 @@ pub fn h(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
/// # Return value
/// A template that adds vertical spacing.
pub fn v(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
- spacing(ctx, args, SpecAxis::Vertical)
+ spacing_impl(ctx, args, SpecAxis::Vertical)
}
-/// Apply spacing along a specific axis.
-fn spacing(ctx: &mut EvalContext, args: &mut FuncArgs, axis: SpecAxis) -> Value {
+fn spacing_impl(ctx: &mut EvalContext, args: &mut FuncArgs, axis: SpecAxis) -> Value {
let spacing: Option<Linear> = args.require(ctx, "spacing");
Value::template("spacing", move |ctx| {
if let Some(linear) = spacing {
diff --git a/tests/ref/library/circle.png b/tests/ref/library/circle.png
new file mode 100644
index 00000000..7244d0dc
--- /dev/null
+++ b/tests/ref/library/circle.png
Binary files differ
diff --git a/tests/ref/library/ellipse.png b/tests/ref/library/ellipse.png
new file mode 100644
index 00000000..de178d60
--- /dev/null
+++ b/tests/ref/library/ellipse.png
Binary files differ
diff --git a/tests/ref/library/pagebreak.png b/tests/ref/library/pagebreak.png
index f0052f9e..c5ac1f54 100644
--- a/tests/ref/library/pagebreak.png
+++ b/tests/ref/library/pagebreak.png
Binary files differ
diff --git a/tests/ref/library/rect.png b/tests/ref/library/rect.png
new file mode 100644
index 00000000..81ee91d7
--- /dev/null
+++ b/tests/ref/library/rect.png
Binary files differ
diff --git a/tests/ref/library/shapes.png b/tests/ref/library/shapes.png
deleted file mode 100644
index a244feb1..00000000
--- a/tests/ref/library/shapes.png
+++ /dev/null
Binary files differ
diff --git a/tests/ref/library/square.png b/tests/ref/library/square.png
new file mode 100644
index 00000000..401b1ab2
--- /dev/null
+++ b/tests/ref/library/square.png
Binary files differ
diff --git a/tests/typ/library/base.typ b/tests/typ/library/base.typ
index 29c976c2..cc9f14a0 100644
--- a/tests/typ/library/base.typ
+++ b/tests/typ/library/base.typ
@@ -3,7 +3,7 @@
---
#test(type("hi"), "string")
-#test(repr([Hi #rect[there]]), "[Hi [<node box>]]")
+#test(repr([Hi #rect[there]]), "[Hi [<node rect>]]")
---
// Check the output.
diff --git a/tests/typ/library/circle.typ b/tests/typ/library/circle.typ
new file mode 100644
index 00000000..b395ee2b
--- /dev/null
+++ b/tests/typ/library/circle.typ
@@ -0,0 +1,41 @@
+// Test the `circle` function.
+
+---
+// Test auto sizing.
+
+Auto-sized circle. \
+#circle(fill: #eb5278, align(center, center, [But, soft!]))
+
+Center-aligned rect in auto-sized circle.
+#circle(fill: #43a127)[
+ #align(center, center)
+ #rect(fill: #9feb52, pad(5pt)[But, soft!])
+]
+
+100%-width rect in auto-sized circle. \
+#circle(fill: #43a127, rect(width: 100%, fill: #9feb52)[
+ But, soft! what light through yonder window breaks?
+])
+
+Expanded by height.
+#circle(fill: #9feb52)[A \ B \ C]
+
+---
+// Test relative sizing.
+#rect(width: 100%, height: 50pt, fill: #aaa)[
+ #align(center, center)
+ #font(color: #fff)
+ #circle(radius: 10pt, fill: #239DAD)[A]
+ #circle(height: 60%, fill: #239DAD)[B]
+ #circle(width: 20% + 20pt, fill: #239DAD)[C]
+]
+
+---
+// Radius wins over width and height.
+// Error: 2:23-2:34 unexpected argument
+// Error: 1:36-1:49 unexpected argument
+#circle(radius: 10pt, width: 50pt, height: 100pt, fill: #239DAD)
+
+// Width wins over height.
+// Error: 22-34 unexpected argument
+#circle(width: 20pt, height: 50pt, fill: #239DAD)
diff --git a/tests/typ/library/ellipse.typ b/tests/typ/library/ellipse.typ
new file mode 100644
index 00000000..06d84a11
--- /dev/null
+++ b/tests/typ/library/ellipse.typ
@@ -0,0 +1,16 @@
+// Test the `ellipse` function.
+
+---
+100% rect in 100% ellipse in fixed rect. \
+#rect(width: 3cm, height: 2cm, fill: #2a631a)[
+ #ellipse(width: 100%, height: 100%, fill: #43a127)[
+ #rect(width: 100%, height: 100%, fill: #9feb52)[
+ #align(center, center)[Stuff inside an ellipse!]
+ ]
+ ]
+]
+
+Auto-sized ellipse. \
+#ellipse(fill: #9feb52)[
+ But, soft! what light through yonder window breaks?
+]
diff --git a/tests/typ/library/pagebreak.typ b/tests/typ/library/pagebreak.typ
index 37a544cf..26629f4b 100644
--- a/tests/typ/library/pagebreak.typ
+++ b/tests/typ/library/pagebreak.typ
@@ -4,3 +4,20 @@
First of two
#pagebreak()
#page(height: 40pt)
+
+---
+// Make sure that you can't do page related stuff in a shape.
+A
+#rect[
+ B
+ // Error: 16 cannot modify page from here
+ #pagebreak()
+
+ // Error: 11-15 cannot modify page from here
+ #page("a4")
+]
+C
+
+// No consequences from the page("A4") call here.
+#pagebreak()
+D
diff --git a/tests/typ/library/rect.typ b/tests/typ/library/rect.typ
new file mode 100644
index 00000000..40713411
--- /dev/null
+++ b/tests/typ/library/rect.typ
@@ -0,0 +1,27 @@
+// Test shapes.
+
+---
+// Test the `rect` function.
+
+#page(width: 150pt)
+
+// Fit to text.
+#rect(fill: #9feb52)[Textbox]
+
+// Empty with fixed width and height.
+#rect(width: 3cm, height: 12pt, fill: #CB4CED)
+
+// Fixed width, text height.
+#rect(width: 2cm, fill: #9650D6, pad(5pt)[Fixed and padded])
+
+// Page width, fixed height.
+#rect(height: 1cm, width: 100%, fill: #734CED)[Topleft]
+
+// Not visible, but creates a gap between the boxes above and below
+// due to line spacing.
+#rect(width: 2in, fill: #ff0000)
+
+// These are in a row!
+#rect(width: 0.5in, height: 10pt, fill: #D6CD67)
+#rect(width: 0.5in, height: 10pt, fill: #EDD466)
+#rect(width: 0.5in, height: 10pt, fill: #E3BE62)
diff --git a/tests/typ/library/shapes.typ b/tests/typ/library/shapes.typ
deleted file mode 100644
index c5a8abf9..00000000
--- a/tests/typ/library/shapes.typ
+++ /dev/null
@@ -1,42 +0,0 @@
-// Test shapes.
-
----
-// Test `rect` function.
-
-#page("a8", flip: true)
-
-// Fixed width, should have text height.
-#rect(width: 2cm, fill: #9650D6)[Legal]
-
-Sometimes there is no box.
-
-// Fixed height, should span line.
-#rect(height: 1cm, width: 100%, fill: #734CED)[B]
-
-// Empty with fixed width and height.
-#rect(width: 6cm, height: 12pt, fill: #CB4CED)
-
-// Not visible, but creates a gap between the boxes above and below.
-#rect(width: 2in, fill: #ff0000)
-
-// These are in a row!
-#rect(width: 0.5in, height: 10pt, fill: #D6CD67)
-#rect(width: 0.5in, height: 10pt, fill: #EDD466)
-#rect(width: 0.5in, height: 10pt, fill: #E3BE62)
-
----
-// Make sure that you can't do page related stuff in a shape.
-A
-#rect[
- B
- // Error: 16 cannot modify page from here
- #pagebreak()
-
- // Error: 11-15 cannot modify page from here
- #page("a4")
-]
-C
-
-// No consequences from the page("A4") call here.
-#pagebreak()
-D
diff --git a/tests/typ/library/square.typ b/tests/typ/library/square.typ
new file mode 100644
index 00000000..5f224b56
--- /dev/null
+++ b/tests/typ/library/square.typ
@@ -0,0 +1,31 @@
+// Test the `square` function.
+
+---
+Auto-sized square. \
+#square(fill: #239DAD)[
+ #align(center)
+ #pad(5pt)[
+ #font(color: #fff, weight: bold)
+ Typst \
+ ]
+]
+
+---
+// Length wins over width and height.
+// Error: 2:9-2:20 unexpected argument
+// Error: 1:22-1:34 unexpected argument
+#square(width: 10cm, height: 20cm, length: 1cm, fill: #eb5278)
+
+---
+// Test height overflow.
+#page(width: 75pt, height: 100pt)
+#square(fill: #9feb52)[
+ But, soft! what light through yonder window breaks?
+]
+
+---
+// Test width overflow.
+#page(width: 100pt, height: 75pt)
+#square(fill: #9feb52)[
+ But, soft! what light through yonder window breaks?
+]
diff --git a/tests/typeset.rs b/tests/typeset.rs
index 65347033..2cf6bfb6 100644
--- a/tests/typeset.rs
+++ b/tests/typeset.rs
@@ -8,18 +8,19 @@ use std::rc::Rc;
use fontdock::fs::FsIndex;
use image::{GenericImageView, Rgba};
use tiny_skia::{
- Canvas, Color, ColorU8, FillRule, FilterQuality, Paint, PathBuilder, Pattern, Pixmap,
- Rect, SpreadMode, Transform,
+ Canvas, Color, ColorU8, FillRule, FilterQuality, Paint, Pattern, Pixmap, Rect,
+ SpreadMode, Transform,
};
use ttf_parser::OutlineBuilder;
use walkdir::WalkDir;
+use typst::color;
use typst::diag::{Diag, DiagSet, Level, Pass};
use typst::env::{Env, FsIndexExt, ImageResource, ResourceLoader};
use typst::eval::{EvalContext, FuncArgs, FuncValue, Scope, Value};
use typst::exec::State;
use typst::export::pdf;
-use typst::geom::{Length, Point, Sides, Size};
+use typst::geom::{self, Length, Point, Sides, Size};
use typst::layout::{Element, Fill, Frame, Geometry, Image, Shape, Shaped};
use typst::library;
use typst::parse::{LineMap, Scanner};
@@ -418,7 +419,7 @@ fn draw_text(env: &Env, canvas: &mut Canvas, pos: Point, shaped: &Shaped) {
let y = pos.y.to_pt() as f32;
let scale = (shaped.font_size / units_per_em as f64).to_pt() as f32;
- let mut builder = WrappedPathBuilder(PathBuilder::new());
+ let mut builder = WrappedPathBuilder::default();
face.outline_glyph(glyph, &mut builder);
if let Some(path) = builder.0.finish() {
@@ -426,7 +427,7 @@ fn draw_text(env: &Env, canvas: &mut Canvas, pos: Point, shaped: &Shaped) {
.transform(&Transform::from_row(scale, 0.0, 0.0, -scale, x, y).unwrap())
.unwrap();
- let mut paint = paint_from_fill(shaped.color);
+ let mut paint = convert_fill(shaped.color);
paint.anti_alias = true;
canvas.fill_path(&placed, &paint, FillRule::default());
@@ -438,28 +439,27 @@ fn draw_geometry(_: &Env, canvas: &mut Canvas, pos: Point, element: &Geometry) {
let x = pos.x.to_pt() as f32;
let y = pos.y.to_pt() as f32;
- let paint = paint_from_fill(element.fill);
+ let paint = convert_fill(element.fill);
+ let rule = FillRule::default();
- match &element.shape {
- Shape::Rect(s) => {
- let (w, h) = (s.width.to_pt() as f32, s.height.to_pt() as f32);
- canvas.fill_rect(Rect::from_xywh(x, y, w, h).unwrap(), &paint);
+ match element.shape {
+ Shape::Rect(Size { width, height }) => {
+ let w = width.to_pt() as f32;
+ let h = height.to_pt() as f32;
+ let rect = Rect::from_xywh(x, y, w, h).unwrap();
+ canvas.fill_rect(rect, &paint);
+ }
+ Shape::Ellipse(size) => {
+ let path = convert_path(x, y, &geom::ellipse_path(size));
+ canvas.fill_path(&path, &paint, rule);
+ }
+ Shape::Path(ref path) => {
+ let path = convert_path(x, y, path);
+ canvas.fill_path(&path, &paint, rule);
}
};
}
-fn paint_from_fill(fill: Fill) -> Paint<'static> {
- let mut paint = Paint::default();
- match fill {
- Fill::Color(c) => match c {
- typst::color::Color::Rgba(c) => paint.set_color_rgba8(c.r, c.g, c.b, c.a),
- },
- Fill::Image(_) => todo!(),
- }
-
- paint
-}
-
fn draw_image(env: &Env, canvas: &mut Canvas, pos: Point, element: &Image) {
let img = &env.resources.loaded::<ImageResource>(element.res);
@@ -492,7 +492,40 @@ fn draw_image(env: &Env, canvas: &mut Canvas, pos: Point, element: &Image) {
);
}
-struct WrappedPathBuilder(PathBuilder);
+fn convert_fill(fill: Fill) -> Paint<'static> {
+ let mut paint = Paint::default();
+ match fill {
+ Fill::Color(c) => match c {
+ color::Color::Rgba(c) => paint.set_color_rgba8(c.r, c.g, c.b, c.a),
+ },
+ Fill::Image(_) => todo!(),
+ }
+ paint
+}
+
+fn convert_path(x: f32, y: f32, path: &geom::Path) -> tiny_skia::Path {
+ let f = |length: Length| length.to_pt() as f32;
+ let mut builder = tiny_skia::PathBuilder::new();
+ for elem in &path.0 {
+ match elem {
+ geom::PathElement::MoveTo(p) => builder.move_to(x + f(p.x), y + f(p.y)),
+ geom::PathElement::LineTo(p) => builder.line_to(x + f(p.x), y + f(p.y)),
+ geom::PathElement::CubicTo(p1, p2, p3) => builder.cubic_to(
+ x + f(p1.x),
+ y + f(p1.y),
+ x + f(p2.x),
+ y + f(p2.y),
+ x + f(p3.x),
+ y + f(p3.y),
+ ),
+ geom::PathElement::ClosePath => builder.close(),
+ };
+ }
+ builder.finish().unwrap()
+}
+
+#[derive(Default)]
+struct WrappedPathBuilder(tiny_skia::PathBuilder);
impl OutlineBuilder for WrappedPathBuilder {
fn move_to(&mut self, x: f32, y: f32) {