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(" ")