Fill and stroke properties for containers
This commit is contained in:
parent
db49b628f7
commit
72b60dfde7
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -184,7 +184,6 @@ impl Show for OutlineNode {
|
||||
body: filler.clone(),
|
||||
width: Sizing::Fr(Fr::one()),
|
||||
height: Smart::Auto,
|
||||
baseline: Rel::zero(),
|
||||
}
|
||||
.pack(),
|
||||
);
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
26
src/doc.rs
26
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);
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
BIN
tests/ref/layout/container-fill.png
Normal file
BIN
tests/ref/layout/container-fill.png
Normal file
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 |
@ -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),
|
||||
|
@ -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)
|
||||
|
||||
---
|
||||
|
@ -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)
|
||||
الجزيئات الضخمة الأربعة الضرورية للحياة.
|
||||
|
||||
---
|
||||
|
7
tests/typ/layout/container-fill.typ
Normal file
7
tests/typ/layout/container-fill.typ
Normal 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(" ")
|
||||
]
|
Loading…
x
Reference in New Issue
Block a user