Fill and stroke properties for containers

This commit is contained in:
Laurenz 2023-02-13 12:04:26 +01:00
parent db49b628f7
commit 72b60dfde7
13 changed files with 216 additions and 71 deletions

View File

@ -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
/// ]
/// #block(
/// fill: luma(230),
/// inset: 8pt,
/// radius: 4pt,
/// lorem(30),
/// )
/// ```
///
/// #[
/// #show heading: it => block(it.title)
/// = Block
/// Some more text
/// ]
/// 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))
}
}

View File

@ -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())
}
}

View File

@ -184,7 +184,6 @@ impl Show for OutlineNode {
body: filler.clone(),
width: Sizing::Fr(Fr::one()),
height: Smart::Auto,
baseline: Rel::zero(),
}
.pack(),
);

View File

@ -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)

View File

@ -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);
}
}

View File

@ -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);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

@ -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),

View File

@ -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)
---

View File

@ -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)
الجزيئات الضخمة الأربعة الضرورية للحياة.
---

View File

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