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, pub width: Sizing,
/// The box's height. /// The box's height.
pub height: Smart<Rel<Length>>, pub height: Smart<Rel<Length>>,
/// The box's baseline shift.
pub baseline: Rel<Length>,
} }
#[node] #[node]
impl BoxNode { 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> { 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 width = args.named("width")?.unwrap_or_default();
let height = args.named("height")?.unwrap_or_default(); let height = args.named("height")?.unwrap_or_default();
let baseline = args.named("baseline")?.unwrap_or_default(); Ok(Self { body, width, height }.pack())
Ok(Self { body, width, height, baseline }.pack())
} }
} }
@ -96,42 +133,86 @@ impl Layout for BoxNode {
.map(|(s, b)| s.map(|v| v.relative_to(b))) .map(|(s, b)| s.map(|v| v.relative_to(b)))
.unwrap_or(regions.size); .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 // Select the appropriate base and expansion for the child depending
// on whether it is automatically or relatively sized. // on whether it is automatically or relatively sized.
let is_auto = sizing.as_ref().map(Smart::is_auto); let is_auto = sizing.as_ref().map(Smart::is_auto);
let expand = regions.expand | !is_auto; let expand = regions.expand | !is_auto;
let pod = Regions::one(size, expand); 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. // 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() { if !shift.is_zero() {
frame.set_baseline(frame.baseline() - shift); 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)) Ok(Fragment::frame(frame))
} }
} }
/// # Block /// # 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 /// Such a container can be used to separate content, size it and give it a
/// block-level. This is especially useful when writing show rules. /// 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 /// ```example
/// #[ /// #block(
/// #show heading: it => it.title /// fill: luma(230),
/// = No block /// inset: 8pt,
/// Some text /// 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) /// #show heading: it => block(it.title)
/// = Block /// = Blocky
/// Some more text /// 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 /// ## Parameters
@ -158,16 +239,44 @@ impl Layout for BoxNode {
#[func] #[func]
#[capable(Layout)] #[capable(Layout)]
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
pub struct BlockNode(pub Content); pub struct BlockNode {
pub body: Content,
}
#[node] #[node]
impl BlockNode { 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. /// The spacing between the previous and this block.
#[property(skip)] #[property(skip)]
pub const ABOVE: VNode = VNode::block_spacing(Em::new(1.2).into()); pub const ABOVE: VNode = VNode::block_spacing(Em::new(1.2).into());
/// The spacing between this and the following block. /// The spacing between this and the following block.
#[property(skip)] #[property(skip)]
pub const BELOW: VNode = VNode::block_spacing(Em::new(1.2).into()); pub const BELOW: VNode = VNode::block_spacing(Em::new(1.2).into());
/// Whether this block must stick to the following one. /// Whether this block must stick to the following one.
/// ///
/// Use this to prevent page breaks between e.g. a heading and its body. /// 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; pub const STICKY: bool = false;
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> { 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(...) { fn set(...) {
@ -198,7 +308,37 @@ impl Layout for BlockNode {
styles: StyleChain, styles: StyleChain,
regions: Regions, regions: Regions,
) -> SourceResult<Fragment> { ) -> 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 { if numbers != Value::None {
realized = numbers.display() + SpaceNode.pack() + realized; 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(), body: filler.clone(),
width: Sizing::Fr(Fr::one()), width: Sizing::Fr(Fr::one()),
height: Smart::Auto, height: Smart::Auto,
baseline: Rel::zero(),
} }
.pack(), .pack(),
); );

View File

@ -63,17 +63,16 @@ use crate::prelude::*;
/// ````example /// ````example
/// // Display inline code in a small box /// // Display inline code in a small box
/// // that retains the correct baseline. /// // that retains the correct baseline.
/// #show raw.where(block: false): it => box(rect( /// #show raw.where(block: false): box.with(
/// fill: luma(240), /// fill: luma(240),
/// inset: (x: 3pt, y: 0pt), /// inset: (x: 3pt, y: 0pt),
/// outset: (y: 3pt), /// outset: (y: 3pt),
/// radius: 2pt, /// radius: 2pt,
/// it, /// )
/// ))
/// ///
/// // Display block code in a larger box /// // Display block code in a larger block
/// // with more padding. /// // with more padding.
/// #show raw.where(block: true): rect.with( /// #show raw.where(block: true): block.with(
/// fill: luma(240), /// fill: luma(240),
/// inset: 10pt, /// inset: 10pt,
/// radius: 4pt, /// radius: 4pt,
@ -200,7 +199,7 @@ impl Show for RawNode {
}; };
if self.block { if self.block {
realized = BlockNode(realized).pack(); realized = BlockNode { body: realized }.pack();
} }
Ok(realized) Ok(realized)

View File

@ -133,28 +133,13 @@ impl RectNode {
/// current [text edges]($func/text.top-edge). /// current [text edges]($func/text.top-edge).
/// ///
/// ```example /// ```example
/// A #box(rect(inset: 0pt)[tight]) fit. /// #rect(inset: 0pt)[Tight])
/// ``` /// ```
#[property(resolve, fold)] #[property(resolve, fold)]
pub const INSET: Sides<Option<Rel<Length>>> = Sides::splat(Abs::pt(5.0).into()); 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. /// How much to expand the rectangle's size without affecting the layout.
/// /// See the [box's documentation]($func/box.outset) for more details.
/// 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.
/// ```
#[property(resolve, fold)] #[property(resolve, fold)]
pub const OUTSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero()); pub const OUTSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero());
@ -535,7 +520,6 @@ fn layout(
let mut frame; let mut frame;
if let Some(child) = body { if let Some(child) = body {
let region = resolved.unwrap_or(regions.base()); let region = resolved.unwrap_or(regions.base());
if kind.is_round() { if kind.is_round() {
inset = inset.map(|side| side + Ratio::new(0.5 - SQRT_2 / 4.0)); inset = inset.map(|side| side + Ratio::new(0.5 - SQRT_2 / 4.0));
} }
@ -565,7 +549,7 @@ fn layout(
frame = Frame::new(size); frame = Frame::new(size);
} }
// Add fill and/or stroke. // Prepare stroke.
let stroke = match stroke { let stroke = match stroke {
Smart::Auto if fill.is_none() => Sides::splat(Some(Stroke::default())), Smart::Auto if fill.is_none() => Sides::splat(Some(Stroke::default())),
Smart::Auto => Sides::splat(None), Smart::Auto => Sides::splat(None),
@ -574,21 +558,16 @@ fn layout(
} }
}; };
let outset = outset.relative_to(frame.size()); // Add fill and/or stroke.
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);
if fill.is_some() || stroke.iter().any(Option::is_some) { if fill.is_some() || stroke.iter().any(Option::is_some) {
if kind.is_round() { 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); let shape = ellipse(size, fill, stroke.left);
frame.prepend(pos, Element::Shape(shape)); frame.prepend(pos, Element::Shape(shape));
} else { } else {
frame.prepend_multiple( frame.fill_and_stroke(fill, stroke, outset, radius);
rounded_rect(size, radius, fill, stroke)
.into_iter()
.map(|x| (pos, Element::Shape(x))),
)
} }
} }

