diff --git a/docs/src/html.rs b/docs/src/html.rs index 550e6ff80..9fbb4fd8d 100644 --- a/docs/src/html.rs +++ b/docs/src/html.rs @@ -287,17 +287,17 @@ fn code_block(resolver: &dyn Resolver, lang: &str, text: &str) -> Html { let source = Source::new(SourceId::from_u16(0), Path::new("main.typ"), compile); let world = DocWorld(source); - let mut frame = match typst::compile(&world, &world.0) { - Ok(doc) => doc.pages.into_iter().next().unwrap(), + let mut frames = match typst::compile(&world, &world.0) { + Ok(doc) => doc.pages, Err(err) => panic!("failed to compile {text}: {err:?}"), }; if let Some([x, y, w, h]) = zoom { - frame.translate(Point::new(-x, -y)); - *frame.size_mut() = Size::new(w, h); + frames[0].translate(Point::new(-x, -y)); + *frames[0].size_mut() = Size::new(w, h); } - resolver.example(highlighted, frame) + resolver.example(highlighted, &frames) } /// World for example compilations. diff --git a/docs/src/lib.rs b/docs/src/lib.rs index 620aaaaaf..c9e7af25a 100644 --- a/docs/src/lib.rs +++ b/docs/src/lib.rs @@ -67,7 +67,7 @@ pub trait Resolver { fn image(&self, filename: &str, data: &[u8]) -> String; /// Produce HTML for an example. - fn example(&self, source: Html, frame: Frame) -> Html; + fn example(&self, source: Html, frames: &[Frame]) -> Html; } /// Details about a documentation page and its children. diff --git a/library/src/layout/container.rs b/library/src/layout/container.rs index 438233396..8b10f7a69 100644 --- a/library/src/layout/container.rs +++ b/library/src/layout/container.rs @@ -41,20 +41,13 @@ use crate::prelude::*; /// - height: `Rel` (named) /// The height of the box. /// -/// - baseline: `Rel` (named) -/// An amount to shift the box's baseline by. -/// -/// ```example -/// Image: #box(baseline: 40%, image("tiger.jpg", width: 2cm)). -/// ``` -/// /// ## Category /// layout #[func] #[capable(Layout)] #[derive(Debug, Hash)] pub struct BoxNode { - /// The content to be sized. + /// The box's content. pub body: Content, /// The box's width. pub width: Sizing, @@ -64,7 +57,11 @@ pub struct BoxNode { #[node] impl BoxNode { - /// The box's baseline shift. + /// An amount to shift the box's baseline by. + /// + /// ```example + /// Image: #box(baseline: 40%, image("tiger.jpg", width: 2cm)). + /// ``` #[property(resolve)] pub const BASELINE: Rel = Rel::zero(); @@ -127,11 +124,12 @@ impl Layout for BoxNode { // Resolve the sizing to a concrete size. let sizing = Axes::new(width, self.height); + let expand = sizing.as_ref().map(Smart::is_custom); let size = sizing .resolve(styles) .zip(regions.base()) .map(|(s, b)| s.map(|v| v.relative_to(b))) - .unwrap_or(regions.size); + .unwrap_or(regions.base()); // Apply inset. let mut child = self.body.clone(); @@ -142,8 +140,6 @@ impl Layout for BoxNode { // Select the appropriate base and expansion for the child depending // on whether it is automatically or relatively sized. - let is_auto = sizing.as_ref().map(Smart::is_auto); - let expand = regions.expand | !is_auto; let pod = Regions::one(size, expand); let mut frame = child.layout(vt, styles, pod)?.into_frame(); @@ -181,9 +177,9 @@ impl Layout for BoxNode { /// /// ## Examples /// With a block, you can give a background to content while still allowing it -/// to break across multiple pages. The documentation examples can only have a -/// single page, but the example below demonstrates how this would work. +/// to break across multiple pages. /// ```example +/// #set page(height: 100pt) /// #block( /// fill: luma(230), /// inset: 8pt, @@ -204,27 +200,55 @@ impl Layout for BoxNode { /// More text. /// ``` /// -/// Last but not least, set rules for the block function can be used to -/// configure the spacing around arbitrary block-level elements. -/// ```example -/// #set align(center) -/// #show math.formula: set block(above: 8pt, below: 16pt) -/// -/// This sum of $x$ and $y$: -/// $ x + y = z $ -/// A second paragraph. -/// ``` -/// /// ## Parameters /// - body: `Content` (positional) /// The contents of the block. /// +/// - width: `Smart>` (named) +/// The block's width. +/// +/// ```example +/// #set align(center) +/// #block( +/// width: 60%, +/// inset: 8pt, +/// fill: silver, +/// lorem(10), +/// ) +/// ``` +/// +/// - height: `Smart>` (named) +/// The block's height. When the height is larger than the remaining space on +/// a page and [`breakable`]($func/block.breakable) is `{true}`, the block +/// will continue on the next page with the remaining height. +/// +/// ```example +/// #set page(height: 80pt) +/// #set align(center) +/// #block( +/// width: 80%, +/// height: 150%, +/// fill: aqua, +/// ) +/// ``` +/// /// - spacing: `Spacing` (named, settable) -/// The spacing around this block. +/// The spacing around this block. This is shorthand to set `above` and +/// `below` to the same value. +/// +/// ```example +/// #set align(center) +/// #show math.formula: set block(above: 8pt, below: 16pt) +/// +/// This sum of $x$ and $y$: +/// $ x + y = z $ +/// A second paragraph. +/// ``` /// /// - above: `Spacing` (named, settable) /// The spacing between this block and its predecessor. Takes precedence over -/// `spacing`. +/// `spacing`. Can be used in combination with a show rule to adjust the +/// spacing around arbitrary block-level elements. /// /// The default value is `{1.2em}`. /// @@ -240,11 +264,30 @@ impl Layout for BoxNode { #[capable(Layout)] #[derive(Debug, Hash)] pub struct BlockNode { + /// The block's content. pub body: Content, + /// The box's width. + pub width: Smart>, + /// The box's height. + pub height: Smart>, } #[node] impl BlockNode { + /// Whether the block can be broken and continue on the next page. + /// + /// Defaults to `{true}`. + /// ```example + /// #set page(height: 80pt) + /// The following block will + /// jump to its own page. + /// #block( + /// breakable: false, + /// lorem(15), + /// ) + /// ``` + pub const BREAKABLE: bool = true; + /// The block's background color. See the /// [rectangle's documentation]($func/rect.fill) for more details. pub const FILL: Option = None; @@ -285,7 +328,9 @@ impl BlockNode { fn construct(_: &Vm, args: &mut Args) -> SourceResult { let body = args.eat()?.unwrap_or_default(); - Ok(Self { body }.pack()) + let width = args.named("width")?.unwrap_or_default(); + let height = args.named("height")?.unwrap_or_default(); + Ok(Self { body, width, height }.pack()) } fn set(...) { @@ -315,8 +360,52 @@ impl Layout for BlockNode { child = child.clone().padded(inset.map(|side| side.map(Length::from))); } + // Resolve the sizing to a concrete size. + let sizing = Axes::new(self.width, self.height); + let mut expand = sizing.as_ref().map(Smart::is_custom); + let mut size = sizing + .resolve(styles) + .zip(regions.base()) + .map(|(s, b)| s.map(|v| v.relative_to(b))) + .unwrap_or(regions.base()); + // Layout the child. - let mut frames = child.layout(vt, styles, regions)?.into_frames(); + let mut frames = if styles.get(Self::BREAKABLE) { + // Measure to ensure frames for all regions have the same width. + if self.width == Smart::Auto { + let pod = Regions::one(size, Axes::splat(false)); + let frame = child.layout(vt, styles, pod)?.into_frame(); + size.x = frame.width(); + expand.x = true; + } + + let mut pod = regions; + pod.size.x = size.x; + pod.expand = expand; + + // Generate backlog for fixed height. + let mut heights = vec![]; + if self.height.is_custom() { + let mut remaining = size.y; + for region in regions.iter() { + let limited = region.y.min(remaining); + heights.push(limited); + remaining -= limited; + if Abs::zero().fits(remaining) { + break; + } + } + + pod.size.y = heights[0]; + pod.backlog = &heights[1..]; + pod.last = None; + } + + child.layout(vt, styles, pod)?.into_frames() + } else { + let pod = Regions::one(size, expand); + child.layout(vt, styles, pod)?.into_frames() + }; // Prepare fill and stroke. let fill = styles.get(Self::FILL); @@ -326,9 +415,14 @@ impl Layout for BlockNode { // Add fill and/or stroke. if fill.is_some() || stroke.iter().any(Option::is_some) { + let mut skip = false; + if let [first, rest @ ..] = frames.as_slice() { + skip = first.is_empty() && rest.iter().any(|frame| !frame.is_empty()); + } + let outset = styles.get(Self::OUTSET); let radius = styles.get(Self::RADIUS); - for frame in &mut frames { + for frame in frames.iter_mut().skip(skip as usize) { frame.fill_and_stroke(fill, stroke, outset, radius); } } diff --git a/library/src/layout/enum.rs b/library/src/layout/enum.rs index 9a83420c6..585b20e5d 100644 --- a/library/src/layout/enum.rs +++ b/library/src/layout/enum.rs @@ -25,12 +25,21 @@ use crate::prelude::*; /// #enum[First][Second] /// ``` /// +/// You can easily switch all your enumerations to a different numbering style +/// with a set rule. +/// ```example +/// #set enum(numbering: "a)") +/// +/// + Starting off ... +/// + Don't forget step two +/// ``` +/// /// ## Syntax /// This functions also has dedicated syntax: /// /// - Starting a line with a plus sign creates an automatically numbered /// enumeration item. -/// - Start a line with a number followed by a dot creates an explicitly +/// - Starting a line with a number followed by a dot creates an explicitly /// numbered enumeration item. /// /// Enumeration items can contain multiple paragraphs and other block-level @@ -98,10 +107,13 @@ impl EnumNode { /// /// ```example /// #set enum(numbering: "(a)") - /// /// + Different /// + Numbering /// + Style + /// + /// #set enum(numbering: n => super[#n]) + /// + Superscript + /// + Numbering! /// ``` #[property(referenced)] pub const NUMBERING: Numbering = diff --git a/library/src/layout/flow.rs b/library/src/layout/flow.rs index db9eed8d4..00eeb5374 100644 --- a/library/src/layout/flow.rs +++ b/library/src/layout/flow.rs @@ -44,7 +44,10 @@ impl Layout for FlowNode { } else if child.has::() { layouter.layout_multiple(vt, child, styles)?; } else if child.is::() { - layouter.finish_region(); + if !layouter.regions.backlog.is_empty() || layouter.regions.last.is_some() + { + layouter.finish_region(); + } } else { panic!("unexpected flow child: {child:?}"); } @@ -207,7 +210,10 @@ impl<'a> FlowLayouter<'a> { // Layout the block itself. let sticky = styles.get(BlockNode::STICKY); let fragment = block.layout(vt, styles, self.regions)?; - for frame in fragment { + for (i, frame) in fragment.into_iter().enumerate() { + if i > 0 { + self.finish_region(); + } self.layout_item(FlowItem::Frame(frame, aligns, sticky)); } @@ -264,8 +270,7 @@ impl<'a> FlowLayouter<'a> { // Determine the size of the flow in this region depending on whether // the region expands. - let mut size = self.expand.select(self.full, used); - size.y.set_min(self.full.y); + let mut size = self.expand.select(self.full, used).min(self.full); // Account for fractional spacing in the size calculation. let remaining = self.full.y - used.y; diff --git a/library/src/layout/grid.rs b/library/src/layout/grid.rs index 5e8b50d11..92bf19d1a 100644 --- a/library/src/layout/grid.rs +++ b/library/src/layout/grid.rs @@ -37,7 +37,7 @@ use super::Sizing; /// /// ## Example /// ```example -/// #set text(10pt, weight: "bold") +/// #set text(10pt, style: "italic") /// #let cell = rect.with( /// inset: 8pt, /// fill: rgb("e4e5ea"), diff --git a/library/src/layout/pad.rs b/library/src/layout/pad.rs index e43763e5f..4fc2ff298 100644 --- a/library/src/layout/pad.rs +++ b/library/src/layout/pad.rs @@ -13,7 +13,7 @@ use crate::prelude::*; /// /// #pad(x: 16pt, image("typing.jpg")) /// _Typing speeds can be -/// measured in words per minute._ +/// measured in words per minute._ /// ``` /// /// ## Parameters diff --git a/library/src/layout/page.rs b/library/src/layout/page.rs index e38e6d874..ac0e27cb0 100644 --- a/library/src/layout/page.rs +++ b/library/src/layout/page.rs @@ -351,14 +351,14 @@ impl Debug for PageNode { /// more details on compound theory. /// #pagebreak() /// -/// // Examples only render the first -/// // page, so this is not visible. /// == Compound Theory +/// In 1984, the first ... /// ``` /// /// ## Parameters /// - weak: `bool` (named) -/// If `{true}`, the page break is skipped if the current page is already empty. +/// If `{true}`, the page break is skipped if the current page is already +/// empty. /// /// ## Category /// layout diff --git a/library/src/layout/place.rs b/library/src/layout/place.rs index c64766ccc..05de369b7 100644 --- a/library/src/layout/place.rs +++ b/library/src/layout/place.rs @@ -16,8 +16,8 @@ use crate::prelude::*; /// #place( /// top + right, /// square( -/// width: 10pt, -/// stroke: 1pt + blue +/// width: 20pt, +/// stroke: 2pt + blue /// ), /// ) /// ``` diff --git a/library/src/meta/heading.rs b/library/src/meta/heading.rs index bc7882431..b5b0fd9e8 100644 --- a/library/src/meta/heading.rs +++ b/library/src/meta/heading.rs @@ -147,7 +147,12 @@ impl Show for HeadingNode { if numbers != Value::None { realized = numbers.display() + SpaceNode.pack() + realized; } - Ok(BlockNode { body: realized }.pack()) + Ok(BlockNode { + body: realized, + width: Smart::Auto, + height: Smart::Auto, + } + .pack()) } } diff --git a/library/src/text/raw.rs b/library/src/text/raw.rs index a20ec6f39..aa3a10d73 100644 --- a/library/src/text/raw.rs +++ b/library/src/text/raw.rs @@ -199,7 +199,12 @@ impl Show for RawNode { }; if self.block { - realized = BlockNode { body: realized }.pack(); + realized = BlockNode { + body: realized, + width: Smart::Auto, + height: Smart::Auto, + } + .pack(); } Ok(realized) diff --git a/library/src/visualize/shape.rs b/library/src/visualize/shape.rs index 7eddc6a6f..e5259d917 100644 --- a/library/src/visualize/shape.rs +++ b/library/src/visualize/shape.rs @@ -160,6 +160,8 @@ impl Layout for RectNode { ) -> SourceResult { layout( vt, + styles, + regions, ShapeKind::Rect, &self.body, Axes::new(self.width, self.height), @@ -168,8 +170,6 @@ impl Layout for RectNode { styles.get(Self::INSET), styles.get(Self::OUTSET), styles.get(Self::RADIUS), - styles, - regions, ) } } @@ -278,6 +278,8 @@ impl Layout for SquareNode { ) -> SourceResult { layout( vt, + styles, + regions, ShapeKind::Square, &self.body, Axes::new(self.width, self.height), @@ -286,8 +288,6 @@ impl Layout for SquareNode { styles.get(Self::INSET), styles.get(Self::OUTSET), styles.get(Self::RADIUS), - styles, - regions, ) } } @@ -372,6 +372,8 @@ impl Layout for EllipseNode { ) -> SourceResult { layout( vt, + styles, + regions, ShapeKind::Ellipse, &self.body, Axes::new(self.width, self.height), @@ -380,8 +382,6 @@ impl Layout for EllipseNode { styles.get(Self::INSET), styles.get(Self::OUTSET), Corners::splat(Rel::zero()), - styles, - regions, ) } } @@ -485,6 +485,8 @@ impl Layout for CircleNode { ) -> SourceResult { layout( vt, + styles, + regions, ShapeKind::Circle, &self.body, Axes::new(self.width, self.height), @@ -493,8 +495,6 @@ impl Layout for CircleNode { styles.get(Self::INSET), styles.get(Self::OUTSET), Corners::splat(Rel::zero()), - styles, - regions, ) } } @@ -502,6 +502,8 @@ impl Layout for CircleNode { /// Layout a shape. fn layout( vt: &mut Vt, + styles: StyleChain, + regions: Regions, kind: ShapeKind, body: &Option, sizing: Axes>>, @@ -510,8 +512,6 @@ fn layout( mut inset: Sides>, outset: Sides>, radius: Corners>, - styles: StyleChain, - regions: Regions, ) -> SourceResult { let resolved = sizing .zip(regions.base()) diff --git a/src/doc.rs b/src/doc.rs index a15fbca94..0ca930bf8 100644 --- a/src/doc.rs +++ b/src/doc.rs @@ -17,7 +17,7 @@ use crate::model::{ use crate::util::EcoString; /// A finished document with metadata and page frames. -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Clone, Hash)] pub struct Document { /// The page frames. pub pages: Vec, @@ -28,7 +28,7 @@ pub struct Document { } /// A finished layout with elements at fixed positions. -#[derive(Default, Clone)] +#[derive(Default, Clone, Hash)] pub struct Frame { /// The size of the frame. size: Size, @@ -304,12 +304,16 @@ impl Frame { /// Arbitrarily transform the contents of the frame. pub fn transform(&mut self, transform: Transform) { - self.group(|g| g.transform = transform); + if !self.is_empty() { + self.group(|g| g.transform = transform); + } } /// Clip the contents of a frame to its size. pub fn clip(&mut self) { - self.group(|g| g.clips = true); + if !self.is_empty() { + self.group(|g| g.clips = true); + } } /// Wrap the frame's contents in a group and modify that group with `f`. @@ -386,7 +390,7 @@ impl Debug for Frame { } /// The building block frames are composed of. -#[derive(Clone)] +#[derive(Clone, Hash)] pub enum Element { /// A group of elements. Group(Group), @@ -413,7 +417,7 @@ impl Debug for Element { } /// A group of elements with optional clipping. -#[derive(Clone)] +#[derive(Clone, Hash)] pub struct Group { /// The group's frame. pub frame: Frame, @@ -442,7 +446,7 @@ impl Debug for Group { } /// A run of shaped text. -#[derive(Clone, Eq, PartialEq)] +#[derive(Clone, Eq, PartialEq, Hash)] pub struct Text { /// The font the glyphs are contained in. pub font: Font, @@ -477,7 +481,7 @@ impl Debug for Text { } /// A glyph in a run of shaped text. -#[derive(Debug, Copy, Clone, Eq, PartialEq)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct Glyph { /// The glyph's index in the font. pub id: u16, diff --git a/src/geom/mod.rs b/src/geom/mod.rs index 225eb10d2..ebe4436cd 100644 --- a/src/geom/mod.rs +++ b/src/geom/mod.rs @@ -73,7 +73,7 @@ pub trait Get { } /// A geometric shape with optional fill and stroke. -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct Shape { /// The shape's geometry. pub geometry: Geometry, @@ -84,7 +84,7 @@ pub struct Shape { } /// A shape's geometry. -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq, Hash)] pub enum Geometry { /// A line to a point (relative to its position). Line(Point), diff --git a/src/geom/path.rs b/src/geom/path.rs index ffd3db1c6..3a7c3033a 100644 --- a/src/geom/path.rs +++ b/src/geom/path.rs @@ -1,11 +1,11 @@ use super::*; /// A bezier path. -#[derive(Debug, Default, Clone, Eq, PartialEq)] +#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] pub struct Path(pub Vec); /// An element in a bezier path. -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq, Hash)] pub enum PathElement { MoveTo(Point), LineTo(Point), diff --git a/tests/ref/bugs/flow-4.png b/tests/ref/bugs/flow-4.png new file mode 100644 index 000000000..59caa2fd9 Binary files /dev/null and b/tests/ref/bugs/flow-4.png differ diff --git a/tests/ref/compiler/show-selector.png b/tests/ref/compiler/show-selector.png index bbd303d18..9239602c8 100644 Binary files a/tests/ref/compiler/show-selector.png and b/tests/ref/compiler/show-selector.png differ diff --git a/tests/ref/layout/block-sizing.png b/tests/ref/layout/block-sizing.png new file mode 100644 index 000000000..d0a488ead Binary files /dev/null and b/tests/ref/layout/block-sizing.png differ diff --git a/tests/ref/meta/link.png b/tests/ref/meta/link.png index 5d175516a..267490ad0 100644 Binary files a/tests/ref/meta/link.png and b/tests/ref/meta/link.png differ diff --git a/tests/typ/bugs/flow-4.typ b/tests/typ/bugs/flow-4.typ new file mode 100644 index 000000000..f49873f50 --- /dev/null +++ b/tests/typ/bugs/flow-4.typ @@ -0,0 +1,5 @@ +// In this bug, a frame intended for the second region ended up in the first. + +--- +#set page(height: 105pt) +#block(lorem(20)) diff --git a/tests/typ/layout/block-sizing.typ b/tests/typ/layout/block-sizing.typ new file mode 100644 index 000000000..a768c3e3a --- /dev/null +++ b/tests/typ/layout/block-sizing.typ @@ -0,0 +1,16 @@ +// Test blocks with fixed height. + +--- +#set page(height: 100pt) +#set align(center) + +#lorem(10) +#block(width: 80%, height: 60pt, fill: aqua) +#lorem(6) +#block( + breakable: false, + width: 100%, + inset: 4pt, + fill: aqua, + lorem(8) + colbreak(), +) diff --git a/tests/typ/layout/container-fill.typ b/tests/typ/layout/container-fill.typ index ab5913abc..34849d88c 100644 --- a/tests/typ/layout/container-fill.typ +++ b/tests/typ/layout/container-fill.typ @@ -1,6 +1,6 @@ #set page(height: 100pt) #let words = lorem(18).split() -#block(inset: 8pt, fill: aqua, stroke: aqua.darken(30%))[ +#block(inset: 8pt, width: 100%, fill: aqua, stroke: aqua.darken(30%))[ #words.slice(0, 12).join(" ") #box(fill: teal, outset: 2pt)[incididunt] #words.slice(12).join(" ")