diff --git a/library/src/layout/container.rs b/library/src/layout/container.rs index 7814c0834..438233396 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>, - /// The box's baseline shift. - pub baseline: Rel, } #[node] impl BoxNode { + /// The box's baseline shift. + #[property(resolve)] + pub const BASELINE: Rel = Rel::zero(); + + /// The box's background color. See the + /// [rectangle's documentation]($func/rect.fill) for more details. + pub const FILL: Option = None; + + /// The box's border color. See the + /// [rectangle's documentation]($func/rect.stroke) for more details. + #[property(resolve, fold)] + pub const STROKE: Sides>> = 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>> = 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>> = 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>> = Sides::splat(Rel::zero()); + fn construct(_: &Vm, args: &mut Args) -> SourceResult { - let body = args.eat::()?.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 = None; + + /// The block's border color. See the + /// [rectangle's documentation]($func/rect.stroke) for more details. + #[property(resolve, fold)] + pub const STROKE: Sides>> = 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>> = 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>> = 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>> = 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 { - 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 { - 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 8da5cd6e4..bc7882431 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 da2e1f00d..2d1cf185e 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 d29cb7bf6..a20ec6f39 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 8a2567625..7eddc6a6f 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>> = 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>> = 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 64f7ae91c..a15fbca94 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, + stroke: Sides>, + outset: Sides>, + radius: Corners>, + ) { + 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 799fcf93f..bbd303d18 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/container-fill.png b/tests/ref/layout/container-fill.png new file mode 100644 index 000000000..c2cc78d4e Binary files /dev/null and b/tests/ref/layout/container-fill.png differ diff --git a/tests/ref/meta/link.png b/tests/ref/meta/link.png index 604e09d0a..5d175516a 100644 Binary files a/tests/ref/meta/link.png and b/tests/ref/meta/link.png differ diff --git a/tests/typ/compiler/show-selector.typ b/tests/typ/compiler/show-selector.typ index ebd84837d..d1229eee4 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 e0fdb7930..705c11125 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 1d82113c3..b5dbf96f0 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 000000000..ab5913abc --- /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(" ") +]