419 lines
14 KiB
Rust

use std::cell::LazyCell;
use smallvec::SmallVec;
use typst_library::diag::SourceResult;
use typst_library::engine::Engine;
use typst_library::foundations::{Packed, Resolve, StyleChain};
use typst_library::introspection::Locator;
use typst_library::layout::{
Abs, Axes, BlockBody, BlockElem, Fragment, Frame, FrameKind, Region, Regions, Rel,
Sides, Size, Sizing,
};
use typst_library::visualize::Stroke;
use typst_utils::Numeric;
use crate::shapes::{clip_rect, fill_and_stroke};
/// Lay this out as an unbreakable block.
#[typst_macros::time(name = "block", span = elem.span())]
pub fn layout_single_block(
elem: &Packed<BlockElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame> {
// Fetch sizing properties.
let width = elem.width(styles);
let height = elem.height(styles);
let inset = elem.inset(styles).unwrap_or_default();
// Build the pod regions.
let pod = unbreakable_pod(&width.into(), &height, &inset, styles, region.size);
// Layout the body.
let body = elem.body(styles);
let mut frame = match body {
// If we have no body, just create one frame. Its size will be
// adjusted below.
None => Frame::hard(Size::zero()),
// If we have content as our body, just layout it.
Some(BlockBody::Content(body)) => {
crate::layout_frame(engine, body, locator.relayout(), styles, pod)?
}
// If we have a child that wants to layout with just access to the
// base region, give it that.
Some(BlockBody::SingleLayouter(callback)) => {
callback.call(engine, locator, styles, pod)?
}
// If we have a child that wants to layout with full region access,
// we layout it.
Some(BlockBody::MultiLayouter(callback)) => {
let expand = (pod.expand | region.expand) & pod.size.map(Abs::is_finite);
let pod = Region { expand, ..pod };
callback.call(engine, locator, styles, pod.into())?.into_frame()
}
};
// Explicit blocks are boundaries for gradient relativeness.
if matches!(body, None | Some(BlockBody::Content(_))) {
frame.set_kind(FrameKind::Hard);
}
// Enforce a correct frame size on the expanded axes. Do this before
// applying the inset, since the pod shrunk.
frame.set_size(pod.expand.select(pod.size, frame.size()));
// Apply the inset.
if !inset.is_zero() {
crate::pad::grow(&mut frame, &inset);
}
// Prepare fill and stroke.
let fill = elem.fill(styles);
let stroke = elem
.stroke(styles)
.unwrap_or_default()
.map(|s| s.map(Stroke::unwrap_or_default));
// Only fetch these if necessary (for clipping or filling/stroking).
let outset = LazyCell::new(|| elem.outset(styles).unwrap_or_default());
let radius = LazyCell::new(|| elem.radius(styles).unwrap_or_default());
// Clip the contents, if requested.
if elem.clip(styles) {
frame.clip(clip_rect(frame.size(), &radius, &stroke, &outset));
}
// Add fill and/or stroke.
if fill.is_some() || stroke.iter().any(Option::is_some) {
fill_and_stroke(&mut frame, fill, &stroke, &outset, &radius, elem.span());
}
// Assign label to each frame in the fragment.
if let Some(label) = elem.label() {
frame.label(label);
}
Ok(frame)
}
/// Lay this out as a breakable block.
#[typst_macros::time(name = "block", span = elem.span())]
pub fn layout_multi_block(
elem: &Packed<BlockElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
// Fetch sizing properties.
let width = elem.width(styles);
let height = elem.height(styles);
let inset = elem.inset(styles).unwrap_or_default();
// Allocate a small vector for backlogs.
let mut buf = SmallVec::<[Abs; 2]>::new();
// Build the pod regions.
let pod = breakable_pod(&width.into(), &height, &inset, styles, regions, &mut buf);
// Layout the body.
let body = elem.body(styles);
let mut fragment = match body {
// If we have no body, just create one frame plus one per backlog
// region. We create them zero-sized; if necessary, their size will
// be adjusted below.
None => {
let mut frames = vec![];
frames.push(Frame::hard(Size::zero()));
if pod.expand.y {
let mut iter = pod;
while !iter.backlog.is_empty() {
frames.push(Frame::hard(Size::zero()));
iter.next();
}
}
Fragment::frames(frames)
}
// If we have content as our body, just layout it.
Some(BlockBody::Content(body)) => {
let mut fragment =
crate::layout_fragment(engine, body, locator.relayout(), styles, pod)?;
// If the body is automatically sized and produced more than one
// fragment, ensure that the width was consistent across all
// regions. If it wasn't, we need to relayout with expansion.
if !pod.expand.x
&& fragment
.as_slice()
.windows(2)
.any(|w| !w[0].width().approx_eq(w[1].width()))
{
let max_width =
fragment.iter().map(|frame| frame.width()).max().unwrap_or_default();
let pod = Regions {
size: Size::new(max_width, pod.size.y),
expand: Axes::new(true, pod.expand.y),
..pod
};
fragment = crate::layout_fragment(engine, body, locator, styles, pod)?;
}
fragment
}
// If we have a child that wants to layout with just access to the
// base region, give it that.
Some(BlockBody::SingleLayouter(callback)) => {
let pod = Region::new(pod.base(), pod.expand);
callback.call(engine, locator, styles, pod).map(Fragment::frame)?
}
// If we have a child that wants to layout with full region access,
// we layout it.
//
// For auto-sized multi-layouters, we propagate the outer expansion
// so that they can decide for themselves. We also ensure again to
// only expand if the size is finite.
Some(BlockBody::MultiLayouter(callback)) => {
let expand = (pod.expand | regions.expand) & pod.size.map(Abs::is_finite);
let pod = Regions { expand, ..pod };
callback.call(engine, locator, styles, pod)?
}
};
// Prepare fill and stroke.
let fill = elem.fill(styles);
let stroke = elem
.stroke(styles)
.unwrap_or_default()
.map(|s| s.map(Stroke::unwrap_or_default));
// Only fetch these if necessary (for clipping or filling/stroking).
let outset = LazyCell::new(|| elem.outset(styles).unwrap_or_default());
let radius = LazyCell::new(|| elem.radius(styles).unwrap_or_default());
// Fetch/compute these outside of the loop.
let clip = elem.clip(styles);
let has_fill_or_stroke = fill.is_some() || stroke.iter().any(Option::is_some);
let has_inset = !inset.is_zero();
let is_explicit = matches!(body, None | Some(BlockBody::Content(_)));
// Skip filling/stroking the first frame if it is empty and a non-empty
// one follows.
let mut skip_first = false;
if let [first, rest @ ..] = fragment.as_slice() {
skip_first = has_fill_or_stroke
&& first.is_empty()
&& rest.iter().any(|frame| !frame.is_empty());
}
// Post-process to apply insets, clipping, fills, and strokes.
for (i, (frame, region)) in fragment.iter_mut().zip(pod.iter()).enumerate() {
// Explicit blocks are boundaries for gradient relativeness.
if is_explicit {
frame.set_kind(FrameKind::Hard);
}
// Enforce a correct frame size on the expanded axes. Do this before
// applying the inset, since the pod shrunk.
frame.set_size(pod.expand.select(region, frame.size()));
// Apply the inset.
if has_inset {
crate::pad::grow(frame, &inset);
}
// Clip the contents, if requested.
if clip {
frame.clip(clip_rect(frame.size(), &radius, &stroke, &outset));
}
// Add fill and/or stroke.
if has_fill_or_stroke && (i > 0 || !skip_first) {
fill_and_stroke(frame, fill.clone(), &stroke, &outset, &radius, elem.span());
}
}
// Assign label to each frame in the fragment.
if let Some(label) = elem.label() {
for frame in fragment.iter_mut() {
frame.label(label);
}
}
Ok(fragment)
}
/// Builds the pod region for an unbreakable sized container.
pub(crate) fn unbreakable_pod(
width: &Sizing,
height: &Sizing,
inset: &Sides<Rel<Abs>>,
styles: StyleChain,
base: Size,
) -> Region {
// Resolve the size.
let mut size = Size::new(
match width {
// - For auto, the whole region is available.
// - Fr is handled outside and already factored into the `region`,
// so we can treat it equivalently to 100%.
Sizing::Auto | Sizing::Fr(_) => base.x,
// Resolve the relative sizing.
Sizing::Rel(rel) => rel.resolve(styles).relative_to(base.x),
},
match height {
Sizing::Auto | Sizing::Fr(_) => base.y,
Sizing::Rel(rel) => rel.resolve(styles).relative_to(base.y),
},
);
// Take the inset, if any, into account.
if !inset.is_zero() {
size = crate::pad::shrink(size, inset);
}
// If the child is manually, the size is forced and we should enable
// expansion.
let expand = Axes::new(
*width != Sizing::Auto && size.x.is_finite(),
*height != Sizing::Auto && size.y.is_finite(),
);
Region::new(size, expand)
}
/// Builds the pod regions for a breakable sized container.
fn breakable_pod<'a>(
width: &Sizing,
height: &Sizing,
inset: &Sides<Rel<Abs>>,
styles: StyleChain,
regions: Regions,
buf: &'a mut SmallVec<[Abs; 2]>,
) -> Regions<'a> {
let base = regions.base();
// The vertical region sizes we're about to build.
let first;
let full;
let backlog: &mut [Abs];
let last;
// If the block has a fixed height, things are very different, so we
// handle that case completely separately.
match height {
Sizing::Auto | Sizing::Fr(_) => {
// If the block is automatically sized, we can just inherit the
// regions.
first = regions.size.y;
full = regions.full;
buf.extend_from_slice(regions.backlog);
backlog = buf;
last = regions.last;
}
Sizing::Rel(rel) => {
// Resolve the sizing to a concrete size.
let resolved = rel.resolve(styles).relative_to(base.y);
// Since we're manually sized, the resolved size is the base height.
full = resolved;
// Distribute the fixed height across a start region and a backlog.
(first, backlog) = distribute(resolved, regions, buf);
// If the height is manually sized, we don't want a final repeatable
// region.
last = None;
}
};
// Resolve the horizontal sizing to a concrete width and combine
// `width` and `first` into `size`.
let mut size = Size::new(
match width {
Sizing::Auto | Sizing::Fr(_) => regions.size.x,
Sizing::Rel(rel) => rel.resolve(styles).relative_to(base.x),
},
first,
);
// Take the inset, if any, into account, applying it to the
// individual region components.
let (mut full, mut last) = (full, last);
if !inset.is_zero() {
crate::pad::shrink_multiple(&mut size, &mut full, backlog, &mut last, inset);
}
// If the child is manually, the size is forced and we should enable
// expansion.
let expand = Axes::new(
*width != Sizing::Auto && size.x.is_finite(),
*height != Sizing::Auto && size.y.is_finite(),
);
Regions { size, full, backlog, last, expand }
}
/// Distribute a fixed height spread over existing regions into a new first
/// height and a new backlog.
///
/// Note that, if the given height fits within the first region, no backlog is
/// generated and the first region's height shrinks to fit exactly the given
/// height. In particular, negative and zero heights always fit in any region,
/// so such heights are always directly returned as the new first region
/// height.
fn distribute<'a>(
height: Abs,
mut regions: Regions,
buf: &'a mut SmallVec<[Abs; 2]>,
) -> (Abs, &'a mut [Abs]) {
// Build new region heights from old regions.
let mut remaining = height;
// Negative and zero heights always fit, so just keep them.
// No backlog is generated.
if remaining <= Abs::zero() {
buf.push(remaining);
return (buf[0], &mut buf[1..]);
}
loop {
// This clamp is safe (min <= max), as 'remaining' won't be negative
// due to the initial check above (on the first iteration) and due to
// stopping on 'remaining.approx_empty()' below (for the second
// iteration onwards).
let limited = regions.size.y.clamp(Abs::zero(), remaining);
buf.push(limited);
remaining -= limited;
if remaining.approx_empty()
|| !regions.may_break()
|| (!regions.may_progress() && limited.approx_empty())
{
break;
}
regions.next();
}
// If there is still something remaining, apply it to the
// last region (it will overflow, but there's nothing else
// we can do).
if !remaining.approx_empty() {
if let Some(last) = buf.last_mut() {
*last += remaining;
}
}
// Distribute the heights to the first region and the
// backlog. There is no last region, since the height is
// fixed.
(buf[0], &mut buf[1..])
}