diff --git a/src/export/pdf.rs b/src/export/pdf.rs index 03c9ae950..e5124c21f 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(_) => todo!(), + } + + let x = pos.x.to_pt() as f32; + + 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; + if w > 0.0 && h > 0.0 { + content.rect(x, y, w, h, 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/background.rs b/src/layout/background.rs new file mode 100644 index 000000000..07248e02d --- /dev/null +++ b/src/layout/background.rs @@ -0,0 +1,37 @@ +use super::*; + +/// A node that represents a rectangular box. +#[derive(Debug, Clone, PartialEq)] +pub struct NodeBackground { + /// The background fill. + pub fill: Fill, + /// The child node to be filled in. + pub child: Node, +} + +impl Layout for NodeBackground { + fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Layouted { + let mut layouted = self.child.layout(ctx, areas); + + if let Some(first) = layouted.frames_mut().first_mut() { + first.elements.insert( + 0, + ( + Point::ZERO, + Element::Geometry(Geometry { + shape: Shape::Rect(first.size), + fill: self.fill.clone(), + }), + ), + ) + } + + layouted + } +} + +impl From for NodeAny { + fn from(background: NodeBackground) -> Self { + Self::new(background) + } +} diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 44960de7e..302958417 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -1,5 +1,6 @@ //! Layouting. +mod background; mod fixed; mod node; mod pad; @@ -8,10 +9,12 @@ mod spacing; mod stack; mod text; +use crate::color::Color; use crate::env::{Env, ResourceId}; use crate::geom::*; use crate::shaping::Shaped; +pub use background::*; pub use fixed::*; pub use node::*; pub use pad::*; @@ -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,37 @@ 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), +} + +/// 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(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/library/layout.rs b/src/library/layout.rs index 0e04c5071..2812a48f4 100644 --- a/src/library/layout.rs +++ b/src/library/layout.rs @@ -1,9 +1,9 @@ use std::fmt::{self, Display, Formatter}; -use crate::eval::Softness; -use crate::layout::{Expansion, NodeFixed, NodeSpacing, NodeStack}; +use crate::layout::{Expansion, Fill, NodeFixed, NodeSpacing, NodeStack}; use crate::paper::{Paper, PaperClass}; use crate::prelude::*; +use crate::{eval::Softness, layout::NodeBackground}; /// `align`: Align content along the layouting axes. /// @@ -175,6 +175,7 @@ impl Display for Alignment { /// # 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. +/// - Background color of the box: `color`, of type `color`. pub fn box_(ctx: &mut EvalContext, args: &mut Args) -> Value { let snapshot = ctx.state.clone(); @@ -182,6 +183,7 @@ pub fn box_(ctx: &mut EvalContext, args: &mut Args) -> Value { let height = args.get(ctx, "height"); let main = args.get(ctx, "main-dir"); let cross = args.get(ctx, "cross-dir"); + let color = args.get(ctx, "color"); ctx.set_dirs(Gen::new(main, cross)); @@ -199,11 +201,21 @@ pub fn box_(ctx: &mut EvalContext, args: &mut Args) -> Value { 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(NodeFixed { + let fixed_node = NodeFixed { width, height, child: NodeStack { dirs, align, expand, children }.into(), - }); + }; + + if let Some(color) = color { + ctx.push(NodeBackground { + fill: Fill::Color(color), + child: Node::Any(fixed_node.into()), + }) + } else { + ctx.push(fixed_node); + } + ctx.state = snapshot; Value::None diff --git a/tests/library/ref/box.png b/tests/library/ref/box.png new file mode 100644 index 000000000..37fd7d273 Binary files /dev/null and b/tests/library/ref/box.png differ diff --git a/tests/library/typ/box.typ b/tests/library/typ/box.typ new file mode 100644 index 000000000..03e5da54b --- /dev/null +++ b/tests/library/typ/box.typ @@ -0,0 +1,23 @@ +#[page "a5", flip: true] + +// Rectangle with width, should have paragraph height +#[box width: 2cm, color: #9650D6][aa] + +Sometimes there is no box + +// Rectangle with height, should span line +#[box height: 2cm, width: 100%, color: #734CED][bb] + +// Empty rectangle with width and height +#[box width: 6cm, height: 12pt, color: #CB4CED] + +// This empty rectangle should not be displayed +#[box width: 2in, color: #ff0000] + +// This one should be +#[box height: 15mm, width: 100%, color: #494DE3] + +// These are in a row! +#[box width: 2in, height: 10pt, color: #D6CD67] +#[box width: 2in, height: 10pt, color: #EDD466] +#[box width: 2in, height: 10pt, color: #E3BE62] diff --git a/tests/typeset.rs b/tests/typeset.rs index 807d55d9b..d431abfec 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,27 @@ 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 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!(), + }; + + 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); + } + }; +} + fn draw_image(canvas: &mut Canvas, pos: Point, env: &Env, element: &Image) { let img = &env.resources.loaded::(element.res);