From 898728f260923a91444eb23b522d0abf01a4299b Mon Sep 17 00:00:00 2001 From: Laurenz Date: Sat, 20 Mar 2021 20:19:30 +0100 Subject: [PATCH] =?UTF-8?q?Square,=20circle=20and=20ellipse=20=F0=9F=94=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/exec/mod.rs | 1 + src/export/pdf.rs | 210 ++++++++++++++++++-------------- src/geom/length.rs | 4 +- src/geom/mod.rs | 2 + src/geom/path.rs | 60 +++++++++ src/geom/relative.rs | 4 +- src/geom/sides.rs | 2 +- src/geom/size.rs | 8 +- src/layout/background.rs | 29 +++-- src/layout/fixed.rs | 16 ++- src/layout/frame.rs | 8 +- src/layout/mod.rs | 55 +++++++-- src/layout/pad.rs | 35 +++--- src/layout/par.rs | 16 +-- src/layout/stack.rs | 35 ++++-- src/library/font.rs | 2 +- src/library/image.rs | 10 +- src/library/mod.rs | 3 + src/library/shapes.rs | 161 ++++++++++++++++++++---- src/library/spacing.rs | 7 +- tests/ref/library/circle.png | Bin 0 -> 13617 bytes tests/ref/library/ellipse.png | Bin 0 -> 7638 bytes tests/ref/library/pagebreak.png | Bin 803 -> 1357 bytes tests/ref/library/rect.png | Bin 0 -> 2769 bytes tests/ref/library/shapes.png | Bin 3136 -> 0 bytes tests/ref/library/square.png | Bin 0 -> 7166 bytes tests/typ/library/base.typ | 2 +- tests/typ/library/circle.typ | 41 +++++++ tests/typ/library/ellipse.typ | 16 +++ tests/typ/library/pagebreak.typ | 17 +++ tests/typ/library/rect.typ | 27 ++++ tests/typ/library/shapes.typ | 42 ------- tests/typ/library/square.typ | 31 +++++ tests/typeset.rs | 79 ++++++++---- 34 files changed, 671 insertions(+), 252 deletions(-) create mode 100644 src/geom/path.rs create mode 100644 tests/ref/library/circle.png create mode 100644 tests/ref/library/ellipse.png create mode 100644 tests/ref/library/rect.png delete mode 100644 tests/ref/library/shapes.png create mode 100644 tests/ref/library/square.png create mode 100644 tests/typ/library/circle.typ create mode 100644 tests/typ/library/ellipse.typ create mode 100644 tests/typ/library/rect.typ delete mode 100644 tests/typ/library/shapes.typ create mode 100644 tests/typ/library/square.typ diff --git a/src/exec/mod.rs b/src/exec/mod.rs index 90e5a2255..5a2ff6984 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 d8391e2dd..6881188d9 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 = 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, 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, 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, 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, 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 419da5c37..1175876c7 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 5d9068348..5099c6b06 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 000000000..c9fcf1c09 --- /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); + +/// 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 9d7b3d3e1..65312e999 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 292f00c45..deeced452 100644 --- a/src/geom/sides.rs +++ b/src/geom/sides.rs @@ -34,7 +34,7 @@ impl Sides { } impl Sides { - /// 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 { Sides { left: self.left.resolve(size.width), diff --git a/src/geom/size.rs b/src/geom/size.rs index 67e5643d6..2feaa950e 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 for Size { diff --git a/src/layout/background.rs b/src/layout/background.rs index bb155073f..17280a86a 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 layouted.frames_mut() { - let element = Element::Geometry(Geometry { - shape: Shape::Rect(frame.size), - fill: self.fill, - }); - frame.elements.insert(0, (Point::ZERO, element)); + 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)) + } + }; + + 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 e3365668a..22c45ef12 100644 --- a/src/layout/fixed.rs +++ b/src/layout/fixed.rs @@ -7,6 +7,10 @@ pub struct FixedNode { pub width: Option, /// The fixed height, if any. pub height: Option, + /// The fixed aspect ratio between width and height, if any. + /// + /// The resulting frame will satisfy `width = aspect * height`. + pub aspect: Option, /// 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 c85d75392..6e8761514 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 ae4ab89d3..360c9d84b 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 { 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,21 +88,17 @@ pub struct Areas { pub last: Option, /// 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, + /// The aspect ratio the resulting frame should respect. + /// + /// This property is only handled by the stack layouter. + pub aspect: Option, } impl Areas { - /// Create a new length-1 sequence of areas with just one `area`. - pub fn once(size: Size, expand: Spec) -> Self { - Self { - current: size, - full: size, - backlog: vec![], - last: None, - expand, - } - } - /// Create a new sequence of areas that repeats `area` indefinitely. pub fn repeat(size: Size, expand: Spec) -> Self { Self { @@ -112,6 +107,40 @@ impl Areas { backlog: vec![], last: Some(size), expand, + aspect: None, + } + } + + /// Create a new length-1 sequence of areas with just one `area`. + pub fn once(size: Size, full: Size, expand: Spec) -> Self { + Self { + current: size, + full, + backlog: vec![], + last: None, + expand, + aspect: None, + } + } + + /// Builder-style method for setting the aspect ratio. + pub fn with_aspect(mut self, aspect: Option) -> Self { + self.aspect = aspect; + self + } + + /// Map all areas. + pub fn map(&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 33ce217d9..fb0389965 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 for AnyNode { /// Shrink all areas by the padding. fn shrink(areas: &Areas, padding: Sides) -> 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) { - 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, 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 e9fda015a..0364a03a0 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 32eba6765..6a87290ea 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 0993f7f0f..ed2c0ef3c 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 8cb094630..10217e313 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, } -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 for AnyNode { - fn from(image: NodeImage) -> Self { +impl From for AnyNode { + fn from(image: ImageNode) -> Self { Self::new(image) } } diff --git a/src/library/mod.rs b/src/library/mod.rs index d0920cf1c..b09f94a0c 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 211a4f2e9..a5faf73ec 100644 --- a/src/library/shapes.rs +++ b/src/library/shapes.rs @@ -1,49 +1,162 @@ -use super::*; -use crate::layout::{BackgroundNode, Fill, FixedNode}; +use std::f64::consts::SQRT_2; -/// `rect`: Create a rectangular box. +use super::*; +use crate::color::Color; +use crate::layout::{BackgroundNode, BackgroundShape, Fill, FixedNode, PadNode}; + +/// `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::(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::(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::(ctx).unwrap_or_default(); + rect_impl("square", width, height, Some(1.0), fill, body) +} + +fn rect_impl( + name: &str, + width: Option, + height: Option, + aspect: Option, + fill: Option, + body: TemplateValue, +) -> Value { + Value::template(name, move |ctx| { let snapshot = ctx.state.clone(); - - ctx.set_dirs(Gen::new(main, cross)); - let child = ctx.exec(&body).into(); - let fixed = FixedNode { width, height, child }; + let node = FixedNode { width, height, aspect, child }; + if let Some(color) = fill { ctx.push(BackgroundNode { + shape: BackgroundShape::Rect, fill: Fill::Color(color), - child: fixed.into(), + child: node.into(), }); } else { - ctx.push(fixed); + ctx.push(node); + } + + ctx.state = snapshot; + }) +} + +/// `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::(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::(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::(ctx).unwrap_or_default(); + ellipse_impl("circle", width, height, Some(1.0), fill, body) +} + +fn ellipse_impl( + name: &str, + width: Option, + height: Option, + aspect: Option, + fill: Option, + 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 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: node.into(), + }); + } else { + ctx.push(node); } ctx.state = snapshot; diff --git a/src/library/spacing.rs b/src/library/spacing.rs index fee802fac..c96b8be48 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 = 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 0000000000000000000000000000000000000000..7244d0dce08f5d33b61697041bc910b9e3a2d867 GIT binary patch literal 13617 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 B4$%Mr delta 655 zcmV;A0&xA!3Zn*)7GD6Q2mk;808|JMi~s-v>PbXFRCwC$*xilWI24BA(}C21rUR)1 zx&yfbO$SN`WCuzIN(W2_XF6aBDuE@udm$6pS$H>-V1bOC@4YenCqR!$in5tfN-3q3 zQXh;zER*p97k_+Kgz{FO451u9OAtb5`6&?U(wT)2f~|0T6apwS)DWL!BxIWbJV4)n z^!V^no@q^RJAcj{#_-YOQ$G7L`)dgQ*8RmV+K)c-s0bYYJSEuIA0fc&_8bT$Lrp#4 z7tfb3)U?hCl3h~T+h^<7aCtp|GFeLLX7*~=lzdAm)qk8)%H)(bAYU(6e-wl%rPODb zEkN#U3d78@QisZFV~h=0fxT5pAK$*KJ0)kz>*=3w4*)BbT~2)0+7#B8vAo2)DVE(3czOD zT?B>+vVU2nj!^puP;Gh)gtqjZ2)9!zon7mDgxT5`?jvkgnB6w^t=Yn0kAV>GAy8+V z>N|wchJCz`P;EVC!o*8$2oSnQMK~{$tL1kH+d048@_huBe$DI_7vQ+fgMN8b1n?aW zbFfQ$m!o^Bz#4$H9StaLLb(kmI$P)aGKlv3(We*uT;2D2C|BH{o5002ovPDHLkV1gUgFyH_H diff --git a/tests/ref/library/rect.png b/tests/ref/library/rect.png new file mode 100644 index 0000000000000000000000000000000000000000..81ee91d7043b2d58e60990d149228e831e4789d9 GIT binary patch literal 2769 zcmai$do+~$8pq!;NMtCXFsM+B`)%BkOSxZ$jS#~|?q){Q47p@d*kwnVB!nrq>=>7t zE%!-eE1G>=Gs(4FVrFC-mmJL*?X}Nw*4b;HKi+5k*0bLC`K|Z&e4g+3cOBtkD=I7_ z3;=+rz1?Yd003F=o?sy`FO!Hr%8MW^gp5zk> zqN2({L*;`8Pzf`rgy}(fk%LfSsFMY8CMXOM z5Eg*!6%Z8?5ZfaF;T;-SO3M)d_WIkOw)BXnGw9s2bO)K9&qK^N=c`%}=%r;Ok#Mu~ zSI2FeVYeb%R({ZNN!19~G8pp19y!4FBBdRx-cM37GYyP6{tn zC67%q=ZWf7ZuwG&aleK5fli`Vt-Vw!N`vV?2ICLjayJbFki$BsBEE5bdJ7x8n&TkXehS1P6xY&?fqDycMVD^xC|&yo!Vwg^v5nSPIk>O(R>F|_-AYtn`hS2T658(9oulGGBD z+Am5@g&^_cuMnyR*yt?qLOLeFKB(bjE)y!#+2c@@;s82zk2n*%7N0T>>i5td)@s4( zWrl>^DHub1+8_$tcL8DXIr%zmVNQe8&~>(&)~vaRs}$|Sazt}yp-J90gYCC=T=kdn zc6Aq-!ES~sCAj&+8e0$s>wB_`-=^EY#ILqGsjycpRfzSnv%+?}fB^B(04eRD@h1-P;Hhk` z*5-a*eutaC3)FYOf$uNy?R>qv?4QLpqQIXfO?HWcb>$+3dM)R8OogaoV@Fb)C|GAwltH+|vi@4^C9(K${dPcS7-uWd4{yee_D{%BpZdJ0n+3mQ^h9G*?6 zeMp&%D)3!aZVtL}q3y?)AJRrRT}-@T^U=`en@`uu8BtIN{}z|`_5AwWl-^qgg^NMt z$VpS^6SL({zk4UQSyi3w>m`>_TwF`$XjUyJM_wP=6Q161xY5la$RcBYPm3f3K#oQD z0fp9tdO7-b`IUT5R3T=m6J>0H9okbTc8CJ2;7>MvN*VF2+DE^Z(XTDfC&$z$6GD$J zoNm4}U3fd%L3?$3VtQ&(qY2{<`Mpmkbt}R=NO^rR;z`v<6nqISr3&^`cJcRP;f(> zr+u@1)!3siXZzfqK({z}1E(3If6Qqe2v~9-a8?r?y}os268>Lp*5q|fja*(jEuvq; z$vi+9RbGB3n^rLY*x~JKVAvVvKLd z7jlBXf3-6mY97*yW4m^KSx0iqB3a|Iloz-yA*<*$o6xCrI=ln45d(A}tGnEXZ-#RF zq#6e)j@39y6l_3$RBOg)A}0s+*1%bSS_^XAR?yK1F~QH5q4<>tc5sTvDFRb{Ua?rme%O%=R>vmN-c1E zTrT;{l$d6{T8k&qkXa^{=qiRSjYK#>1Nvv z&0hPm7ZqLm34(R+-_ad@J~Kqbmuol&*!tf_dD81^ODpUel^6*j>vUWo<@i9g-2pJK z`HsvZ{#$;vQD0HDO&*Ih1=Ntu1Jiw^q$ucnL-w;|nk993eZ8AW zCKNX!>|oK)Tl;fulY0~c9}AK)Wgm)+rt~P1-W7mX5~!UV0?P{|C%7-Qa}Ko>s4SG4 zR-*N~7NVK$CFdg^&Aglhmp*9tF&vAfV3hV07K#86J}H;imUU{Z)WGS?Wimgk_JpF0{KTZ@<(9MKfTXc2|$W$;1AWQV3Vo-0GiLKhupLp zABCsmI6Yn^)i@sj!ym|X*Az;ndBt$@QbA_gWdW~}8zha3L{i}*%uce|r-4i3z|~dS z(Y(UN^Jk{nA0Ow{`eJ`o?~rRv;xx(eV_6?AK6g5PAz^a@)u!sbTw1zScefoDSQj`; zzA7C_xsc)~?G+^z{zfh?V?d*&^U}E%X=!YnfL!i3r3;hb9es}XoP&ID22tPf@s5Du zz$hMH*(yq3dG@{A|3~Tn*9Tg)2q7NdjpVN!;*ZV23)rW z<8Ex%btF}Ohj5oW{l}+xt69iy&)K!xcfi+0Z;k8zp-DRwW6ap6R!J7|UGF0qy!#el Mf5zoB*(xygUq{-k4FCWD literal 0 HcmV?d00001 diff --git a/tests/ref/library/shapes.png b/tests/ref/library/shapes.png deleted file mode 100644 index a244feb1a1ac1206efde067257d81e9a53e032ba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3136 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 diff --git a/tests/ref/library/square.png b/tests/ref/library/square.png new file mode 100644 index 0000000000000000000000000000000000000000..401b1ab2a72662182f07b8aa55eb718171d28fd8 GIT binary patch literal 7166 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 literal 0 HcmV?d00001 diff --git a/tests/typ/library/base.typ b/tests/typ/library/base.typ index 29c976c2a..cc9f14a03 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 []]") +#test(repr([Hi #rect[there]]), "[Hi []]") --- // Check the output. diff --git a/tests/typ/library/circle.typ b/tests/typ/library/circle.typ new file mode 100644 index 000000000..b395ee2be --- /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 000000000..06d84a114 --- /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 37a544cfa..26629f4bd 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 000000000..407134114 --- /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 c5a8abf95..000000000 --- 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 000000000..5f224b569 --- /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 653470339..2cf6bfb6e 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::(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) {