summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2023-02-13 12:04:26 +0100
committerLaurenz <laurmaedje@gmail.com>2023-02-13 12:18:34 +0100
commit72b60dfde751b4a2ab279aa1fcfa559b4a75eb51 (patch)
tree013065649ef0972bd342ad5c5821842a9c48c740
parentdb49b628f73d3c328aacadbb9126616e7cccfc49 (diff)
Fill and stroke properties for containers
-rw-r--r--library/src/layout/container.rs190
-rw-r--r--library/src/meta/heading.rs2
-rw-r--r--library/src/meta/outline.rs1
-rw-r--r--library/src/text/raw.rs11
-rw-r--r--library/src/visualize/shape.rs37
-rw-r--r--src/doc.rs26
-rw-r--r--tests/ref/compiler/show-selector.pngbin20689 -> 20686 bytes
-rw-r--r--tests/ref/layout/container-fill.pngbin0 -> 18080 bytes
-rw-r--r--tests/ref/meta/link.pngbin49384 -> 49325 bytes
-rw-r--r--tests/typ/compiler/show-selector.typ9
-rw-r--r--tests/typ/compiler/show-text.typ2
-rw-r--r--tests/typ/layout/columns.typ4
-rw-r--r--tests/typ/layout/container-fill.typ7
13 files changed, 217 insertions, 72 deletions
diff --git a/library/src/layout/container.rs b/library/src/layout/container.rs
index 7814c083..43823339 100644
--- a/library/src/layout/container.rs
+++ b/library/src/layout/container.rs
@@ -60,18 +60,55 @@ pub struct BoxNode {
pub width: Sizing,
/// The box's height.
pub height: Smart<Rel<Length>>,
- /// The box's baseline shift.
- pub baseline: Rel<Length>,
}
#[node]
impl BoxNode {
+ /// The box's baseline shift.
+ #[property(resolve)]
+ pub const BASELINE: Rel<Length> = Rel::zero();
+
+ /// The box's background color. See the
+ /// [rectangle's documentation]($func/rect.fill) for more details.
+ pub const FILL: Option<Paint> = None;
+
+ /// The box's border color. See the
+ /// [rectangle's documentation]($func/rect.stroke) for more details.
+ #[property(resolve, fold)]
+ pub const STROKE: Sides<Option<Option<PartialStroke>>> = Sides::splat(None);
+
+ /// How much to round the box's corners. See the [rectangle's
+ /// documentation]($func/rect.radius) for more details.
+ #[property(resolve, fold)]
+ pub const RADIUS: Corners<Option<Rel<Length>>> = Corners::splat(Rel::zero());
+
+ /// How much to pad the box's content. See the [rectangle's
+ /// documentation]($func/rect.inset) for more details.
+ #[property(resolve, fold)]
+ pub const INSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero());
+
+ /// How much to expand the box's size without affecting the layout.
+ ///
+ /// This is useful to prevent padding from affecting line layout. For a
+ /// generalized version of the example below, see the documentation for the
+ /// [raw text's block parameter]($func/raw.block).
+ ///
+ /// ```example
+ /// An inline
+ /// #box(
+ /// fill: luma(235),
+ /// inset: (x: 3pt, y: 0pt),
+ /// outset: (y: 3pt),
+ /// radius: 2pt,
+ /// )[rectangle].
+ #[property(resolve, fold)]
+ pub const OUTSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero());
+
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
- let body = args.eat::<Content>()?.unwrap_or_default();
+ let body = args.eat()?.unwrap_or_default();
let width = args.named("width")?.unwrap_or_default();
let height = args.named("height")?.unwrap_or_default();
- let baseline = args.named("baseline")?.unwrap_or_default();
- Ok(Self { body, width, height, baseline }.pack())
+ Ok(Self { body, width, height }.pack())
}
}
@@ -96,42 +133,86 @@ impl Layout for BoxNode {
.map(|(s, b)| s.map(|v| v.relative_to(b)))
.unwrap_or(regions.size);
+ // Apply inset.
+ let mut child = self.body.clone();
+ let inset = styles.get(Self::INSET);
+ if inset.iter().any(|v| !v.is_zero()) {
+ child = child.clone().padded(inset.map(|side| side.map(Length::from)));
+ }
+
// 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 = self.body.layout(vt, styles, pod)?.into_frame();
+ let mut frame = child.layout(vt, styles, pod)?.into_frame();
// Apply baseline shift.
- let shift = self.baseline.resolve(styles).relative_to(frame.height());
+ let shift = styles.get(Self::BASELINE).relative_to(frame.height());
if !shift.is_zero() {
frame.set_baseline(frame.baseline() - shift);
}
+ // Prepare fill and stroke.
+ let fill = styles.get(Self::FILL);
+ let stroke = styles
+ .get(Self::STROKE)
+ .map(|s| s.map(PartialStroke::unwrap_or_default));
+
+ // Add fill and/or stroke.
+ if fill.is_some() || stroke.iter().any(Option::is_some) {
+ let outset = styles.get(Self::OUTSET);
+ let radius = styles.get(Self::RADIUS);
+ frame.fill_and_stroke(fill, stroke, outset, radius);
+ }
+
+ // Apply metadata.
+ frame.meta(styles);
+
Ok(Fragment::frame(frame))
}
}
/// # Block
-/// A block-level container that places content into a separate flow.
+/// A block-level container.
///
-/// This can be used to force elements that would otherwise be inline to become
-/// block-level. This is especially useful when writing show rules.
+/// Such a container can be used to separate content, size it and give it a
+/// background or border.
///
-/// ## Example
+/// ## 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.
/// ```example
-/// #[
-/// #show heading: it => it.title
-/// = No block
-/// Some text
-/// ]
-///
-/// #[
-/// #show heading: it => block(it.title)
-/// = Block
-/// Some more text
-/// ]
+/// #block(
+/// fill: luma(230),
+/// inset: 8pt,
+/// radius: 4pt,
+/// lorem(30),
+/// )
+/// ```
+///
+/// Blocks are also useful to force elements that would otherwise be inline to
+/// become block-level, especially when writing show rules.
+/// ```example
+/// #show heading: it => it.title
+/// = Blockless
+/// More text.
+///
+/// #show heading: it => block(it.title)
+/// = Blocky
+/// 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
@@ -158,16 +239,44 @@ impl Layout for BoxNode {
#[func]
#[capable(Layout)]
#[derive(Debug, Hash)]
-pub struct BlockNode(pub Content);
+pub struct BlockNode {
+ pub body: Content,
+}
#[node]
impl BlockNode {
+ /// The block's background color. See the
+ /// [rectangle's documentation]($func/rect.fill) for more details.
+ pub const FILL: Option<Paint> = None;
+
+ /// The block's border color. See the
+ /// [rectangle's documentation]($func/rect.stroke) for more details.
+ #[property(resolve, fold)]
+ pub const STROKE: Sides<Option<Option<PartialStroke>>> = Sides::splat(None);
+
+ /// How much to round the block's corners. See the [rectangle's
+ /// documentation]($func/rect.radius) for more details.
+ #[property(resolve, fold)]
+ pub const RADIUS: Corners<Option<Rel<Length>>> = Corners::splat(Rel::zero());
+
+ /// How much to pad the block's content. See the [rectangle's
+ /// documentation]($func/rect.inset) for more details.
+ #[property(resolve, fold)]
+ pub const INSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero());
+
+ /// How much to expand the block's size without affecting the layout. See
+ /// the [rectangle's documentation]($func/rect.outset) for more details.
+ #[property(resolve, fold)]
+ pub const OUTSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero());
+
/// The spacing between the previous and this block.
#[property(skip)]
pub const ABOVE: VNode = VNode::block_spacing(Em::new(1.2).into());
+
/// The spacing between this and the following block.
#[property(skip)]
pub const BELOW: VNode = VNode::block_spacing(Em::new(1.2).into());
+
/// Whether this block must stick to the following one.
///
/// Use this to prevent page breaks between e.g. a heading and its body.
@@ -175,7 +284,8 @@ impl BlockNode {
pub const STICKY: bool = false;
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
- Ok(Self(args.eat()?.unwrap_or_default()).pack())
+ let body = args.eat()?.unwrap_or_default();
+ Ok(Self { body }.pack())
}
fn set(...) {
@@ -198,7 +308,37 @@ impl Layout for BlockNode {
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
- self.0.layout(vt, styles, regions)
+ // Apply inset.
+ let mut child = self.body.clone();
+ let inset = styles.get(Self::INSET);
+ if inset.iter().any(|v| !v.is_zero()) {
+ child = child.clone().padded(inset.map(|side| side.map(Length::from)));
+ }
+
+ // Layout the child.
+ let mut frames = child.layout(vt, styles, regions)?.into_frames();
+
+ // Prepare fill and stroke.
+ let fill = styles.get(Self::FILL);
+ let stroke = styles
+ .get(Self::STROKE)
+ .map(|s| s.map(PartialStroke::unwrap_or_default));
+
+ // Add fill and/or stroke.
+ if fill.is_some() || stroke.iter().any(Option::is_some) {
+ let outset = styles.get(Self::OUTSET);
+ let radius = styles.get(Self::RADIUS);
+ for frame in &mut frames {
+ frame.fill_and_stroke(fill, stroke, outset, radius);
+ }
+ }
+
+ // Apply metadata.
+ for frame in &mut frames {
+ frame.meta(styles);
+ }
+
+ Ok(Fragment::frames(frames))
}
}
diff --git a/library/src/meta/heading.rs b/library/src/meta/heading.rs
index 8da5cd6e..bc788243 100644
--- a/library/src/meta/heading.rs
+++ b/library/src/meta/heading.rs
@@ -147,7 +147,7 @@ impl Show for HeadingNode {
if numbers != Value::None {
realized = numbers.display() + SpaceNode.pack() + realized;
}
- Ok(BlockNode(realized).pack())
+ Ok(BlockNode { body: realized }.pack())
}
}
diff --git a/library/src/meta/outline.rs b/library/src/meta/outline.rs
index da2e1f00..2d1cf185 100644
--- a/library/src/meta/outline.rs
+++ b/library/src/meta/outline.rs
@@ -184,7 +184,6 @@ impl Show for OutlineNode {
body: filler.clone(),
width: Sizing::Fr(Fr::one()),
height: Smart::Auto,
- baseline: Rel::zero(),
}
.pack(),
);
diff --git a/library/src/text/raw.rs b/library/src/text/raw.rs
index d29cb7bf..a20ec6f3 100644
--- a/library/src/text/raw.rs
+++ b/library/src/text/raw.rs
@@ -63,17 +63,16 @@ use crate::prelude::*;
/// ````example
/// // Display inline code in a small box
/// // that retains the correct baseline.
-/// #show raw.where(block: false): it => box(rect(
+/// #show raw.where(block: false): box.with(
/// fill: luma(240),
/// inset: (x: 3pt, y: 0pt),
/// outset: (y: 3pt),
/// radius: 2pt,
-/// it,
-/// ))
+/// )
///
-/// // Display block code in a larger box
+/// // Display block code in a larger block
/// // with more padding.
-/// #show raw.where(block: true): rect.with(
+/// #show raw.where(block: true): block.with(
/// fill: luma(240),
/// inset: 10pt,
/// radius: 4pt,
@@ -200,7 +199,7 @@ impl Show for RawNode {
};
if self.block {
- realized = BlockNode(realized).pack();
+ realized = BlockNode { body: realized }.pack();
}
Ok(realized)
diff --git a/library/src/visualize/shape.rs b/library/src/visualize/shape.rs
index 8a256762..7eddc6a6 100644
--- a/library/src/visualize/shape.rs
+++ b/library/src/visualize/shape.rs
@@ -133,28 +133,13 @@ impl RectNode {
/// current [text edges]($func/text.top-edge).
///
/// ```example
- /// A #box(rect(inset: 0pt)[tight]) fit.
+ /// #rect(inset: 0pt)[Tight])
/// ```
#[property(resolve, fold)]
pub const INSET: Sides<Option<Rel<Length>>> = Sides::splat(Abs::pt(5.0).into());
/// How much to expand the rectangle's size without affecting the layout.
- ///
- /// This is, for instance, useful to prevent an inline rectangle from
- /// affecting line layout. For a generalized version of the example below,
- /// see the documentation for the
- /// [raw text's block parameter]($func/raw.block).
- ///
- /// ```example
- /// This
- /// #box(rect(
- /// fill: luma(235),
- /// inset: (x: 3pt, y: 0pt),
- /// outset: (y: 3pt),
- /// radius: 2pt,
- /// )[rectangle])
- /// is inline.
- /// ```
+ /// See the [box's documentation]($func/box.outset) for more details.
#[property(resolve, fold)]
pub const OUTSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero());
@@ -535,7 +520,6 @@ fn layout(
let mut frame;
if let Some(child) = body {
let region = resolved.unwrap_or(regions.base());
-
if kind.is_round() {
inset = inset.map(|side| side + Ratio::new(0.5 - SQRT_2 / 4.0));
}
@@ -565,7 +549,7 @@ fn layout(
frame = Frame::new(size);
}
- // Add fill and/or stroke.
+ // Prepare stroke.
let stroke = match stroke {
Smart::Auto if fill.is_none() => Sides::splat(Some(Stroke::default())),
Smart::Auto => Sides::splat(None),
@@ -574,21 +558,16 @@ fn layout(
}
};
- let outset = outset.relative_to(frame.size());
- let size = frame.size() + outset.sum_by_axis();
- let radius = radius.map(|side| side.relative_to(size.x.min(size.y) / 2.0));
- let pos = Point::new(-outset.left, -outset.top);
-
+ // Add fill and/or stroke.
if fill.is_some() || stroke.iter().any(Option::is_some) {
if kind.is_round() {
+ let outset = outset.relative_to(frame.size());
+ let size = frame.size() + outset.sum_by_axis();
+ let pos = Point::new(-outset.left, -outset.top);
let shape = ellipse(size, fill, stroke.left);
frame.prepend(pos, Element::Shape(shape));
} else {
- frame.prepend_multiple(
- rounded_rect(size, radius, fill, stroke)
- .into_iter()
- .map(|x| (pos, Element::Shape(x))),
- )
+ frame.fill_and_stroke(fill, stroke, outset, radius);
}
}
diff --git a/src/doc.rs b/src/doc.rs
index 64f7ae91..a15fbca9 100644
--- a/src/doc.rs
+++ b/src/doc.rs
@@ -7,8 +7,8 @@ use std::sync::Arc;
use crate::font::Font;
use crate::geom::{
- self, Abs, Align, Axes, Color, Dir, Em, Geometry, Numeric, Paint, Point, RgbaColor,
- Shape, Size, Stroke, Transform,
+ self, rounded_rect, Abs, Align, Axes, Color, Corners, Dir, Em, Geometry, Numeric,
+ Paint, Point, Rel, RgbaColor, Shape, Sides, Size, Stroke, Transform,
};
use crate::image::Image;
use crate::model::{
@@ -271,6 +271,9 @@ impl Frame {
/// Attach the metadata from this style chain to the frame.
pub fn meta(&mut self, styles: StyleChain) {
+ if self.is_empty() {
+ return;
+ }
for meta in styles.get(Meta::DATA) {
if matches!(meta, Meta::Hidden) {
self.clear();
@@ -280,6 +283,25 @@ impl Frame {
}
}
+ /// Add a fill and stroke with optional radius and outset to the frame.
+ pub fn fill_and_stroke(
+ &mut self,
+ fill: Option<Paint>,
+ stroke: Sides<Option<Stroke>>,
+ outset: Sides<Rel<Abs>>,
+ radius: Corners<Rel<Abs>>,
+ ) {
+ let outset = outset.relative_to(self.size());
+ let size = self.size() + outset.sum_by_axis();
+ let pos = Point::new(-outset.left, -outset.top);
+ let radius = radius.map(|side| side.relative_to(size.x.min(size.y) / 2.0));
+ self.prepend_multiple(
+ rounded_rect(size, radius, fill, stroke)
+ .into_iter()
+ .map(|x| (pos, Element::Shape(x))),
+ )
+ }
+
/// Arbitrarily transform the contents of the frame.
pub fn transform(&mut self, transform: Transform) {
self.group(|g| g.transform = transform);
diff --git a/tests/ref/compiler/show-selector.png b/tests/ref/compiler/show-selector.png
index 799fcf93..bbd303d1 100644
--- a/tests/ref/compiler/show-selector.png
+++ b/tests/ref/compiler/show-selector.png
Binary files differ
diff --git a/tests/ref/layout/container-fill.png b/tests/ref/layout/container-fill.png
new file mode 100644
index 00000000..c2cc78d4
--- /dev/null
+++ b/tests/ref/layout/container-fill.png
Binary files differ
diff --git a/tests/ref/meta/link.png b/tests/ref/meta/link.png
index 604e09d0..5d175516 100644
--- a/tests/ref/meta/link.png
+++ b/tests/ref/meta/link.png
Binary files differ
diff --git a/tests/typ/compiler/show-selector.typ b/tests/typ/compiler/show-selector.typ
index ebd84837..d1229eee 100644
--- a/tests/typ/compiler/show-selector.typ
+++ b/tests/typ/compiler/show-selector.typ
@@ -2,16 +2,15 @@
---
// Inline code.
-#show raw.where(block: false): it => box(rect(
+#show raw.where(block: false): box.with(
radius: 2pt,
- outset: (y: 3pt),
+ outset: (y: 2.5pt),
inset: (x: 3pt, y: 0pt),
fill: luma(230),
- it,
-))
+)
// Code blocks.
-#show raw.where(block: true): rect.with(
+#show raw.where(block: true): block.with(
outset: -3pt,
inset: 11pt,
fill: luma(230),
diff --git a/tests/typ/compiler/show-text.typ b/tests/typ/compiler/show-text.typ
index e0fdb793..705c1112 100644
--- a/tests/typ/compiler/show-text.typ
+++ b/tests/typ/compiler/show-text.typ
@@ -28,7 +28,7 @@ Treeworld, the World of worlds, is a world.
---
// This is a fun one.
#set par(justify: true)
-#show regex("\S"): letter => box(rect(inset: 2pt, upper(letter)))
+#show regex("\S"): letter => box(stroke: 1pt, inset: 2pt, upper(letter))
#lorem(5)
---
diff --git a/tests/typ/layout/columns.typ b/tests/typ/layout/columns.typ
index 1d82113c..b5dbf96f 100644
--- a/tests/typ/layout/columns.typ
+++ b/tests/typ/layout/columns.typ
@@ -6,10 +6,10 @@
#set text(lang: "ar", "Noto Sans Arabic", "IBM Plex Serif")
#set columns(gutter: 30pt)
-#box(rect(fill: conifer, height: 8pt, width: 6pt)) وتحفيز
+#box(fill: conifer, height: 8pt, width: 6pt) وتحفيز
العديد من التفاعلات الكيميائية. (DNA) من أهم الأحماض النووية التي تُشكِّل
إلى جانب كل من البروتينات والليبيدات والسكريات المتعددة
-#box(rect(fill: eastern, height: 8pt, width: 6pt))
+#box(fill: eastern, height: 8pt, width: 6pt)
الجزيئات الضخمة الأربعة الضرورية للحياة.
---
diff --git a/tests/typ/layout/container-fill.typ b/tests/typ/layout/container-fill.typ
new file mode 100644
index 00000000..ab5913ab
--- /dev/null
+++ b/tests/typ/layout/container-fill.typ
@@ -0,0 +1,7 @@
+#set page(height: 100pt)
+#let words = lorem(18).split()
+#block(inset: 8pt, fill: aqua, stroke: aqua.darken(30%))[
+ #words.slice(0, 12).join(" ")
+ #box(fill: teal, outset: 2pt)[incididunt]
+ #words.slice(12).join(" ")
+]