mirror of
https://github.com/typst/typst
synced 2025-05-15 09:35:28 +08:00
504 lines
15 KiB
Rust
504 lines
15 KiB
Rust
use typst::eval::AutoValue;
|
|
|
|
use super::VElem;
|
|
use crate::layout::Spacing;
|
|
use crate::prelude::*;
|
|
|
|
/// An inline-level container that sizes content.
|
|
///
|
|
/// All elements except inline math, text, and boxes are block-level and cannot
|
|
/// occur inside of a paragraph. The box function can be used to integrate such
|
|
/// elements into a paragraph. Boxes take the size of their contents by default
|
|
/// but can also be sized explicitly.
|
|
///
|
|
/// # Example
|
|
/// ```example
|
|
/// Refer to the docs
|
|
/// #box(
|
|
/// height: 9pt,
|
|
/// image("docs.svg")
|
|
/// )
|
|
/// for more information.
|
|
/// ```
|
|
#[elem(Layout)]
|
|
pub struct BoxElem {
|
|
/// The width of the box.
|
|
///
|
|
/// Boxes can have [fractional]($fraction) widths, as the example below
|
|
/// demonstrates.
|
|
///
|
|
/// _Note:_ Currently, only boxes and only their widths might be fractionally
|
|
/// sized within paragraphs. Support for fractionally sized images, shapes,
|
|
/// and more might be added in the future.
|
|
///
|
|
/// ```example
|
|
/// Line in #box(width: 1fr, line(length: 100%)) between.
|
|
/// ```
|
|
pub width: Sizing,
|
|
|
|
/// The height of the box.
|
|
pub height: Smart<Rel<Length>>,
|
|
|
|
/// An amount to shift the box's baseline by.
|
|
///
|
|
/// ```example
|
|
/// Image: #box(baseline: 40%, image("tiger.jpg", width: 2cm)).
|
|
/// ```
|
|
#[resolve]
|
|
pub baseline: Rel<Length>,
|
|
|
|
/// The box's background color. See the
|
|
/// [rectangle's documentation]($rect.fill) for more details.
|
|
pub fill: Option<Paint>,
|
|
|
|
/// The box's border color. See the
|
|
/// [rectangle's documentation]($rect.stroke) for more details.
|
|
#[resolve]
|
|
#[fold]
|
|
pub stroke: Sides<Option<Option<Stroke>>>,
|
|
|
|
/// How much to round the box's corners. See the
|
|
/// [rectangle's documentation]($rect.radius) for more details.
|
|
#[resolve]
|
|
#[fold]
|
|
pub radius: Corners<Option<Rel<Length>>>,
|
|
|
|
/// How much to pad the box's content.
|
|
///
|
|
/// _Note:_ When the box contains text, its exact size depends on the
|
|
/// current [text edges]($text.top-edge).
|
|
///
|
|
/// ```example
|
|
/// #rect(inset: 0pt)[Tight]
|
|
/// ```
|
|
#[resolve]
|
|
#[fold]
|
|
pub inset: Sides<Option<Rel<Length>>>,
|
|
|
|
/// 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]($raw.block).
|
|
///
|
|
/// ```example
|
|
/// An inline
|
|
/// #box(
|
|
/// fill: luma(235),
|
|
/// inset: (x: 3pt, y: 0pt),
|
|
/// outset: (y: 3pt),
|
|
/// radius: 2pt,
|
|
/// )[rectangle].
|
|
/// ```
|
|
#[resolve]
|
|
#[fold]
|
|
pub outset: Sides<Option<Rel<Length>>>,
|
|
|
|
/// Whether to clip the content inside the box.
|
|
#[default(false)]
|
|
pub clip: bool,
|
|
|
|
/// The contents of the box.
|
|
#[positional]
|
|
pub body: Option<Content>,
|
|
}
|
|
|
|
impl Layout for BoxElem {
|
|
#[tracing::instrument(name = "BoxElem::layout", skip_all)]
|
|
fn layout(
|
|
&self,
|
|
vt: &mut Vt,
|
|
styles: StyleChain,
|
|
regions: Regions,
|
|
) -> SourceResult<Fragment> {
|
|
let width = match self.width(styles) {
|
|
Sizing::Auto => Smart::Auto,
|
|
Sizing::Rel(rel) => Smart::Custom(rel),
|
|
Sizing::Fr(_) => Smart::Custom(Ratio::one().into()),
|
|
};
|
|
|
|
// Resolve the sizing to a concrete size.
|
|
let sizing = Axes::new(width, self.height(styles));
|
|
let expand = sizing.as_ref().map(Smart::is_custom);
|
|
let size = sizing
|
|
.resolve(styles)
|
|
.zip_map(regions.base(), |s, b| s.map(|v| v.relative_to(b)))
|
|
.unwrap_or(regions.base());
|
|
|
|
// Apply inset.
|
|
let mut body = self.body(styles).unwrap_or_default();
|
|
let inset = self.inset(styles);
|
|
if inset.iter().any(|v| !v.is_zero()) {
|
|
body = body.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 pod = Regions::one(size, expand);
|
|
let mut frame = body.layout(vt, styles, pod)?.into_frame();
|
|
|
|
// Enforce correct size.
|
|
*frame.size_mut() = expand.select(size, frame.size());
|
|
|
|
// Apply baseline shift.
|
|
let shift = self.baseline(styles).relative_to(frame.height());
|
|
if !shift.is_zero() {
|
|
frame.set_baseline(frame.baseline() - shift);
|
|
}
|
|
|
|
// Prepare fill and stroke.
|
|
let fill = self.fill(styles);
|
|
let stroke = self.stroke(styles).map(|s| s.map(Stroke::unwrap_or_default));
|
|
|
|
// Clip the contents
|
|
if self.clip(styles) {
|
|
let outset = self.outset(styles).relative_to(frame.size());
|
|
let size = frame.size() + outset.sum_by_axis();
|
|
let radius = self.radius(styles);
|
|
frame.clip(path_rect(size, radius, &stroke));
|
|
}
|
|
|
|
// Add fill and/or stroke.
|
|
if fill.is_some() || stroke.iter().any(Option::is_some) {
|
|
let outset = self.outset(styles);
|
|
let radius = self.radius(styles);
|
|
frame.fill_and_stroke(fill, stroke, outset, radius, self.span());
|
|
}
|
|
|
|
// Apply metadata.
|
|
frame.meta(styles, false);
|
|
frame.set_kind(FrameKind::Hard);
|
|
|
|
Ok(Fragment::frame(frame))
|
|
}
|
|
}
|
|
|
|
/// A block-level container.
|
|
///
|
|
/// Such a container can be used to separate content, size it, and give it a
|
|
/// background or border.
|
|
///
|
|
/// # Examples
|
|
/// With a block, you can give a background to content while still allowing it
|
|
/// to break across multiple pages.
|
|
/// ```example
|
|
/// #set page(height: 100pt)
|
|
/// #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.body
|
|
/// = Blockless
|
|
/// More text.
|
|
///
|
|
/// #show heading: it => block(it.body)
|
|
/// = Blocky
|
|
/// More text.
|
|
/// ```
|
|
#[elem(Layout)]
|
|
pub struct BlockElem {
|
|
/// The block's width.
|
|
///
|
|
/// ```example
|
|
/// #set align(center)
|
|
/// #block(
|
|
/// width: 60%,
|
|
/// inset: 8pt,
|
|
/// fill: silver,
|
|
/// lorem(10),
|
|
/// )
|
|
/// ```
|
|
pub width: Smart<Rel<Length>>,
|
|
|
|
/// The block's height. When the height is larger than the remaining space
|
|
/// on a page and [`breakable`]($block.breakable) is `{true}`, the
|
|
/// block will continue on the next page with the remaining height.
|
|
///
|
|
/// ```example
|
|
/// #set page(height: 80pt)
|
|
/// #set align(center)
|
|
/// #block(
|
|
/// width: 80%,
|
|
/// height: 150%,
|
|
/// fill: aqua,
|
|
/// )
|
|
/// ```
|
|
pub height: Smart<Rel<Length>>,
|
|
|
|
/// Whether the block can be broken and continue on the next page.
|
|
///
|
|
/// ```example
|
|
/// #set page(height: 80pt)
|
|
/// The following block will
|
|
/// jump to its own page.
|
|
/// #block(
|
|
/// breakable: false,
|
|
/// lorem(15),
|
|
/// )
|
|
/// ```
|
|
#[default(true)]
|
|
pub breakable: bool,
|
|
|
|
/// The block's background color. See the
|
|
/// [rectangle's documentation]($rect.fill) for more details.
|
|
pub fill: Option<Paint>,
|
|
|
|
/// The block's border color. See the
|
|
/// [rectangle's documentation]($rect.stroke) for more details.
|
|
#[resolve]
|
|
#[fold]
|
|
pub stroke: Sides<Option<Option<Stroke>>>,
|
|
|
|
/// How much to round the block's corners. See the
|
|
/// [rectangle's documentation]($rect.radius) for more details.
|
|
#[resolve]
|
|
#[fold]
|
|
pub radius: Corners<Option<Rel<Length>>>,
|
|
|
|
/// How much to pad the block's content. See the
|
|
/// [box's documentation]($box.inset) for more details.
|
|
#[resolve]
|
|
#[fold]
|
|
pub inset: Sides<Option<Rel<Length>>>,
|
|
|
|
/// How much to expand the block's size without affecting the layout. See
|
|
/// the [box's documentation]($box.outset) for more details.
|
|
#[resolve]
|
|
#[fold]
|
|
pub outset: Sides<Option<Rel<Length>>>,
|
|
|
|
/// The spacing around this block. This is shorthand to set `above` and
|
|
/// `below` to the same value.
|
|
///
|
|
/// ```example
|
|
/// #set align(center)
|
|
/// #show math.equation: set block(above: 8pt, below: 16pt)
|
|
///
|
|
/// This sum of $x$ and $y$:
|
|
/// $ x + y = z $
|
|
/// A second paragraph.
|
|
/// ```
|
|
#[external]
|
|
#[default(Em::new(1.2).into())]
|
|
pub spacing: Spacing,
|
|
|
|
/// The spacing between this block and its predecessor. Takes precedence
|
|
/// over `spacing`. Can be used in combination with a show rule to adjust
|
|
/// the spacing around arbitrary block-level elements.
|
|
#[external]
|
|
#[default(Em::new(1.2).into())]
|
|
pub above: Spacing,
|
|
#[internal]
|
|
#[parse(
|
|
let spacing = args.named("spacing")?;
|
|
args.named("above")?
|
|
.map(VElem::block_around)
|
|
.or_else(|| spacing.map(VElem::block_spacing))
|
|
)]
|
|
#[default(VElem::block_spacing(Em::new(1.2).into()))]
|
|
pub above: VElem,
|
|
|
|
/// The spacing between this block and its successor. Takes precedence
|
|
/// over `spacing`.
|
|
#[external]
|
|
#[default(Em::new(1.2).into())]
|
|
pub below: Spacing,
|
|
#[internal]
|
|
#[parse(
|
|
args.named("below")?
|
|
.map(VElem::block_around)
|
|
.or_else(|| spacing.map(VElem::block_spacing))
|
|
)]
|
|
#[default(VElem::block_spacing(Em::new(1.2).into()))]
|
|
pub below: VElem,
|
|
|
|
/// Whether to clip the content inside the block.
|
|
#[default(false)]
|
|
pub clip: bool,
|
|
|
|
/// The contents of the block.
|
|
#[positional]
|
|
pub body: Option<Content>,
|
|
|
|
/// Whether this block must stick to the following one.
|
|
///
|
|
/// Use this to prevent page breaks between e.g. a heading and its body.
|
|
#[internal]
|
|
#[default(false)]
|
|
pub sticky: bool,
|
|
}
|
|
|
|
impl Layout for BlockElem {
|
|
#[tracing::instrument(name = "BlockElem::layout", skip_all)]
|
|
fn layout(
|
|
&self,
|
|
vt: &mut Vt,
|
|
styles: StyleChain,
|
|
regions: Regions,
|
|
) -> SourceResult<Fragment> {
|
|
// Apply inset.
|
|
let mut body = self.body(styles).unwrap_or_default();
|
|
let inset = self.inset(styles);
|
|
if inset.iter().any(|v| !v.is_zero()) {
|
|
body = body.clone().padded(inset.map(|side| side.map(Length::from)));
|
|
}
|
|
|
|
// Resolve the sizing to a concrete size.
|
|
let sizing = Axes::new(self.width(styles), self.height(styles));
|
|
let mut expand = sizing.as_ref().map(Smart::is_custom);
|
|
let mut size = sizing
|
|
.resolve(styles)
|
|
.zip_map(regions.base(), |s, b| s.map(|v| v.relative_to(b)))
|
|
.unwrap_or(regions.base());
|
|
|
|
// Layout the child.
|
|
let mut frames = if self.breakable(styles) {
|
|
// Measure to ensure frames for all regions have the same width.
|
|
if sizing.x == Smart::Auto {
|
|
let pod = Regions::one(size, Axes::splat(false));
|
|
let frame = body.measure(vt, styles, pod)?.into_frame();
|
|
size.x = frame.width();
|
|
expand.x = true;
|
|
}
|
|
|
|
let mut pod = regions;
|
|
pod.size.x = size.x;
|
|
pod.expand = expand;
|
|
|
|
if expand.y {
|
|
pod.full = size.y;
|
|
}
|
|
|
|
// Generate backlog for fixed height.
|
|
let mut heights = vec![];
|
|
if sizing.y.is_custom() {
|
|
let mut remaining = size.y;
|
|
for region in regions.iter() {
|
|
let limited = region.y.min(remaining);
|
|
heights.push(limited);
|
|
remaining -= limited;
|
|
if Abs::zero().fits(remaining) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if let Some(last) = heights.last_mut() {
|
|
*last += remaining;
|
|
}
|
|
|
|
pod.size.y = heights[0];
|
|
pod.backlog = &heights[1..];
|
|
pod.last = None;
|
|
}
|
|
|
|
let mut frames = body.layout(vt, styles, pod)?.into_frames();
|
|
for (frame, &height) in frames.iter_mut().zip(&heights) {
|
|
*frame.size_mut() =
|
|
expand.select(Size::new(size.x, height), frame.size());
|
|
}
|
|
frames
|
|
} else {
|
|
let pod = Regions::one(size, expand);
|
|
let mut frames = body.layout(vt, styles, pod)?.into_frames();
|
|
*frames[0].size_mut() = expand.select(size, frames[0].size());
|
|
frames
|
|
};
|
|
|
|
// Prepare fill and stroke.
|
|
let fill = self.fill(styles);
|
|
let stroke = self.stroke(styles).map(|s| s.map(Stroke::unwrap_or_default));
|
|
|
|
// Clip the contents
|
|
if self.clip(styles) {
|
|
for frame in frames.iter_mut() {
|
|
let outset = self.outset(styles).relative_to(frame.size());
|
|
let size = frame.size() + outset.sum_by_axis();
|
|
let radius = self.radius(styles);
|
|
frame.clip(path_rect(size, radius, &stroke));
|
|
}
|
|
}
|
|
|
|
// Add fill and/or stroke.
|
|
if fill.is_some() || stroke.iter().any(Option::is_some) {
|
|
let mut skip = false;
|
|
if let [first, rest @ ..] = frames.as_slice() {
|
|
skip = first.is_empty() && rest.iter().any(|frame| !frame.is_empty());
|
|
}
|
|
|
|
let outset = self.outset(styles);
|
|
let radius = self.radius(styles);
|
|
for frame in frames.iter_mut().skip(skip as usize) {
|
|
frame.fill_and_stroke(
|
|
fill.clone(),
|
|
stroke.clone(),
|
|
outset,
|
|
radius,
|
|
self.span(),
|
|
);
|
|
}
|
|
}
|
|
|
|
// Apply metadata.
|
|
for frame in &mut frames {
|
|
frame.set_kind(FrameKind::Hard);
|
|
frame.meta(styles, false);
|
|
}
|
|
|
|
Ok(Fragment::frames(frames))
|
|
}
|
|
}
|
|
|
|
/// Defines how to size a grid cell along an axis.
|
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
|
pub enum Sizing {
|
|
/// A track that fits its cell's contents.
|
|
Auto,
|
|
/// A track size specified in absolute terms and relative to the parent's
|
|
/// size.
|
|
Rel(Rel<Length>),
|
|
/// A track size specified as a fraction of the remaining free space in the
|
|
/// parent.
|
|
Fr(Fr),
|
|
}
|
|
|
|
impl Sizing {
|
|
/// Whether this is fractional sizing.
|
|
pub fn is_fractional(self) -> bool {
|
|
matches!(self, Self::Fr(_))
|
|
}
|
|
}
|
|
|
|
impl Default for Sizing {
|
|
fn default() -> Self {
|
|
Self::Auto
|
|
}
|
|
}
|
|
|
|
impl<T: Into<Spacing>> From<T> for Sizing {
|
|
fn from(spacing: T) -> Self {
|
|
match spacing.into() {
|
|
Spacing::Rel(rel) => Self::Rel(rel),
|
|
Spacing::Fr(fr) => Self::Fr(fr),
|
|
}
|
|
}
|
|
}
|
|
|
|
cast! {
|
|
Sizing,
|
|
self => match self {
|
|
Self::Auto => Value::Auto,
|
|
Self::Rel(rel) => rel.into_value(),
|
|
Self::Fr(fr) => fr.into_value(),
|
|
},
|
|
_: AutoValue => Self::Auto,
|
|
v: Rel<Length> => Self::Rel(v),
|
|
v: Fr => Self::Fr(v),
|
|
}
|