View File

@ -7,8 +7,8 @@ use std::sync::Arc;
use crate::font::Font; use crate::font::Font;
use crate::geom::{ use crate::geom::{
self, Abs, Align, Axes, Color, Dir, Em, Geometry, Numeric, Paint, Point, RgbaColor, self, rounded_rect, Abs, Align, Axes, Color, Corners, Dir, Em, Geometry, Numeric,
Shape, Size, Stroke, Transform, Paint, Point, Rel, RgbaColor, Shape, Sides, Size, Stroke, Transform,
}; };
use crate::image::Image; use crate::image::Image;
use crate::model::{ use crate::model::{
@ -271,6 +271,9 @@ impl Frame {
/// Attach the metadata from this style chain to the frame. /// Attach the metadata from this style chain to the frame.
pub fn meta(&mut self, styles: StyleChain) { pub fn meta(&mut self, styles: StyleChain) {
if self.is_empty() {
return;
}
for meta in styles.get(Meta::DATA) { for meta in styles.get(Meta::DATA) {
if matches!(meta, Meta::Hidden) { if matches!(meta, Meta::Hidden) {
self.clear(); 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. /// Arbitrarily transform the contents of the frame.
pub fn transform(&mut self, transform: Transform) { pub fn transform(&mut self, transform: Transform) {
self.group(|g| g.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. // Inline code.
#show raw.where(block: false): it => box(rect( #show raw.where(block: false): box.with(
radius: 2pt, radius: 2pt,
outset: (y: 3pt), outset: (y: 2.5pt),
inset: (x: 3pt, y: 0pt), inset: (x: 3pt, y: 0pt),
fill: luma(230), fill: luma(230),
it, )
))
// Code blocks. // Code blocks.
#show raw.where(block: true): rect.with( #show raw.where(block: true): block.with(
outset: -3pt, outset: -3pt,
inset: 11pt, inset: 11pt,
fill: luma(230), fill: luma(230),

View File

@ -28,7 +28,7 @@ Treeworld, the World of worlds, is a world.
--- ---
// This is a fun one. // This is a fun one.
#set par(justify: true) #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) #lorem(5)
--- ---

View File

@ -6,10 +6,10 @@
#set text(lang: "ar", "Noto Sans Arabic", "IBM Plex Serif") #set text(lang: "ar", "Noto Sans Arabic", "IBM Plex Serif")
#set columns(gutter: 30pt) #set columns(gutter: 30pt)
#box(rect(fill: conifer, height: 8pt, width: 6pt)) وتحفيز #box(fill: conifer, height: 8pt, width: 6pt) وتحفيز
العديد من التفاعلات الكيميائية. (DNA) من أهم الأحماض النووية التي تُشكِّل العديد من التفاعلات الكيميائية. (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(" ")
]