diff --git a/src/export/pdf.rs b/src/export/pdf.rs
index 03c9ae950..7556e3704 100644
--- a/src/export/pdf.rs
+++ b/src/export/pdf.rs
@@ -13,9 +13,10 @@ use pdf_writer::{
};
use ttf_parser::{name_id, GlyphId};
+use crate::color::Color;
use crate::env::{Env, ImageResource, ResourceId};
use crate::geom::Length;
-use crate::layout::{Element, Frame};
+use crate::layout::{Element, Fill, Frame, Shape};
/// Export a collection of frames into a _PDF_ document.
///
@@ -57,6 +58,7 @@ impl<'a> PdfExporter<'a> {
}
images.insert(image.res);
}
+ Element::Geometry(_) => {}
}
}
}
@@ -127,6 +129,56 @@ impl<'a> PdfExporter<'a> {
fn write_page(&mut self, id: Ref, page: &'a Frame) {
let mut content = Content::new();
+ for (pos, element) in &page.elements {
+ match element {
+ Element::Image(image) => {
+ let name = format!("Im{}", self.images.map(image.res));
+ let size = image.size;
+ let x = pos.x.to_pt() as f32;
+ 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;
+
+ content.save_state();
+ content.matrix(w, 0.0, 0.0, h, x, y);
+ content.x_object(Name(name.as_bytes()));
+ content.restore_state();
+ }
+
+ Element::Geometry(geometry) => {
+ content.save_state();
+
+ match geometry.fill {
+ Fill::Color(Color::Rgba(c)) => {
+ content.fill_rgb(
+ c.r as f32 / 255.,
+ c.g as f32 / 255.,
+ c.b as f32 / 255.,
+ );
+ }
+ Fill::Image(_) => unimplemented!(),
+ }
+
+ match &geometry.shape {
+ Shape::Rect(r) => {
+ content.rect(
+ pos.x.to_pt() as f32,
+ (page.size.height - pos.y - r.size.height).to_pt() as f32,
+ r.size.width.to_pt() as f32,
+ r.size.height.to_pt() as f32,
+ false,
+ true,
+ );
+ }
+ }
+
+ content.restore_state();
+ }
+
+ _ => {}
+ }
+ }
+
// We only write font switching actions when the used face changes. To
// do that, we need to remember the active face.
let mut face = FaceId::MAX;
@@ -153,22 +205,6 @@ impl<'a> PdfExporter<'a> {
drop(text);
- for (pos, element) in &page.elements {
- if let Element::Image(image) = element {
- let name = format!("Im{}", self.images.map(image.res));
- let size = image.size;
- let x = pos.x.to_pt() as f32;
- 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;
-
- content.save_state();
- content.matrix(w, 0.0, 0.0, h, x, y);
- content.x_object(Name(name.as_bytes()));
- content.restore_state();
- }
- }
-
self.writer.stream(id, &content.finish());
}
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index 44960de7e..1bb4419da 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -4,10 +4,12 @@ mod fixed;
mod node;
mod pad;
mod par;
+mod rect;
mod spacing;
mod stack;
mod text;
+use crate::color::Color;
use crate::env::{Env, ResourceId};
use crate::geom::*;
use crate::shaping::Shaped;
@@ -16,6 +18,7 @@ pub use fixed::*;
pub use node::*;
pub use pad::*;
pub use par::*;
+pub use rect::*;
pub use spacing::*;
pub use stack::*;
pub use text::*;
@@ -54,7 +57,7 @@ impl NodePages {
pub fn layout(&self, ctx: &mut LayoutContext) -> Vec {
let areas = Areas::repeat(self.size);
let layouted = self.child.layout(ctx, &areas);
- layouted.frames()
+ layouted.into_frames()
}
}
@@ -157,9 +160,27 @@ pub enum Layouted {
}
impl Layouted {
- /// Return all frames contained in this variant (zero, one or arbitrarily
- /// many).
- pub fn frames(self) -> Vec {
+ /// Return a reference to all frames contained in this variant (zero, one or
+ /// arbitrarily many).
+ pub fn frames(&self) -> &[Frame] {
+ match self {
+ Self::Spacing(_) => &[],
+ Self::Frame(frame, _) => std::slice::from_ref(frame),
+ Self::Frames(frames, _) => frames,
+ }
+ }
+
+ /// Return a mutable reference to all frames contained in this variant.
+ pub fn frames_mut(&mut self) -> &mut [Frame] {
+ match self {
+ Self::Spacing(_) => &mut [],
+ Self::Frame(frame, _) => std::slice::from_mut(frame),
+ Self::Frames(frames, _) => frames,
+ }
+ }
+
+ /// Return all frames contained in this varian.
+ pub fn into_frames(self) -> Vec {
match self {
Self::Spacing(_) => vec![],
Self::Frame(frame, _) => vec![frame],
@@ -204,6 +225,46 @@ pub enum Element {
Text(Shaped),
/// An image.
Image(Image),
+ /// Some shape that could hold another frame.
+ Geometry(Geometry),
+}
+
+/// The kind of graphic fill to be applied to a [`Shape`].
+#[derive(Debug, Clone, PartialEq)]
+pub enum Fill {
+ /// The fill is a color.
+ Color(Color),
+ /// The fill is an image.
+ Image(Image),
+ // Gradient(Gradient),
+}
+
+/// A shape with some kind of fill.
+#[derive(Debug, Clone, PartialEq)]
+pub struct Geometry {
+ /// The shape to draw.
+ pub shape: Shape,
+ /// How the shape looks on the inside.
+ ///
+ /// **TODO:** This could be made into a Vec or something such that
+ /// the user can compose multiple fills with alpha values less
+ /// than one to achieve cool effects.
+ pub fill: Fill,
+}
+
+/// Some shape.
+#[derive(Debug, Clone, PartialEq)]
+pub enum Shape {
+ /// A rectangle.
+ Rect(Rect),
+ // Ellipse(Ellipse),
+}
+
+/// An rectangle.
+#[derive(Debug, Clone, PartialEq)]
+pub struct Rect {
+ /// The dimensions of the rectangle.
+ pub size: Size,
}
/// An image element.
diff --git a/src/layout/pad.rs b/src/layout/pad.rs
index f8a623e39..425fa41b5 100644
--- a/src/layout/pad.rs
+++ b/src/layout/pad.rs
@@ -15,14 +15,8 @@ impl Layout for NodePad {
let areas = shrink(areas, self.padding);
let mut layouted = self.child.layout(ctx, &areas);
- match &mut layouted {
- Layouted::Spacing(_) => {}
- Layouted::Frame(frame, _) => pad(frame, self.padding),
- Layouted::Frames(frames, _) => {
- for frame in frames {
- pad(frame, self.padding);
- }
- }
+ for frame in layouted.frames_mut() {
+ pad(frame, self.padding);
}
layouted
diff --git a/src/layout/rect.rs b/src/layout/rect.rs
new file mode 100644
index 000000000..ab8e3b075
--- /dev/null
+++ b/src/layout/rect.rs
@@ -0,0 +1,71 @@
+use std::cmp::Ordering;
+
+use super::*;
+use crate::{color::RgbaColor, geom::Linear};
+
+/// A node that represents a rectangular box.
+#[derive(Debug, Clone, PartialEq)]
+pub struct NodeRect {
+ /// The fixed width, if any.
+ pub width: Option,
+ /// The fixed height, if any.
+ pub height: Option,
+ /// The background color.
+ pub color: Option,
+ /// The child node whose sides to pad.
+ pub child: Node,
+}
+
+impl Layout for NodeRect {
+ fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Layouted {
+ let Areas { current, full, .. } = areas;
+
+ let height_opt = self.height.map(|h| h.resolve(full.height));
+ let mut size = Size::new(
+ self.width.map(|w| w.resolve(full.width)).unwrap_or(current.width),
+ height_opt.unwrap_or(current.height),
+ );
+
+ let areas = Areas::once(size);
+ let mut layouted = self.child.layout(ctx, &areas);
+
+ // If the children have some height, apply that,
+ // otherwise fall back to zero or the height property.
+ if let Some(max) = layouted
+ .frames()
+ .iter()
+ .map(|f| f.size.height)
+ .max_by(|x, y| x.partial_cmp(y).unwrap_or(Ordering::Equal))
+ {
+ size.height = max;
+ } else {
+ size.height = height_opt.unwrap_or(Length::ZERO)
+ }
+
+ if let Some(first) = layouted.frames_mut().first_mut() {
+ first.elements.insert(
+ 0,
+ (
+ Point::ZERO,
+ Element::Geometry(Geometry {
+ shape: Shape::Rect(Rect { size }),
+ fill: Fill::Color(self.color.unwrap_or(Color::Rgba(RgbaColor {
+ r: 255,
+ g: 255,
+ b: 255,
+ a: 0,
+ }))),
+ }),
+ ),
+ )
+ }
+
+ layouted
+ }
+}
+
+impl From for NodeAny {
+ fn from(pad: NodeRect) -> Self {
+ Self::new(pad)
+ }
+}
diff --git a/src/library/insert.rs b/src/library/insert.rs
index 58e8a11c2..169fad970 100644
--- a/src/library/insert.rs
+++ b/src/library/insert.rs
@@ -4,6 +4,43 @@ use crate::env::{ImageResource, ResourceId};
use crate::layout::*;
use crate::prelude::*;
+/// `rect`: Layout content into a rectangle that also might have a fill.
+///
+/// # Named arguments
+/// - Width of the box: `width`, of type `linear` relative to parent width.
+/// - Height of the box: `height`, of type `linear` relative to parent height.
+pub fn rect(ctx: &mut EvalContext, args: &mut Args) -> Value {
+ let snapshot = ctx.state.clone();
+
+ let width = args.get(ctx, "width");
+ let height = args.get(ctx, "height");
+ let color = args.get(ctx, "color");
+
+ let dirs = ctx.state.dirs;
+ let align = ctx.state.align;
+
+ ctx.start_content_group();
+
+ if let Some(body) = args.find::(ctx) {
+ body.eval(ctx);
+ }
+
+ let children = ctx.end_content_group();
+
+ let fill_if = |c| if c { Expansion::Fill } else { Expansion::Fit };
+ let expand = Spec::new(fill_if(width.is_some()), fill_if(height.is_some()));
+
+ ctx.push(NodeRect {
+ width,
+ height,
+ color,
+ child: NodeStack { dirs, align, expand, children }.into(),
+ });
+
+ ctx.state = snapshot;
+ Value::None
+}
+
/// `image`: Insert an image.
///
/// Supports PNG and JPEG files.
diff --git a/src/library/mod.rs b/src/library/mod.rs
index 48da093b2..7e20f5fbd 100644
--- a/src/library/mod.rs
+++ b/src/library/mod.rs
@@ -38,6 +38,7 @@ pub fn new() -> Scope {
set!(func: "image", image);
set!(func: "page", page);
set!(func: "pagebreak", pagebreak);
+ set!(func: "rect", rect);
set!(func: "rgb", rgb);
set!(func: "type", type_);
set!(func: "v", v);
diff --git a/tests/library/ref/geom.png b/tests/library/ref/geom.png
new file mode 100644
index 000000000..37fd7d273
Binary files /dev/null and b/tests/library/ref/geom.png differ
diff --git a/tests/library/typ/geom.typ b/tests/library/typ/geom.typ
new file mode 100644
index 000000000..26ba7ca32
--- /dev/null
+++ b/tests/library/typ/geom.typ
@@ -0,0 +1,23 @@
+#[page "a5", flip: true]
+
+// Rectangle with width, should have paragraph height
+#[rect width: 2cm, color: #9650D6][aa]
+
+Sometimes there is no box
+
+// Rectangle with height, should span line
+#[rect height: 2cm, color: #734CED][bb]
+
+// Empty rectangle with width and height
+#[rect width: 6cm, height: 12pt, color: #CB4CED]
+
+// This empty rectangle should not be displayed
+#[rect width: 2in, color: #ff0000]
+
+// This one should be
+#[rect height: 15mm, color: #494DE3]
+
+// These are in a row!
+#[rect width: 2in, height: 10pt, color: #D6CD67]
+#[rect width: 2in, height: 10pt, color: #EDD466]
+#[rect width: 2in, height: 10pt, color: #E3BE62]
diff --git a/tests/typeset.rs b/tests/typeset.rs
index 807d55d9b..432941ce2 100644
--- a/tests/typeset.rs
+++ b/tests/typeset.rs
@@ -20,7 +20,7 @@ use typst::eval::{Args, EvalContext, Scope, State, Value, ValueFunc};
use typst::export::pdf;
use typst::font::FsIndexExt;
use typst::geom::{Length, Point, Sides, Size, Spec};
-use typst::layout::{Element, Expansion, Frame, Image};
+use typst::layout::{Element, Expansion, Fill, Frame, Geometry, Image, Shape};
use typst::library;
use typst::parse::{LineMap, Scanner};
use typst::pretty::{Pretty, Printer};
@@ -409,6 +409,9 @@ fn draw(frames: &[Frame], env: &Env, pixel_per_pt: f32) -> Canvas {
Element::Image(image) => {
draw_image(&mut canvas, pos, env, image);
}
+ Element::Geometry(geom) => {
+ draw_geometry(&mut canvas, pos, env, geom);
+ }
}
}
@@ -444,6 +447,28 @@ fn draw_text(canvas: &mut Canvas, pos: Point, env: &Env, shaped: &Shaped) {
}
}
+fn draw_geometry(canvas: &mut Canvas, pos: Point, _: &Env, element: &Geometry) {
+ let x = pos.x.to_pt() as f32;
+ let y = pos.y.to_pt() as f32;
+
+ let (w, h) = match &element.shape {
+ Shape::Rect(s) => (s.size.width.to_pt() as f32, s.size.height.to_pt() as f32),
+ };
+
+ let mut paint = Paint::default();
+
+ match &element.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!(),
+ };
+
+ if let Some(rect) = Rect::from_xywh(x, y, w, h) {
+ canvas.fill_rect(rect, &paint);
+ }
+}
+
fn draw_image(canvas: &mut Canvas, pos: Point, env: &Env, element: &Image) {
let img = &env.resources.loaded::(element.res);