Show block-level elements as blocks (#4310)

This commit is contained in:
Laurenz 2024-06-03 10:12:52 +02:00 committed by GitHub
parent 23746ee189
commit 755dd4112d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
59 changed files with 2161 additions and 1642 deletions

View File

@ -2,10 +2,8 @@ use std::num::{NonZeroI64, NonZeroIsize, NonZeroU64, NonZeroUsize, ParseIntError
use ecow::{eco_format, EcoString};
use crate::{
diag::StrResult,
foundations::{cast, func, repr, scope, ty, Repr, Str, Value},
};
use crate::diag::StrResult;
use crate::foundations::{cast, func, repr, scope, ty, Repr, Str, Value};
/// A whole number.
///

View File

@ -25,9 +25,9 @@ pub use self::state::*;
use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
use crate::foundations::NativeElement;
use crate::foundations::{
category, elem, Args, Category, Construct, Content, Packed, Scope, Unlabellable,
category, elem, Args, Category, Construct, Content, NativeElement, Packed, Scope,
Unlabellable,
};
use crate::realize::{Behave, Behaviour};

View File

@ -7,6 +7,9 @@ use ecow::EcoString;
use crate::foundations::{cast, repr, Fold, Repr, Value};
use crate::utils::{Numeric, Scalar};
/// The epsilon for approximate comparisons.
const EPS: f64 = 1e-6;
/// An absolute length.
#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Abs(Scalar);
@ -54,7 +57,7 @@ impl Abs {
/// Get the value of this absolute length in raw units.
pub const fn to_raw(self) -> f64 {
(self.0).get()
self.0.get()
}
/// Get the value of this absolute length in a unit.
@ -110,12 +113,17 @@ impl Abs {
/// Whether the other absolute length fits into this one (i.e. is smaller).
/// Allows for a bit of slack.
pub fn fits(self, other: Self) -> bool {
self.0 + 1e-6 >= other.0
self.0 + EPS >= other.0
}
/// Compares two absolute lengths for whether they are approximately equal.
pub fn approx_eq(self, other: Self) -> bool {
self == other || (self - other).to_raw().abs() < 1e-6
self == other || (self - other).to_raw().abs() < EPS
}
/// Whether the size is close to zero or negative.
pub fn approx_empty(self) -> bool {
self.to_raw() <= EPS
}
/// Returns a number that represent the sign of this length

View File

@ -49,10 +49,7 @@ pub struct AlignElem {
impl Show for Packed<AlignElem> {
#[typst_macros::time(name = "align", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
Ok(self
.body()
.clone()
.styled(AlignElem::set_alignment(self.alignment(styles))))
Ok(self.body().clone().aligned(self.alignment(styles)))
}
}

View File

@ -4,7 +4,7 @@ use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Deref, Not};
use crate::diag::bail;
use crate::foundations::{array, cast, Array, Resolve, Smart, StyleChain};
use crate::layout::{Abs, Dir, Length, Ratio, Rel};
use crate::layout::{Abs, Dir, Length, Ratio, Rel, Size};
use crate::utils::Get;
/// A container with a horizontal and vertical component.
@ -120,6 +120,16 @@ impl<T: Ord> Axes<T> {
}
}
impl Axes<Rel<Abs>> {
/// Evaluate the axes relative to the given `size`.
pub fn relative_to(&self, size: Size) -> Size {
Size {
x: self.x.relative_to(size.x),
y: self.y.relative_to(size.y),
}
}
}
impl<T> Get<Axis> for Axes<T> {
type Component = T;

View File

@ -2,10 +2,9 @@ use std::num::NonZeroUsize;
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{elem, Content, Packed, StyleChain};
use crate::foundations::{elem, Content, NativeElement, Packed, Show, StyleChain};
use crate::layout::{
Abs, Axes, Dir, Fragment, Frame, LayoutMultiple, Length, Point, Ratio, Regions, Rel,
Size,
Abs, Axes, BlockElem, Dir, Fragment, Frame, Length, Point, Ratio, Regions, Rel, Size,
};
use crate::realize::{Behave, Behaviour};
use crate::text::TextElem;
@ -42,7 +41,7 @@ use crate::utils::Numeric;
/// increasingly been used to solve a
/// variety of problems.
/// ```
#[elem(LayoutMultiple)]
#[elem(Show)]
pub struct ColumnsElem {
/// The number of columns.
#[positional]
@ -59,82 +58,86 @@ pub struct ColumnsElem {
pub body: Content,
}
impl LayoutMultiple for Packed<ColumnsElem> {
#[typst_macros::time(name = "columns", span = self.span())]
fn layout(
&self,
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
let body = self.body();
impl Show for Packed<ColumnsElem> {
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::multi_layouter(self.clone(), layout_columns)
.with_rootable(true)
.pack())
}
}
// Separating the infinite space into infinite columns does not make
// much sense.
if !regions.size.x.is_finite() {
return body.layout(engine, styles, regions);
}
/// Layout the columns.
#[typst_macros::time(span = elem.span())]
fn layout_columns(
elem: &Packed<ColumnsElem>,
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
let body = elem.body();
// Determine the width of the gutter and each column.
let columns = self.count(styles).get();
let gutter = self.gutter(styles).relative_to(regions.base().x);
let width = (regions.size.x - gutter * (columns - 1) as f64) / columns as f64;
// Separating the infinite space into infinite columns does not make
// much sense.
if !regions.size.x.is_finite() {
return body.layout(engine, styles, regions);
}
let backlog: Vec<_> = std::iter::once(&regions.size.y)
.chain(regions.backlog)
.flat_map(|&height| std::iter::repeat(height).take(columns))
.skip(1)
.collect();
// Determine the width of the gutter and each column.
let columns = elem.count(styles).get();
let gutter = elem.gutter(styles).relative_to(regions.base().x);
let width = (regions.size.x - gutter * (columns - 1) as f64) / columns as f64;
// Create the pod regions.
let pod = Regions {
size: Size::new(width, regions.size.y),
full: regions.full,
backlog: &backlog,
last: regions.last,
expand: Axes::new(true, regions.expand.y),
root: regions.root,
};
let backlog: Vec<_> = std::iter::once(&regions.size.y)
.chain(regions.backlog)
.flat_map(|&height| std::iter::repeat(height).take(columns))
.skip(1)
.collect();
// Layout the children.
let mut frames = body.layout(engine, styles, pod)?.into_iter();
let mut finished = vec![];
// Create the pod regions.
let pod = Regions {
size: Size::new(width, regions.size.y),
full: regions.full,
backlog: &backlog,
last: regions.last,
expand: Axes::new(true, regions.expand.y),
root: regions.root,
};
let dir = TextElem::dir_in(styles);
let total_regions = (frames.len() as f32 / columns as f32).ceil() as usize;
// Layout the children.
let mut frames = body.layout(engine, styles, pod)?.into_iter();
let mut finished = vec![];
// Stitch together the columns for each region.
for region in regions.iter().take(total_regions) {
// The height should be the parent height if we should expand.
// Otherwise its the maximum column height for the frame. In that
// case, the frame is first created with zero height and then
// resized.
let height = if regions.expand.y { region.y } else { Abs::zero() };
let mut output = Frame::hard(Size::new(regions.size.x, height));
let mut cursor = Abs::zero();
let dir = TextElem::dir_in(styles);
let total_regions = (frames.len() as f32 / columns as f32).ceil() as usize;
for _ in 0..columns {
let Some(frame) = frames.next() else { break };
if !regions.expand.y {
output.size_mut().y.set_max(frame.height());
}
// Stitch together the columns for each region.
for region in regions.iter().take(total_regions) {
// The height should be the parent height if we should expand.
// Otherwise its the maximum column height for the frame. In that
// case, the frame is first created with zero height and then
// resized.
let height = if regions.expand.y { region.y } else { Abs::zero() };
let mut output = Frame::hard(Size::new(regions.size.x, height));
let mut cursor = Abs::zero();
let width = frame.width();
let x = if dir == Dir::LTR {
cursor
} else {
regions.size.x - cursor - width
};
output.push_frame(Point::with_x(x), frame);
cursor += width + gutter;
for _ in 0..columns {
let Some(frame) = frames.next() else { break };
if !regions.expand.y {
output.size_mut().y.set_max(frame.height());
}
finished.push(output);
let width = frame.width();
let x =
if dir == Dir::LTR { cursor } else { regions.size.x - cursor - width };
output.push_frame(Point::with_x(x), frame);
cursor += width + gutter;
}
Ok(Fragment::frames(finished))
finished.push(output);
}
Ok(Fragment::frames(finished))
}
/// Forces a column break.

View File

@ -1,11 +1,15 @@
use crate::diag::SourceResult;
use once_cell::unsync::Lazy;
use smallvec::SmallVec;
use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
cast, elem, AutoValue, Content, Packed, Resolve, Smart, StyleChain, Value,
cast, elem, Args, AutoValue, Construct, Content, NativeElement, Packed, Resolve,
Smart, StyleChain, Value,
};
use crate::layout::{
Abs, Axes, Corners, Em, Fr, Fragment, Frame, FrameKind, LayoutMultiple, Length,
Ratio, Regions, Rel, Sides, Size, Spacing, VElem,
Abs, Axes, Corners, Em, Fr, Fragment, Frame, FrameKind, Length, Region, Regions, Rel,
Sides, Size, Spacing, VElem,
};
use crate::utils::Numeric;
use crate::visualize::{clip_rect, Paint, Stroke};
@ -106,47 +110,53 @@ pub struct BoxElem {
/// The contents of the box.
#[positional]
#[borrowed]
pub body: Option<Content>,
}
impl Packed<BoxElem> {
/// Layout this box as part of a paragraph.
#[typst_macros::time(name = "box", span = self.span())]
pub fn layout(
&self,
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
region: Size,
) -> SourceResult<Frame> {
let width = match self.width(styles) {
Sizing::Auto => Smart::Auto,
Sizing::Rel(rel) => Smart::Custom(rel),
Sizing::Fr(_) => Smart::Custom(Ratio::one().into()),
// Fetch sizing properties.
let width = self.width(styles);
let height = self.height(styles);
let inset = self.inset(styles).unwrap_or_default();
// Build the pod region.
let pod = Self::pod(&width, &height, &inset, styles, region);
// Layout the body.
let mut frame = match self.body(styles) {
// If we have no body, just create an empty frame. If necessary,
// its size will be adjusted below.
None => Frame::hard(Size::zero()),
// If we have a child, layout it into the body. Boxes are boundaries
// for gradient relativeness, so we set the `FrameKind` to `Hard`.
Some(body) => body
.layout(engine, styles, pod.into_regions())?
.into_frame()
.with_kind(FrameKind::Hard),
};
// 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());
// 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 inset.
let mut body = self.body(styles).unwrap_or_default();
let inset = self.inset(styles).unwrap_or_default();
if inset.iter().any(|v| !v.is_zero()) {
body = body.padded(inset.map(|side| side.map(Length::from)));
// Apply the inset.
if !inset.is_zero() {
crate::layout::grow(&mut frame, &inset);
}
// 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(engine, styles, pod)?.into_frame();
// Enforce correct size.
*frame.size_mut() = expand.select(size, frame.size());
// Apply baseline shift.
// Apply baseline shift. Do this after setting the size and applying the
// inset, so that a relative shift is resolved relative to the final
// height.
let shift = self.baseline(styles).relative_to(frame.height());
if !shift.is_zero() {
frame.set_baseline(frame.baseline() - shift);
@ -159,27 +169,115 @@ impl Packed<BoxElem> {
.unwrap_or_default()
.map(|s| s.map(Stroke::unwrap_or_default));
// Clip the contents
// Only fetch these if necessary (for clipping or filling/stroking).
let outset = Lazy::new(|| self.outset(styles).unwrap_or_default());
let radius = Lazy::new(|| self.radius(styles).unwrap_or_default());
// Clip the contents, if requested.
if self.clip(styles) {
let outset =
self.outset(styles).unwrap_or_default().relative_to(frame.size());
let size = frame.size() + outset.sum_by_axis();
let radius = self.radius(styles).unwrap_or_default();
frame.clip(clip_rect(size, radius, &stroke));
let size = frame.size() + outset.relative_to(frame.size()).sum_by_axis();
frame.clip(clip_rect(size, &radius, &stroke));
}
// Add fill and/or stroke.
if fill.is_some() || stroke.iter().any(Option::is_some) {
let outset = self.outset(styles).unwrap_or_default();
let radius = self.radius(styles).unwrap_or_default();
frame.fill_and_stroke(fill, stroke, outset, radius, self.span());
frame.fill_and_stroke(fill, &stroke, &outset, &radius, self.span());
}
// Apply metadata.
frame.set_kind(FrameKind::Hard);
Ok(frame)
}
/// Builds the pod region for box layout.
fn pod(
width: &Sizing,
height: &Smart<Rel>,
inset: &Sides<Rel<Abs>>,
styles: StyleChain,
region: Size,
) -> Region {
// Resolve the size.
let mut size = Size::new(
match width {
// For auto, the whole region is available.
Sizing::Auto => region.x,
// Resolve the relative sizing.
Sizing::Rel(rel) => rel.resolve(styles).relative_to(region.x),
// Fr is handled outside and already factored into the `region`,
// so we can treat it equivalently to 100%.
Sizing::Fr(_) => region.x,
},
match height {
// See above. Note that fr is not supported on this axis.
Smart::Auto => region.y,
Smart::Custom(rel) => rel.resolve(styles).relative_to(region.y),
},
);
// Take the inset, if any, into account.
if !inset.is_zero() {
size = crate::layout::shrink(size, inset);
}
// If the child is not auto-sized, the size is forced and we should
// enable expansion.
let expand = Axes::new(*width != Sizing::Auto, *height != Smart::Auto);
Region::new(size, expand)
}
}
/// An inline-level container that can produce arbitrary items that can break
/// across lines.
#[elem(Construct)]
pub struct InlineElem {
/// A callback that is invoked with the regions to produce arbitrary
/// inline items.
#[required]
#[internal]
body: callbacks::InlineCallback,
}
impl Construct for InlineElem {
fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> {
bail!(args.span, "cannot be constructed manually");
}
}
impl InlineElem {
/// Create an inline-level item with a custom layouter.
#[allow(clippy::type_complexity)]
pub fn layouter<T: NativeElement>(
captured: Packed<T>,
callback: fn(
content: &Packed<T>,
engine: &mut Engine,
styles: StyleChain,
region: Size,
) -> SourceResult<Vec<InlineItem>>,
) -> Self {
Self::new(callbacks::InlineCallback::new(captured, callback))
}
}
impl Packed<InlineElem> {
/// Layout the element.
pub fn layout(
&self,
engine: &mut Engine,
styles: StyleChain,
region: Size,
) -> SourceResult<Vec<InlineItem>> {
self.body().call(engine, styles, region)
}
}
/// Layouted items suitable for placing in a paragraph.
#[derive(Debug, Clone)]
pub enum InlineItem {
/// Absolute spacing between other items, and whether it is weak.
Space(Abs, bool),
/// Layouted inline-level content.
Frame(Frame),
}
/// A block-level container.
@ -211,7 +309,7 @@ impl Packed<BoxElem> {
/// = Blocky
/// More text.
/// ```
#[elem(LayoutMultiple)]
#[elem]
pub struct BlockElem {
/// The block's width.
///
@ -332,93 +430,155 @@ pub struct BlockElem {
#[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)]
#[ghost]
#[parse(None)]
pub sticky: bool,
/// Whether this block can host footnotes.
#[internal]
#[default(false)]
#[parse(None)]
pub rootable: bool,
/// The contents of the block.
#[positional]
#[borrowed]
pub body: Option<BlockChild>,
}
impl LayoutMultiple for Packed<BlockElem> {
impl BlockElem {
/// Create a block with a custom single-region layouter.
///
/// Such a block must have `breakable: false` (which is set by this
/// constructor).
pub fn single_layouter<T: NativeElement>(
captured: Packed<T>,
f: fn(
content: &Packed<T>,
engine: &mut Engine,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame>,
) -> Self {
Self::new()
.with_breakable(false)
.with_body(Some(BlockChild::SingleLayouter(
callbacks::BlockSingleCallback::new(captured, f),
)))
}
/// Create a block with a custom multi-region layouter.
pub fn multi_layouter<T: NativeElement>(
captured: Packed<T>,
f: fn(
content: &Packed<T>,
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment>,
) -> Self {
Self::new().with_body(Some(BlockChild::MultiLayouter(
callbacks::BlockMultiCallback::new(captured, f),
)))
}
}
impl Packed<BlockElem> {
/// Layout this block as part of a flow.
#[typst_macros::time(name = "block", span = self.span())]
fn layout(
pub fn layout(
&self,
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
// Apply inset.
let mut body = self.body(styles).unwrap_or_default();
// Fetch sizing properties.
let width = self.width(styles);
let height = self.height(styles);
let inset = self.inset(styles).unwrap_or_default();
if inset.iter().any(|v| !v.is_zero()) {
body = body.clone().padded(inset.map(|side| side.map(Length::from)));
}
let breakable = self.breakable(styles);
// 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());
// Allocate a small vector for backlogs.
let mut buf = SmallVec::<[Abs; 2]>::new();
// 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(engine, styles, pod)?.into_frame();
size.x = frame.width();
expand.x = true;
}
// Build the pod regions.
let pod =
Self::pod(&width, &height, &inset, breakable, styles, regions, &mut buf);
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;
// Layout the body.
let body = self.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 let Some(last) = heights.last_mut() {
*last += remaining;
// If we have content as our body, just layout it.
Some(BlockChild::Content(body)) => {
let mut fragment = body.measure(engine, 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 = body.layout(engine, styles, pod)?;
} else {
// Apply the side effect to turn the `measure` into a
// `layout`.
engine.locator.visit_frames(&fragment);
}
pod.size.y = heights[0];
pod.backlog = &heights[1..];
pod.last = None;
fragment
}
let mut frames = body.layout(engine, 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());
// If we have a child that wants to layout with just access to the
// base region, give it that.
Some(BlockChild::SingleLayouter(callback)) => {
let pod = Region::new(pod.base(), pod.expand);
callback.call(engine, 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(BlockChild::MultiLayouter(callback)) => {
let expand = (pod.expand | regions.expand) & pod.size.map(Abs::is_finite);
let pod = Regions { expand, ..pod };
callback.call(engine, styles, pod)?
}
frames
} else {
let pod = Regions::one(size, expand);
let mut frames = body.layout(engine, styles, pod)?.into_frames();
*frames[0].size_mut() = expand.select(size, frames[0].size());
frames
};
// Prepare fill and stroke.
@ -428,60 +588,219 @@ impl LayoutMultiple for Packed<BlockElem> {
.unwrap_or_default()
.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).unwrap_or_default().relative_to(frame.size());
let size = frame.size() + outset.sum_by_axis();
let radius = self.radius(styles).unwrap_or_default();
frame.clip(clip_rect(size, radius, &stroke));
}
// Only fetch these if necessary (for clipping or filling/stroking).
let outset = Lazy::new(|| self.outset(styles).unwrap_or_default());
let radius = Lazy::new(|| self.radius(styles).unwrap_or_default());
// Fetch/compute these outside of the loop.
let clip = self.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(BlockChild::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());
}
// 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());
// 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);
}
let outset = self.outset(styles).unwrap_or_default();
let radius = self.radius(styles).unwrap_or_default();
for frame in frames.iter_mut().skip(skip as usize) {
// 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::layout::grow(frame, &inset);
}
// Clip the contents, if requested.
if clip {
let size = frame.size() + outset.relative_to(frame.size()).sum_by_axis();
frame.clip(clip_rect(size, &radius, &stroke));
}
// Add fill and/or stroke.
if has_fill_or_stroke && (i > 0 || !skip_first) {
frame.fill_and_stroke(
fill.clone(),
stroke.clone(),
outset,
radius,
&stroke,
&outset,
&radius,
self.span(),
);
}
}
// Apply metadata.
for frame in &mut frames {
frame.set_kind(FrameKind::Hard);
Ok(fragment)
}
/// Builds the pod regions for block layout.
///
/// If `breakable` is `false`, this will only ever return a single region.
fn pod<'a>(
width: &Smart<Rel>,
height: &Smart<Rel>,
inset: &Sides<Rel<Abs>>,
breakable: bool,
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 {
Smart::Auto => {
if breakable {
// If the block automatically sized and breakable, we can
// just inherit the regions.
first = regions.size.y;
buf.extend_from_slice(regions.backlog);
backlog = buf;
last = regions.last;
} else {
// If the block is automatically sized, but not breakable,
// we provide the full base height. It doesn't really make
// sense to provide just the remaining height to an
// unbreakable block.
first = regions.full;
backlog = &mut [];
last = None;
}
// Since we're automatically sized, we inherit the base size.
full = regions.full;
}
Smart::Custom(rel) => {
// Resolve the sizing to a concrete size.
let resolved = rel.resolve(styles).relative_to(base.y);
if breakable {
// If the block is fixed-height and breakable, distribute
// the fixed height across a start region and a backlog.
(first, backlog) = distribute(resolved, regions, buf);
} else {
// If the block is fixed-height, but not breakable, the
// fixed height is all in the first region, and we have no
// backlog.
first = resolved;
backlog = &mut [];
}
// Since we're manually sized, the resolved size is also the
// base height.
full = resolved;
// 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 {
Smart::Auto => regions.size.x,
Smart::Custom(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::layout::shrink_multiple(
&mut size, &mut full, backlog, &mut last, inset,
);
}
Ok(Fragment::frames(frames))
// If the child is manually sized along an axis (i.e. not `auto`), then
// it should expand along that axis. We also ensure that we only expand
// if the size is finite because it just doesn't make sense to expand
// into infinite regions.
let expand = Axes::new(*width != Smart::Auto, *height != Smart::Auto)
& size.map(Abs::is_finite);
Regions {
size,
full,
backlog,
last,
expand,
// This will only ever be set by the flow if the block is
// `rootable`. It is important that we propagate this, so that
// columns can hold footnotes.
root: regions.root,
}
}
}
/// Defines how to size a grid cell along an axis.
/// The contents of a block.
#[derive(Debug, Clone, PartialEq, Hash)]
pub enum BlockChild {
/// The block contains normal content.
Content(Content),
/// The block contains a layout callback that needs access to just one
/// base region.
SingleLayouter(callbacks::BlockSingleCallback),
/// The block contains a layout callback that needs access to the exact
/// regions.
MultiLayouter(callbacks::BlockMultiCallback),
}
impl Default for BlockChild {
fn default() -> Self {
Self::Content(Content::default())
}
}
cast! {
BlockChild,
self => match self {
Self::Content(content) => content.into_value(),
_ => Value::Auto,
},
v: Content => Self::Content(v),
}
/// Defines how to size something along an axis.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum Sizing {
/// A track that fits its cell's contents.
/// A track that fits its item'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
/// A size specified in absolute terms and relative to the parent's size.
Rel(Rel),
/// A size specified as a fraction of the remaining free space in the
/// parent.
Fr(Fr),
}
impl Sizing {
/// Whether this is an automatic sizing.
pub fn is_auto(self) -> bool {
matches!(self, Self::Auto)
}
/// Whether this is fractional sizing.
pub fn is_fractional(self) -> bool {
matches!(self, Self::Fr(_))
@ -494,6 +813,15 @@ impl Default for Sizing {
}
}
impl From<Smart<Rel>> for Sizing {
fn from(smart: Smart<Rel>) -> Self {
match smart {
Smart::Auto => Self::Auto,
Smart::Custom(rel) => Self::Rel(rel),
}
}
}
impl<T: Into<Spacing>> From<T> for Sizing {
fn from(spacing: T) -> Self {
match spacing.into() {
@ -514,3 +842,109 @@ cast! {
v: Rel<Length> => Self::Rel(v),
v: Fr => Self::Fr(v),
}
/// Distribute a fixed height spread over existing regions into a new first
/// height and a new backlog.
fn distribute<'a>(
height: Abs,
regions: Regions,
buf: &'a mut SmallVec<[Abs; 2]>,
) -> (Abs, &'a mut [Abs]) {
// Build new region heights from old regions.
let mut remaining = height;
for region in regions.iter() {
let limited = region.y.min(remaining);
buf.push(limited);
remaining -= limited;
if remaining.approx_empty() {
break;
}
}
// 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..])
}
/// Manual closure implementations for layout callbacks.
///
/// Normal closures are not `Hash`, so we can't use them.
mod callbacks {
use super::*;
macro_rules! callback {
($name:ident = ($($param:ident: $param_ty:ty),* $(,)?) -> $ret:ty) => {
#[derive(Debug, Clone, PartialEq, Hash)]
pub struct $name {
captured: Content,
f: fn(&Content, $($param_ty),*) -> $ret,
}
impl $name {
pub fn new<T: NativeElement>(
captured: Packed<T>,
f: fn(&Packed<T>, $($param_ty),*) -> $ret,
) -> Self {
Self {
// Type-erased the content.
captured: captured.pack(),
// Safety: The only difference between the two function
// pointer types is the type of the first parameter,
// which changes from `&Packed<T>` to `&Content`. This
// is safe because:
// - `Packed<T>` is a transparent wrapper around
// `Content`, so for any `T` it has the same memory
// representation as `Content`.
// - While `Packed<T>` imposes the additional constraint
// that the content is of type `T`, this constraint is
// upheld: It is initially the case because we store a
// `Packed<T>` above. It keeps being the case over the
// lifetime of the closure because `capture` is a
// private field and `Content`'s `Clone` impl is
// guaranteed to retain the type (if it didn't,
// literally everything would break).
f: unsafe { std::mem::transmute(f) },
}
}
pub fn call(&self, $($param: $param_ty),*) -> $ret {
(self.f)(&self.captured, $($param),*)
}
}
};
}
callback! {
InlineCallback = (
engine: &mut Engine,
styles: StyleChain,
region: Size,
) -> SourceResult<Vec<InlineItem>>
}
callback! {
BlockSingleCallback = (
engine: &mut Engine,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame>
}
callback! {
BlockMultiCallback = (
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment>
}
}

View File

@ -13,9 +13,8 @@ use crate::foundations::{
};
use crate::introspection::TagElem;
use crate::layout::{
Abs, AlignElem, Axes, BlockElem, ColbreakElem, ColumnsElem, FixedAlignment,
FlushElem, Fr, Fragment, Frame, FrameItem, LayoutMultiple, LayoutSingle, PlaceElem,
Point, Regions, Rel, Size, Spacing, VElem,
Abs, AlignElem, Axes, BlockElem, ColbreakElem, FixedAlignment, FlushElem, Fr,
Fragment, Frame, FrameItem, PlaceElem, Point, Regions, Rel, Size, Spacing, VElem,
};
use crate::model::{FootnoteElem, FootnoteEntry, ParElem};
use crate::utils::Numeric;
@ -24,16 +23,16 @@ use crate::utils::Numeric;
///
/// This element is responsible for layouting both the top-level content flow
/// and the contents of boxes.
#[elem(Debug, LayoutMultiple)]
#[elem(Debug)]
pub struct FlowElem {
/// The children that will be arranged into a flow.
#[variadic]
pub children: Vec<Content>,
}
impl LayoutMultiple for Packed<FlowElem> {
impl Packed<FlowElem> {
#[typst_macros::time(name = "flow", span = self.span())]
fn layout(
pub fn layout(
&self,
engine: &mut Engine,
styles: StyleChain,
@ -59,12 +58,13 @@ impl LayoutMultiple for Packed<FlowElem> {
alone = child
.to_packed::<StyledElem>()
.map_or(child, |styled| &styled.child)
.can::<dyn LayoutMultiple>();
.is::<BlockElem>();
}
let outer = styles;
let mut layouter = FlowLayouter::new(regions, styles, alone);
for mut child in self.children().iter() {
let outer = styles;
let mut styles = styles;
if let Some(styled) = child.to_packed::<StyledElem>() {
child = &styled.child;
@ -77,6 +77,10 @@ impl LayoutMultiple for Packed<FlowElem> {
layouter.flush(engine)?;
} else if let Some(elem) = child.to_packed::<VElem>() {
layouter.layout_spacing(engine, elem, styles)?;
} else if let Some(elem) = child.to_packed::<ParElem>() {
layouter.layout_par(engine, elem, styles)?;
} else if let Some(elem) = child.to_packed::<BlockElem>() {
layouter.layout_block(engine, elem, styles)?;
} else if let Some(placed) = child.to_packed::<PlaceElem>() {
layouter.layout_placed(engine, placed, styles)?;
} else if child.is::<ColbreakElem>() {
@ -84,12 +88,6 @@ impl LayoutMultiple for Packed<FlowElem> {
{
layouter.finish_region(engine, true)?;
}
} else if let Some(elem) = child.to_packed::<ParElem>() {
layouter.layout_par(engine, elem, styles)?;
} else if let Some(layoutable) = child.with::<dyn LayoutSingle>() {
layouter.layout_single(engine, layoutable, styles)?;
} else if let Some(layoutable) = child.with::<dyn LayoutMultiple>() {
layouter.layout_multiple(engine, child, layoutable, styles)?;
} else {
bail!(child.span(), "unexpected flow child");
}
@ -199,6 +197,7 @@ impl<'a> FlowLayouter<'a> {
/// Create a new flow layouter.
fn new(mut regions: Regions<'a>, styles: StyleChain<'a>, alone: bool) -> Self {
let expand = regions.expand;
let root = std::mem::replace(&mut regions.root, false);
// Disable vertical expansion when there are multiple or not directly
// layoutable children.
@ -206,9 +205,6 @@ impl<'a> FlowLayouter<'a> {
regions.expand.y = false;
}
// Disable root.
let root = std::mem::replace(&mut regions.root, false);
Self {
root,
regions,
@ -253,27 +249,6 @@ impl<'a> FlowLayouter<'a> {
)
}
/// Layout a placed element.
fn layout_placed(
&mut self,
engine: &mut Engine,
placed: &Packed<PlaceElem>,
styles: StyleChain,
) -> SourceResult<()> {
let float = placed.float(styles);
let clearance = placed.clearance(styles);
let alignment = placed.alignment(styles);
let delta = Axes::new(placed.dx(styles), placed.dy(styles)).resolve(styles);
let x_align = alignment.map_or(FixedAlignment::Center, |align| {
align.x().unwrap_or_default().resolve(styles)
});
let y_align = alignment.map(|align| align.y().map(|y| y.resolve(styles)));
let mut frame = placed.layout(engine, styles, self.regions.base())?.into_frame();
frame.post_process(styles);
let item = FlowItem::Placed { frame, x_align, y_align, delta, float, clearance };
self.layout_item(engine, item)
}
/// Layout a paragraph.
fn layout_par(
&mut self,
@ -337,63 +312,33 @@ impl<'a> FlowLayouter<'a> {
Ok(())
}
/// Layout into a single region.
fn layout_single(
&mut self,
engine: &mut Engine,
layoutable: &dyn LayoutSingle,
styles: StyleChain,
) -> SourceResult<()> {
let align = AlignElem::alignment_in(styles).resolve(styles);
let sticky = BlockElem::sticky_in(styles);
let pod = Regions::one(self.regions.base(), Axes::splat(false));
let mut frame = layoutable.layout(engine, styles, pod)?;
self.drain_tag(&mut frame);
frame.post_process(styles);
self.layout_item(
engine,
FlowItem::Frame { frame, align, sticky, movable: true },
)?;
self.last_was_par = false;
Ok(())
}
/// Layout into multiple regions.
fn layout_multiple(
fn layout_block(
&mut self,
engine: &mut Engine,
child: &Content,
layoutable: &dyn LayoutMultiple,
styles: StyleChain,
block: &'a Packed<BlockElem>,
styles: StyleChain<'a>,
) -> SourceResult<()> {
// Temporarily delegerate rootness to the columns.
// Temporarily delegate rootness to the columns.
let is_root = self.root;
if is_root && child.is::<ColumnsElem>() {
if is_root && block.rootable(styles) {
self.root = false;
self.regions.root = true;
}
let mut notes = Vec::new();
if self.regions.is_full() {
// Skip directly if region is already full.
self.finish_region(engine, false)?;
}
// How to align the block.
let align = if let Some(align) = child.to_packed::<AlignElem>() {
align.alignment(styles)
} else if let Some(styled) = child.to_packed::<StyledElem>() {
AlignElem::alignment_in(styles.chain(&styled.styles))
} else {
AlignElem::alignment_in(styles)
}
.resolve(styles);
// Layout the block itself.
let sticky = BlockElem::sticky_in(styles);
let fragment = layoutable.layout(engine, styles, self.regions)?;
let sticky = block.sticky(styles);
let fragment = block.layout(engine, styles, self.regions)?;
// How to align the block.
let align = AlignElem::alignment_in(styles).resolve(styles);
let mut notes = Vec::new();
for (i, mut frame) in fragment.into_iter().enumerate() {
// Find footnotes in the frame.
if self.root {
@ -421,6 +366,27 @@ impl<'a> FlowLayouter<'a> {
Ok(())
}
/// Layout a placed element.
fn layout_placed(
&mut self,
engine: &mut Engine,
placed: &Packed<PlaceElem>,
styles: StyleChain,
) -> SourceResult<()> {
let float = placed.float(styles);
let clearance = placed.clearance(styles);
let alignment = placed.alignment(styles);
let delta = Axes::new(placed.dx(styles), placed.dy(styles)).resolve(styles);
let x_align = alignment.map_or(FixedAlignment::Center, |align| {
align.x().unwrap_or_default().resolve(styles)
});
let y_align = alignment.map(|align| align.y().map(|y| y.resolve(styles)));
let mut frame = placed.layout(engine, styles, self.regions.base())?.into_frame();
frame.post_process(styles);
let item = FlowItem::Placed { frame, x_align, y_align, delta, float, clearance };
self.layout_item(engine, item)
}
/// Attach currently pending metadata to the frame.
fn drain_tag(&mut self, frame: &mut Frame) {
if !self.pending_tags.is_empty() && !frame.is_empty() {
@ -444,13 +410,13 @@ impl<'a> FlowLayouter<'a> {
&& !self
.items
.iter()
.any(|item| matches!(item, FlowItem::Frame { .. }))
.any(|item| matches!(item, FlowItem::Frame { .. },))
{
return Ok(());
}
self.regions.size.y -= v
}
FlowItem::Fractional(_) => {}
FlowItem::Fractional(..) => {}
FlowItem::Frame { ref frame, movable, .. } => {
let height = frame.height();
while !self.regions.size.y.fits(height) && !self.regions.in_last() {
@ -615,7 +581,8 @@ impl<'a> FlowLayouter<'a> {
}
FlowItem::Fractional(v) => {
let remaining = self.initial.y - used.y;
offset += v.share(fr, remaining);
let length = v.share(fr, remaining);
offset += length;
}
FlowItem::Frame { frame, align, .. } => {
ruler = ruler.max(align.y);

View File

@ -41,6 +41,11 @@ impl Fragment {
self.0
}
/// Extract a slice with the contained frames.
pub fn as_slice(&self) -> &[Frame] {
&self.0
}
/// Iterate over the contained frames.
pub fn iter(&self) -> std::slice::Iter<Frame> {
self.0.iter()

View File

@ -30,6 +30,8 @@ pub struct Frame {
/// The items composing this layout.
items: Arc<LazyHash<Vec<(Point, FrameItem)>>>,
/// The hardness of this frame.
///
/// Determines whether it is a boundary for gradient drawing.
kind: FrameKind,
}
@ -70,6 +72,12 @@ impl Frame {
self.kind = kind;
}
/// Sets the frame's hardness builder-style.
pub fn with_kind(mut self, kind: FrameKind) -> Self {
self.kind = kind;
self
}
/// Whether the frame is hard or soft.
pub fn kind(&self) -> FrameKind {
self.kind
@ -217,6 +225,11 @@ impl Frame {
/// Inline a frame at the given layer.
fn inline(&mut self, layer: usize, pos: Point, frame: Frame) {
// Skip work if there's nothing to do.
if frame.items.is_empty() {
return;
}
// Try to just reuse the items.
if pos.is_zero() && self.items.is_empty() {
self.items = frame.items;
@ -354,9 +367,9 @@ impl Frame {
pub fn fill_and_stroke(
&mut self,
fill: Option<Paint>,
stroke: Sides<Option<FixedStroke>>,
outset: Sides<Rel<Abs>>,
radius: Corners<Rel<Abs>>,
stroke: &Sides<Option<FixedStroke>>,
outset: &Sides<Rel<Abs>>,
radius: &Corners<Rel<Abs>>,
span: Span,
) {
let outset = outset.relative_to(self.size());
@ -479,7 +492,7 @@ pub enum FrameKind {
Soft,
/// A container which uses its own size.
///
/// This is used for page, block, box, column, grid, and stack elements.
/// This is used for pages, blocks, and boxes.
Hard,
}

View File

@ -14,10 +14,7 @@ use crate::foundations::{
Array, CastInfo, Content, Context, Fold, FromValue, Func, IntoValue, Reflect,
Resolve, Smart, StyleChain, Value,
};
use crate::layout::{
Abs, Alignment, Axes, Fragment, LayoutMultiple, Length, LinePosition, Regions, Rel,
Sides, Sizing,
};
use crate::layout::{Abs, Alignment, Axes, Length, LinePosition, Rel, Sides, Sizing};
use crate::syntax::Span;
use crate::utils::NonZeroExt;
use crate::visualize::{Paint, Stroke};
@ -204,17 +201,6 @@ impl From<Content> for Cell {
}
}
impl LayoutMultiple for Cell {
fn layout(
&self,
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
self.body.layout(engine, styles, regions)
}
}
/// A grid entry.
#[derive(Clone)]
pub(super) enum Entry {

View File

@ -10,8 +10,8 @@ use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
use crate::foundations::{Resolve, StyleChain};
use crate::layout::{
Abs, Axes, Cell, CellGrid, Dir, Fr, Fragment, Frame, FrameItem, LayoutMultiple,
Length, Point, Regions, Rel, Size, Sizing,
Abs, Axes, Cell, CellGrid, Dir, Fr, Fragment, Frame, FrameItem, Length, Point,
Regions, Rel, Size, Sizing,
};
use crate::syntax::Span;
use crate::text::TextElem;
@ -841,7 +841,7 @@ impl<'a> GridLayouter<'a> {
let size = Size::new(available, height);
let pod = Regions::one(size, Axes::splat(false));
let frame = cell.measure(engine, self.styles, pod)?.into_frame();
let frame = cell.body.measure(engine, self.styles, pod)?.into_frame();
resolved.set_max(frame.width() - already_covered_width);
}
@ -1069,7 +1069,7 @@ impl<'a> GridLayouter<'a> {
pod
};
let frames = cell.measure(engine, self.styles, pod)?.into_frames();
let frames = cell.body.measure(engine, self.styles, pod)?.into_frames();
// Skip the first region if one cell in it is empty. Then,
// remeasure.
@ -1232,7 +1232,7 @@ impl<'a> GridLayouter<'a> {
// rows.
pod.full = self.regions.full;
}
let frame = cell.layout(engine, self.styles, pod)?.into_frame();
let frame = cell.body.layout(engine, self.styles, pod)?.into_frame();
let mut pos = pos;
if self.is_rtl {
// In the grid, cell colspans expand to the right,
@ -1286,7 +1286,7 @@ impl<'a> GridLayouter<'a> {
pod.size.x = width;
// Push the layouted frames into the individual output frames.
let fragment = cell.layout(engine, self.styles, pod)?;
let fragment = cell.body.layout(engine, self.styles, pod)?;
for (output, frame) in outputs.iter_mut().zip(fragment) {
let mut pos = pos;
if self.is_rtl {

View File

@ -19,11 +19,12 @@ use smallvec::{smallvec, SmallVec};
use crate::diag::{bail, SourceResult, StrResult, Trace, Tracepoint};
use crate::engine::Engine;
use crate::foundations::{
cast, elem, scope, Array, Content, Fold, Packed, Show, Smart, StyleChain, Value,
cast, elem, scope, Array, Content, Fold, NativeElement, Packed, Show, Smart,
StyleChain, Value,
};
use crate::layout::{
Abs, AlignElem, Alignment, Axes, Dir, Fragment, LayoutMultiple, Length,
OuterHAlignment, OuterVAlignment, Regions, Rel, Sides, Sizing,
Abs, Alignment, Axes, BlockElem, Dir, Fragment, Length, OuterHAlignment,
OuterVAlignment, Regions, Rel, Sides, Sizing,
};
use crate::model::{TableCell, TableFooter, TableHLine, TableHeader, TableVLine};
use crate::syntax::Span;
@ -148,7 +149,7 @@ use crate::visualize::{Paint, Stroke};
///
/// Furthermore, strokes of a repeated grid header or footer will take
/// precedence over regular cell strokes.
#[elem(scope, LayoutMultiple)]
#[elem(scope, Show)]
pub struct GridElem {
/// The column sizes.
///
@ -335,64 +336,67 @@ impl GridElem {
type GridFooter;
}
impl LayoutMultiple for Packed<GridElem> {
#[typst_macros::time(name = "grid", span = self.span())]
fn layout(
&self,
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
let inset = self.inset(styles);
let align = self.align(styles);
let columns = self.columns(styles);
let rows = self.rows(styles);
let column_gutter = self.column_gutter(styles);
let row_gutter = self.row_gutter(styles);
let fill = self.fill(styles);
let stroke = self.stroke(styles);
let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice());
let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice());
// Use trace to link back to the grid when a specific cell errors
let tracepoint = || Tracepoint::Call(Some(eco_format!("grid")));
let resolve_item = |item: &GridItem| item.to_resolvable(styles);
let children = self.children().iter().map(|child| match child {
GridChild::Header(header) => ResolvableGridChild::Header {
repeat: header.repeat(styles),
span: header.span(),
items: header.children().iter().map(resolve_item),
},
GridChild::Footer(footer) => ResolvableGridChild::Footer {
repeat: footer.repeat(styles),
span: footer.span(),
items: footer.children().iter().map(resolve_item),
},
GridChild::Item(item) => {
ResolvableGridChild::Item(item.to_resolvable(styles))
}
});
let grid = CellGrid::resolve(
tracks,
gutter,
children,
fill,
align,
&inset,
&stroke,
engine,
styles,
self.span(),
)
.trace(engine.world, tracepoint, self.span())?;
let layouter = GridLayouter::new(&grid, regions, styles, self.span());
// Measure the columns and layout the grid row-by-row.
layouter.layout(engine)
impl Show for Packed<GridElem> {
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::multi_layouter(self.clone(), layout_grid).pack())
}
}
/// Layout the grid.
#[typst_macros::time(span = elem.span())]
fn layout_grid(
elem: &Packed<GridElem>,
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
let inset = elem.inset(styles);
let align = elem.align(styles);
let columns = elem.columns(styles);
let rows = elem.rows(styles);
let column_gutter = elem.column_gutter(styles);
let row_gutter = elem.row_gutter(styles);
let fill = elem.fill(styles);
let stroke = elem.stroke(styles);
let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice());
let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice());
// Use trace to link back to the grid when a specific cell errors
let tracepoint = || Tracepoint::Call(Some(eco_format!("grid")));
let resolve_item = |item: &GridItem| item.to_resolvable(styles);
let children = elem.children().iter().map(|child| match child {
GridChild::Header(header) => ResolvableGridChild::Header {
repeat: header.repeat(styles),
span: header.span(),
items: header.children().iter().map(resolve_item),
},
GridChild::Footer(footer) => ResolvableGridChild::Footer {
repeat: footer.repeat(styles),
span: footer.span(),
items: footer.children().iter().map(resolve_item),
},
GridChild::Item(item) => ResolvableGridChild::Item(item.to_resolvable(styles)),
});
let grid = CellGrid::resolve(
tracks,
gutter,
children,
fill,
align,
&inset,
&stroke,
engine,
styles,
elem.span(),
)
.trace(engine.world, tracepoint, elem.span())?;
let layouter = GridLayouter::new(&grid, regions, styles, elem.span());
// Measure the columns and layout the grid row-by-row.
layouter.layout(engine)
}
/// Track sizing definitions.
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct TrackSizings(pub SmallVec<[Sizing; 4]>);
@ -956,7 +960,7 @@ pub fn show_grid_cell(
}
if let Smart::Custom(alignment) = align {
body = body.styled(AlignElem::set_alignment(alignment));
body = body.aligned(alignment);
}
Ok(body)

View File

@ -3,9 +3,7 @@ use super::repeated::Repeatable;
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::Resolve;
use crate::layout::{
Abs, Axes, Cell, Frame, GridLayouter, LayoutMultiple, Point, Regions, Size, Sizing,
};
use crate::layout::{Abs, Axes, Cell, Frame, GridLayouter, Point, Regions, Size, Sizing};
use crate::utils::MaybeReverseIter;
/// All information needed to layout a single rowspan.
@ -138,7 +136,7 @@ impl<'a> GridLayouter<'a> {
}
// Push the layouted frames directly into the finished frames.
let fragment = cell.layout(engine, self.styles, pod)?;
let fragment = cell.body.layout(engine, self.styles, pod)?;
let (current_region, current_rrows) = current_region_data.unzip();
for ((i, finished), frame) in self
.finished

View File

@ -16,10 +16,9 @@ use crate::eval::Tracer;
use crate::foundations::{Content, Packed, Resolve, Smart, StyleChain, StyledElem};
use crate::introspection::{Introspector, Locator, TagElem};
use crate::layout::{
Abs, AlignElem, Axes, BoxElem, Dir, Em, FixedAlignment, Fr, Fragment, Frame,
FrameItem, HElem, Point, Regions, Size, Sizing, Spacing,
Abs, AlignElem, BoxElem, Dir, Em, FixedAlignment, Fr, Fragment, Frame, FrameItem,
HElem, InlineElem, InlineItem, Point, Size, Sizing, Spacing,
};
use crate::math::{EquationElem, MathParItem};
use crate::model::{Linebreaks, ParElem};
use crate::syntax::Span;
use crate::text::{
@ -220,7 +219,7 @@ impl Segment<'_> {
enum Item<'a> {
/// A shaped text run with consistent style and direction.
Text(ShapedText<'a>),
/// Absolute spacing between other items.
/// Absolute spacing between other items, and whether it is weak.
Absolute(Abs, bool),
/// Fractional spacing between other items.
Fractional(Fr, Option<(&'a Packed<BoxElem>, StyleChain<'a>)>),
@ -544,17 +543,15 @@ fn collect<'a>(
} else {
collector.push_text(if double { "\"" } else { "'" }, styles);
}
} else if let Some(elem) = child.to_packed::<EquationElem>() {
} else if let Some(elem) = child.to_packed::<InlineElem>() {
collector.push_item(Item::Skip(LTR_ISOLATE));
let pod = Regions::one(region, Axes::splat(false));
for item in elem.layout_inline(engine, styles, pod)? {
for item in elem.layout(engine, styles, region)? {
match item {
MathParItem::Space(space) => {
// Spaces generated by math layout are weak.
collector.push_item(Item::Absolute(space, true));
InlineItem::Space(space, weak) => {
collector.push_item(Item::Absolute(space, weak));
}
MathParItem::Frame(frame) => {
InlineItem::Frame(frame) => {
collector.push_item(Item::Frame(frame, styles));
}
}
@ -565,8 +562,7 @@ fn collect<'a>(
if let Sizing::Fr(v) = elem.width(styles) {
collector.push_item(Item::Fractional(v, Some((elem, styles))));
} else {
let pod = Regions::one(region, Axes::splat(false));
let frame = elem.layout(engine, styles, pod)?;
let frame = elem.layout(engine, styles, region)?;
collector.push_item(Item::Frame(frame, styles));
}
} else if let Some(elem) = child.to_packed::<TagElem>() {
@ -1440,8 +1436,7 @@ fn commit(
let amount = v.share(fr, remaining);
if let Some((elem, styles)) = elem {
let region = Size::new(amount, full);
let pod = Regions::one(region, Axes::new(true, false));
let mut frame = elem.layout(engine, *styles, pod)?;
let mut frame = elem.layout(engine, *styles, region)?;
frame.post_process(*styles);
frame.translate(Point::with_y(TextElem::baseline_in(*styles)));
push(&mut offset, frame);

View File

@ -3,10 +3,10 @@ use comemo::Track;
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{
dict, elem, func, Content, Context, Func, NativeElement, Packed, StyleChain,
dict, elem, func, Content, Context, Func, NativeElement, Packed, Show, StyleChain,
};
use crate::introspection::Locatable;
use crate::layout::{Fragment, LayoutMultiple, Regions, Size};
use crate::layout::{BlockElem, Size};
use crate::syntax::Span;
/// Provides access to the current outer container's (or page's, if none)
@ -67,30 +67,27 @@ pub fn layout(
}
/// Executes a `layout` call.
#[elem(Locatable, LayoutMultiple)]
#[elem(Locatable, Show)]
struct LayoutElem {
/// The function to call with the outer container's (or page's) size.
#[required]
func: Func,
}
impl LayoutMultiple for Packed<LayoutElem> {
#[typst_macros::time(name = "layout", span = self.span())]
fn layout(
&self,
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
// Gets the current region's base size, which will be the size of the
// outer container, or of the page if there is no such container.
let Size { x, y } = regions.base();
let loc = self.location().unwrap();
let context = Context::new(Some(loc), Some(styles));
let result = self
.func()
.call(engine, context.track(), [dict! { "width" => x, "height" => y }])?
.display();
result.layout(engine, styles, regions)
impl Show for Packed<LayoutElem> {
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::multi_layouter(self.clone(), |elem, engine, styles, regions| {
// Gets the current region's base size, which will be the size of the
// outer container, or of the page if there is no such container.
let Size { x, y } = regions.base();
let loc = elem.location().unwrap();
let context = Context::new(Some(loc), Some(styles));
let result = elem
.func()
.call(engine, context.track(), [dict! { "width" => x, "height" => y }])?
.display();
result.layout(engine, styles, regions)
})
.pack())
}
}

View File

@ -5,7 +5,7 @@ use crate::engine::Engine;
use crate::foundations::{
dict, func, Content, Context, Dict, Resolve, Smart, StyleChain, Styles,
};
use crate::layout::{Abs, Axes, LayoutMultiple, Length, Regions, Size};
use crate::layout::{Abs, Axes, Length, Regions, Size};
use crate::syntax::Span;
/// Measures the layouted size of content.

View File

@ -58,7 +58,7 @@ pub use self::page::*;
pub use self::place::*;
pub use self::point::*;
pub use self::ratio::*;
pub use self::regions::Regions;
pub use self::regions::*;
pub use self::rel::*;
pub use self::repeat::*;
pub use self::sides::*;
@ -119,72 +119,15 @@ pub fn define(global: &mut Scope) {
global.define_func::<layout>();
}
/// Root-level layout.
///
/// This produces a complete document and is implemented for
/// [`DocumentElem`][crate::model::DocumentElem]. Any [`Content`]
/// can also be laid out at root level, in which case it is
/// wrapped inside a document element.
pub trait LayoutRoot {
/// Layout into a document with one frame per page.
fn layout_root(
&self,
engine: &mut Engine,
styles: StyleChain,
) -> SourceResult<Document>;
}
/// Layout into multiple [regions][Regions].
///
/// This is more appropriate for elements that, for example, can be
/// laid out across multiple pages or columns.
pub trait LayoutMultiple {
/// Layout into one frame per region.
fn layout(
&self,
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment>;
/// Layout without side effects.
impl Content {
/// Layout the content into a document.
///
/// This element must be layouted again in the same order for the results to
/// be valid.
fn measure(
&self,
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
let mut locator = Locator::chained(engine.locator.track());
let mut engine = Engine {
world: engine.world,
route: engine.route.clone(),
introspector: engine.introspector,
locator: &mut locator,
tracer: TrackedMut::reborrow_mut(&mut engine.tracer),
};
self.layout(&mut engine, styles, regions)
}
}
/// Layout into a single [region][Regions].
///
/// This is more appropriate for elements that don't make sense to
/// layout across multiple pages or columns, such as shapes.
pub trait LayoutSingle {
/// Layout into one frame per region.
fn layout(
&self,
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Frame>;
}
impl LayoutRoot for Content {
fn layout_root(
/// This first realizes the content into a
/// [`DocumentElem`][crate::model::DocumentElem], which is then laid out. In
/// contrast to [`layout`](Self::layout()), this does not take regions since
/// the regions are defined by the page configuration in the content and
/// style chain.
pub fn layout_document(
&self,
engine: &mut Engine,
styles: StyleChain,
@ -209,7 +152,7 @@ impl LayoutRoot for Content {
};
let arenas = Arenas::default();
let (document, styles) = realize_doc(&mut engine, &arenas, content, styles)?;
document.layout_root(&mut engine, styles)
document.layout(&mut engine, styles)
}
cached(
@ -222,10 +165,25 @@ impl LayoutRoot for Content {
styles,
)
}
}
impl LayoutMultiple for Content {
fn layout(
/// Layout the content into the given regions.
pub fn layout(
&self,
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
let fragment = self.measure(engine, styles, regions)?;
engine.locator.visit_frames(&fragment);
Ok(fragment)
}
/// Layout without side effects.
///
/// For the results to be valid, the element must either be layouted again
/// or the measurement must be confirmed through a call to
/// `engine.locator.visit_frames(&fragment)`.
pub fn measure(
&self,
engine: &mut Engine,
styles: StyleChain,
@ -271,7 +229,7 @@ impl LayoutMultiple for Content {
flow.layout(&mut engine, styles, regions)
}
let fragment = cached(
cached(
self,
engine.world,
engine.introspector,
@ -280,9 +238,6 @@ impl LayoutMultiple for Content {
TrackedMut::reborrow_mut(&mut engine.tracer),
styles,
regions,
)?;
engine.locator.visit_frames(&fragment);
Ok(fragment)
)
}
}

View File

@ -1,8 +1,10 @@
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{elem, Content, Packed, Resolve, StyleChain};
use crate::foundations::{
elem, Content, NativeElement, Packed, Resolve, Show, StyleChain,
};
use crate::layout::{
Abs, Fragment, LayoutMultiple, Length, Point, Regions, Rel, Sides, Size,
Abs, BlockElem, Fragment, Frame, Length, Point, Regions, Rel, Sides, Size,
};
/// Adds spacing around content.
@ -18,7 +20,7 @@ use crate::layout::{
/// _Typing speeds can be
/// measured in words per minute._
/// ```
#[elem(title = "Padding", LayoutMultiple)]
#[elem(title = "Padding", Show)]
pub struct PadElem {
/// The padding at the left side.
#[parse(
@ -60,49 +62,64 @@ pub struct PadElem {
pub body: Content,
}
impl LayoutMultiple for Packed<PadElem> {
#[typst_macros::time(name = "pad", span = self.span())]
fn layout(
&self,
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
let sides = Sides::new(
self.left(styles),
self.top(styles),
self.right(styles),
self.bottom(styles),
);
// Layout child into padded regions.
let mut backlog = vec![];
let padding = sides.resolve(styles);
let pod = regions.map(&mut backlog, |size| shrink(size, padding));
let mut fragment = self.body().layout(engine, styles, pod)?;
for frame in &mut fragment {
// Apply the padding inversely such that the grown size padded
// yields the frame's size.
let padded = grow(frame.size(), padding);
let padding = padding.relative_to(padded);
let offset = Point::new(padding.left, padding.top);
// Grow the frame and translate everything in the frame inwards.
frame.set_size(padded);
frame.translate(offset);
}
Ok(fragment)
impl Show for Packed<PadElem> {
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::multi_layouter(self.clone(), layout_pad).pack())
}
}
/// Shrink a size by padding relative to the size itself.
fn shrink(size: Size, padding: Sides<Rel<Abs>>) -> Size {
size - padding.relative_to(size).sum_by_axis()
/// Layout the padded content.
#[typst_macros::time(span = elem.span())]
fn layout_pad(
elem: &Packed<PadElem>,
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
let padding = Sides::new(
elem.left(styles).resolve(styles),
elem.top(styles).resolve(styles),
elem.right(styles).resolve(styles),
elem.bottom(styles).resolve(styles),
);
let mut backlog = vec![];
let pod = regions.map(&mut backlog, |size| shrink(size, &padding));
// Layout child into padded regions.
let mut fragment = elem.body().layout(engine, styles, pod)?;
for frame in &mut fragment {
grow(frame, &padding);
}
Ok(fragment)
}
/// Grow a size by padding relative to the grown size.
/// Shrink a region size by an inset relative to the size itself.
pub(crate) fn shrink(size: Size, inset: &Sides<Rel<Abs>>) -> Size {
size - inset.sum_by_axis().relative_to(size)
}
/// Shrink the components of possibly multiple `Regions` by an inset relative to
/// the regions themselves.
pub(crate) fn shrink_multiple(
size: &mut Size,
full: &mut Abs,
backlog: &mut [Abs],
last: &mut Option<Abs>,
inset: &Sides<Rel<Abs>>,
) {
let summed = inset.sum_by_axis();
*size -= summed.relative_to(*size);
*full -= summed.y.relative_to(*full);
for item in backlog {
*item -= summed.y.relative_to(*item);
}
*last = last.map(|v| v - summed.y.relative_to(v));
}
/// Grow a frame's size by an inset relative to the grown size.
/// This is the inverse operation to `shrink()`.
///
/// For the horizontal axis the derivation looks as follows.
@ -110,8 +127,8 @@ fn shrink(size: Size, padding: Sides<Rel<Abs>>) -> Size {
///
/// Let w be the grown target width,
/// s be the given width,
/// l be the left padding,
/// r be the right padding,
/// l be the left inset,
/// r be the right inset,
/// p = l + r.
///
/// We want that: w - l.resolve(w) - r.resolve(w) = s
@ -121,6 +138,17 @@ fn shrink(size: Size, padding: Sides<Rel<Abs>>) -> Size {
/// <=> w - p.rel * w - p.abs = s
/// <=> (1 - p.rel) * w = s + p.abs
/// <=> w = (s + p.abs) / (1 - p.rel)
fn grow(size: Size, padding: Sides<Rel<Abs>>) -> Size {
size.zip_map(padding.sum_by_axis(), |s, p| (s + p.abs) / (1.0 - p.rel.get()))
pub(crate) fn grow(frame: &mut Frame, inset: &Sides<Rel<Abs>>) {
// Apply the padding inversely such that the grown size padded
// yields the frame's size.
let padded = frame
.size()
.zip_map(inset.sum_by_axis(), |s, p| (s + p.abs) / (1.0 - p.rel.get()));
let inset = inset.relative_to(padded);
let offset = Point::new(inset.left, inset.top);
// Grow the frame and translate everything in the frame inwards.
frame.set_size(padded);
frame.translate(offset);
}

View File

@ -14,8 +14,8 @@ use crate::foundations::{
};
use crate::introspection::{Counter, CounterDisplayElem, CounterKey, ManualPageCounter};
use crate::layout::{
Abs, AlignElem, Alignment, Axes, ColumnsElem, Dir, Frame, HAlignment, LayoutMultiple,
Length, OuterVAlignment, Point, Ratio, Regions, Rel, Sides, Size, SpecificAlignment,
Abs, AlignElem, Alignment, Axes, ColumnsElem, Dir, Frame, HAlignment, Length,
OuterVAlignment, Point, Ratio, Regions, Rel, Sides, Size, SpecificAlignment,
VAlignment,
};

View File

@ -2,7 +2,7 @@ use crate::diag::{bail, At, Hint, SourceResult};
use crate::engine::Engine;
use crate::foundations::{elem, scope, Content, Packed, Smart, StyleChain, Unlabellable};
use crate::layout::{
Alignment, Axes, Em, Fragment, LayoutMultiple, Length, Regions, Rel, Size, VAlignment,
Alignment, Axes, Em, Fragment, Length, Regions, Rel, Size, VAlignment,
};
use crate::realize::{Behave, Behaviour};

View File

@ -2,6 +2,28 @@ use std::fmt::{self, Debug, Formatter};
use crate::layout::{Abs, Axes, Size};
/// A single region to layout into.
#[derive(Debug, Copy, Clone, Hash)]
pub struct Region {
/// The size of the region.
pub size: Size,
/// Whether elements should expand to fill the regions instead of shrinking
/// to fit the content.
pub expand: Axes<bool>,
}
impl Region {
/// Create a new region.
pub fn new(size: Size, expand: Axes<bool>) -> Self {
Self { size, expand }
}
/// Turns this into a region sequence.
pub fn into_regions(self) -> Regions<'static> {
Regions::one(self.size, self.expand)
}
}
/// A sequence of regions to layout into.
///
/// A *region* is a contiguous rectangular space in which elements
@ -80,7 +102,7 @@ impl Regions<'_> {
backlog,
last: self.last.map(|y| f(Size::new(x, y)).y),
expand: self.expand,
root: false,
root: self.root,
}
}

View File

@ -1,8 +1,10 @@
use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
use crate::foundations::{elem, Content, Packed, Resolve, StyleChain};
use crate::foundations::{
elem, Content, NativeElement, Packed, Resolve, Show, StyleChain,
};
use crate::layout::{
Abs, AlignElem, Axes, Fragment, Frame, LayoutMultiple, Point, Regions, Size,
Abs, AlignElem, Axes, BlockElem, Fragment, Frame, Point, Regions, Size,
};
use crate::utils::Numeric;
@ -27,54 +29,59 @@ use crate::utils::Numeric;
/// Berlin, the 22nd of December, 2022
/// ]
/// ```
#[elem(LayoutMultiple)]
#[elem(Show)]
pub struct RepeatElem {
/// The content to repeat.
#[required]
pub body: Content,
}
impl LayoutMultiple for Packed<RepeatElem> {
#[typst_macros::time(name = "repeat", span = self.span())]
fn layout(
&self,
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
let pod = Regions::one(regions.size, Axes::new(false, false));
let piece = self.body().layout(engine, styles, pod)?.into_frame();
let align = AlignElem::alignment_in(styles).resolve(styles);
let fill = regions.size.x;
let width = piece.width();
let count = (fill / width).floor();
let remaining = fill % width;
let apart = remaining / (count - 1.0);
let size = Size::new(regions.size.x, piece.height());
if !size.is_finite() {
bail!(self.span(), "repeat with no size restrictions");
}
let mut frame = Frame::soft(size);
if piece.has_baseline() {
frame.set_baseline(piece.baseline());
}
let mut offset = Abs::zero();
if count == 1.0 {
offset += align.x.position(remaining);
}
if width > Abs::zero() {
for _ in 0..(count as usize).min(1000) {
frame.push_frame(Point::with_x(offset), piece.clone());
offset += piece.width() + apart;
}
}
Ok(Fragment::frame(frame))
impl Show for Packed<RepeatElem> {
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::multi_layouter(self.clone(), layout_repeat).pack())
}
}
/// Layout the repeated content.
#[typst_macros::time(span = elem.span())]
fn layout_repeat(
elem: &Packed<RepeatElem>,
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
let pod = Regions::one(regions.size, Axes::new(false, false));
let piece = elem.body().layout(engine, styles, pod)?.into_frame();
let align = AlignElem::alignment_in(styles).resolve(styles);
let fill = regions.size.x;
let width = piece.width();
let count = (fill / width).floor();
let remaining = fill % width;
let apart = remaining / (count - 1.0);
let size = Size::new(regions.size.x, piece.height());
if !size.is_finite() {
bail!(elem.span(), "repeat with no size restrictions");
}
let mut frame = Frame::soft(size);
if piece.has_baseline() {
frame.set_baseline(piece.baseline());
}
let mut offset = Abs::zero();
if count == 1.0 {
offset += align.x.position(remaining);
}
if width > Abs::zero() {
for _ in 0..(count as usize).min(1000) {
frame.push_frame(Point::with_x(offset), piece.clone());
offset += piece.width() + apart;
}
}
Ok(Fragment::frame(frame))
}

View File

@ -107,7 +107,7 @@ impl<T> Sides<Option<T>> {
impl Sides<Rel<Abs>> {
/// Evaluate the sides relative to the given `size`.
pub fn relative_to(self, size: Size) -> Sides<Abs> {
pub fn relative_to(&self, size: Size) -> Sides<Abs> {
Sides {
left: self.left.relative_to(size.x),
top: self.top.relative_to(size.y),
@ -115,6 +115,14 @@ impl Sides<Rel<Abs>> {
bottom: self.bottom.relative_to(size.y),
}
}
/// Whether all sides are zero.
pub fn is_zero(&self) -> bool {
self.left.is_zero()
&& self.top.is_zero()
&& self.right.is_zero()
&& self.bottom.is_zero()
}
}
impl<T> Get<Side> for Sides<T> {

View File

@ -130,6 +130,11 @@ pub struct VElem {
#[internal]
#[parse(args.named("weak")?.map(|v: bool| v as usize))]
pub weakness: usize,
/// Whether the element collapses if there is a parbreak in front.
#[internal]
#[parse(Some(false))]
pub attach: bool,
}
impl VElem {
@ -145,7 +150,7 @@ impl VElem {
/// Weak spacing with list attach weakness.
pub fn list_attach(amount: Spacing) -> Self {
Self::new(amount).with_weakness(2)
Self::new(amount).with_weakness(2).with_attach(true)
}
/// Weak spacing with BlockElem::ABOVE/BELOW weakness.

View File

@ -3,10 +3,12 @@ use typst_syntax::Span;
use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
use crate::foundations::{cast, elem, Content, Packed, Resolve, StyleChain, StyledElem};
use crate::foundations::{
cast, elem, Content, NativeElement, Packed, Resolve, Show, StyleChain, StyledElem,
};
use crate::layout::{
Abs, AlignElem, Axes, Axis, Dir, FixedAlignment, Fr, Fragment, Frame, HElem,
LayoutMultiple, Point, Regions, Size, Spacing, VElem,
Abs, AlignElem, Axes, Axis, BlockElem, Dir, FixedAlignment, Fr, Fragment, Frame,
HElem, Point, Regions, Size, Spacing, VElem,
};
use crate::utils::{Get, Numeric};
@ -24,7 +26,7 @@ use crate::utils::{Get, Numeric};
/// rect(width: 90pt),
/// )
/// ```
#[elem(LayoutMultiple)]
#[elem(Show)]
pub struct StackElem {
/// The direction along which the items are stacked. Possible values are:
///
@ -52,54 +54,9 @@ pub struct StackElem {
pub children: Vec<StackChild>,
}
impl LayoutMultiple for Packed<StackElem> {
#[typst_macros::time(name = "stack", span = self.span())]
fn layout(
&self,
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
let mut layouter =
StackLayouter::new(self.span(), self.dir(styles), regions, styles);
let axis = layouter.dir.axis();
// Spacing to insert before the next block.
let spacing = self.spacing(styles);
let mut deferred = None;
for child in self.children() {
match child {
StackChild::Spacing(kind) => {
layouter.layout_spacing(*kind);
deferred = None;
}
StackChild::Block(block) => {
// Transparently handle `h`.
if let (Axis::X, Some(h)) = (axis, block.to_packed::<HElem>()) {
layouter.layout_spacing(*h.amount());
deferred = None;
continue;
}
// Transparently handle `v`.
if let (Axis::Y, Some(v)) = (axis, block.to_packed::<VElem>()) {
layouter.layout_spacing(*v.amount());
deferred = None;
continue;
}
if let Some(kind) = deferred {
layouter.layout_spacing(kind);
}
layouter.layout_block(engine, block, styles)?;
deferred = spacing;
}
}
}
layouter.finish()
impl Show for Packed<StackElem> {
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::multi_layouter(self.clone(), layout_stack).pack())
}
}
@ -131,6 +88,55 @@ cast! {
v: Content => Self::Block(v),
}
/// Layout the stack.
#[typst_macros::time(span = elem.span())]
fn layout_stack(
elem: &Packed<StackElem>,
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
let mut layouter = StackLayouter::new(elem.span(), elem.dir(styles), regions, styles);
let axis = layouter.dir.axis();
// Spacing to insert before the next block.
let spacing = elem.spacing(styles);
let mut deferred = None;
for child in elem.children() {
match child {
StackChild::Spacing(kind) => {
layouter.layout_spacing(*kind);
deferred = None;
}
StackChild::Block(block) => {
// Transparently handle `h`.
if let (Axis::X, Some(h)) = (axis, block.to_packed::<HElem>()) {
layouter.layout_spacing(*h.amount());
deferred = None;
continue;
}
// Transparently handle `v`.
if let (Axis::Y, Some(v)) = (axis, block.to_packed::<VElem>()) {
layouter.layout_spacing(*v.amount());
deferred = None;
continue;
}
if let Some(kind) = deferred {
layouter.layout_spacing(kind);
}
layouter.layout_block(engine, block, styles)?;
deferred = spacing;
}
}
}
layouter.finish()
}
/// Performs stack layout.
struct StackLayouter<'a> {
/// The span to raise errors at during layout.
@ -231,7 +237,7 @@ impl<'a> StackLayouter<'a> {
self.finish_region()?;
}
// Block-axis alignment of the `AlignElement` is respected by stacks.
// Block-axis alignment of the `AlignElem` is respected by stacks.
let align = if let Some(align) = block.to_packed::<AlignElem>() {
align.alignment(styles)
} else if let Some(styled) = block.to_packed::<StyledElem>() {

View File

@ -1,9 +1,11 @@
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{elem, Content, Packed, Resolve, StyleChain};
use crate::foundations::{
elem, Content, NativeElement, Packed, Resolve, Show, StyleChain,
};
use crate::layout::{
Abs, Alignment, Angle, Axes, FixedAlignment, Frame, HAlignment, LayoutMultiple,
LayoutSingle, Length, Point, Ratio, Regions, Rel, Size, VAlignment,
Abs, Alignment, Angle, Axes, BlockElem, FixedAlignment, Frame, HAlignment, Length,
Point, Ratio, Region, Regions, Rel, Size, VAlignment,
};
/// Moves content without affecting layout.
@ -24,7 +26,7 @@ use crate::layout::{
/// )
/// ))
/// ```
#[elem(LayoutSingle)]
#[elem(Show)]
pub struct MoveElem {
/// The horizontal displacement of the content.
pub dx: Rel<Length>,
@ -37,23 +39,30 @@ pub struct MoveElem {
pub body: Content,
}
impl LayoutSingle for Packed<MoveElem> {
#[typst_macros::time(name = "move", span = self.span())]
fn layout(
&self,
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Frame> {
let pod = Regions::one(regions.base(), Axes::splat(false));
let mut frame = self.body().layout(engine, styles, pod)?.into_frame();
let delta = Axes::new(self.dx(styles), self.dy(styles)).resolve(styles);
let delta = delta.zip_map(regions.base(), Rel::relative_to);
frame.translate(delta.to_point());
Ok(frame)
impl Show for Packed<MoveElem> {
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::single_layouter(self.clone(), layout_move).pack())
}
}
/// Layout the moved content.
#[typst_macros::time(span = elem.span())]
fn layout_move(
elem: &Packed<MoveElem>,
engine: &mut Engine,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame> {
let mut frame = elem
.body()
.layout(engine, styles, region.into_regions())?
.into_frame();
let delta = Axes::new(elem.dx(styles), elem.dy(styles)).resolve(styles);
let delta = delta.zip_map(region.size, Rel::relative_to);
frame.translate(delta.to_point());
Ok(frame)
}
/// Rotates content without affecting layout.
///
/// Rotates an element by a given angle. The layout will act as if the element
@ -68,7 +77,7 @@ impl LayoutSingle for Packed<MoveElem> {
/// .map(i => rotate(24deg * i)[X]),
/// )
/// ```
#[elem(LayoutSingle)]
#[elem(Show)]
pub struct RotateElem {
/// The amount of rotation.
///
@ -115,38 +124,43 @@ pub struct RotateElem {
pub body: Content,
}
impl LayoutSingle for Packed<RotateElem> {
#[typst_macros::time(name = "rotate", span = self.span())]
fn layout(
&self,
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Frame> {
let angle = self.angle(styles);
let align = self.origin(styles).resolve(styles);
// Compute the new region's approximate size.
let size = regions
.base()
.to_point()
.transform_inf(Transform::rotate(angle))
.map(Abs::abs)
.to_size();
measure_and_layout(
engine,
regions.base(),
size,
styles,
self.body(),
Transform::rotate(angle),
align,
self.reflow(styles),
)
impl Show for Packed<RotateElem> {
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::single_layouter(self.clone(), layout_rotate).pack())
}
}
/// Layout the rotated content.
#[typst_macros::time(span = elem.span())]
fn layout_rotate(
elem: &Packed<RotateElem>,
engine: &mut Engine,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame> {
let angle = elem.angle(styles);
let align = elem.origin(styles).resolve(styles);
// Compute the new region's approximate size.
let size = region
.size
.to_point()
.transform_inf(Transform::rotate(angle))
.map(Abs::abs)
.to_size();
measure_and_layout(
engine,
region,
size,
styles,
elem.body(),
Transform::rotate(angle),
align,
elem.reflow(styles),
)
}
/// Scales content without affecting layout.
///
/// Lets you mirror content by specifying a negative scale on a single axis.
@ -157,7 +171,7 @@ impl LayoutSingle for Packed<RotateElem> {
/// #scale(x: -100%)[This is mirrored.]
/// #scale(x: -100%, reflow: true)[This is mirrored.]
/// ```
#[elem(LayoutSingle)]
#[elem(Show)]
pub struct ScaleElem {
/// The horizontal scaling factor.
///
@ -203,37 +217,39 @@ pub struct ScaleElem {
pub body: Content,
}
impl LayoutSingle for Packed<ScaleElem> {
#[typst_macros::time(name = "scale", span = self.span())]
fn layout(
&self,
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Frame> {
let sx = self.x(styles);
let sy = self.y(styles);
let align = self.origin(styles).resolve(styles);
// Compute the new region's approximate size.
let size = regions
.base()
.zip_map(Axes::new(sx, sy), |r, s| s.of(r))
.map(Abs::abs);
measure_and_layout(
engine,
regions.base(),
size,
styles,
self.body(),
Transform::scale(sx, sy),
align,
self.reflow(styles),
)
impl Show for Packed<ScaleElem> {
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::single_layouter(self.clone(), layout_scale).pack())
}
}
/// Layout the scaled content.
#[typst_macros::time(span = elem.span())]
fn layout_scale(
elem: &Packed<ScaleElem>,
engine: &mut Engine,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame> {
let sx = elem.x(styles);
let sy = elem.y(styles);
let align = elem.origin(styles).resolve(styles);
// Compute the new region's approximate size.
let size = region.size.zip_map(Axes::new(sx, sy), |r, s| s.of(r)).map(Abs::abs);
measure_and_layout(
engine,
region,
size,
styles,
elem.body(),
Transform::scale(sx, sy),
align,
elem.reflow(styles),
)
}
/// A scale-skew-translate transformation.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Transform {
@ -363,7 +379,7 @@ impl Default for Transform {
#[allow(clippy::too_many_arguments)]
fn measure_and_layout(
engine: &mut Engine,
base_size: Size,
region: Region,
size: Size,
styles: StyleChain,
body: &Content,
@ -371,41 +387,41 @@ fn measure_and_layout(
align: Axes<FixedAlignment>,
reflow: bool,
) -> SourceResult<Frame> {
if !reflow {
// Layout the body.
let pod = Regions::one(base_size, Axes::splat(false));
if reflow {
// Measure the size of the body.
let pod = Regions::one(size, Axes::splat(false));
let frame = body.measure(engine, styles, pod)?.into_frame();
// Actually perform the layout.
let pod = Regions::one(frame.size(), Axes::splat(true));
let mut frame = body.layout(engine, styles, pod)?.into_frame();
let Axes { x, y } = align.zip_map(frame.size(), FixedAlignment::position);
// Apply the transform.
// Compute the transform.
let ts = Transform::translate(x, y)
.pre_concat(transform)
.pre_concat(Transform::translate(-x, -y));
// Compute the bounding box and offset and wrap in a new frame.
let (offset, size) = compute_bounding_box(&frame, ts);
frame.transform(ts);
frame.translate(offset);
frame.set_size(size);
Ok(frame)
} else {
// Layout the body.
let mut frame = body.layout(engine, styles, region.into_regions())?.into_frame();
let Axes { x, y } = align.zip_map(frame.size(), FixedAlignment::position);
return Ok(frame);
// Compute the transform.
let ts = Transform::translate(x, y)
.pre_concat(transform)
.pre_concat(Transform::translate(-x, -y));
// Apply the transform.
frame.transform(ts);
Ok(frame)
}
// Measure the size of the body.
let pod = Regions::one(size, Axes::splat(false));
let frame = body.measure(engine, styles, pod)?.into_frame();
// Actually perform the layout.
let pod = Regions::one(frame.size(), Axes::splat(true));
let mut frame = body.layout(engine, styles, pod)?.into_frame();
let Axes { x, y } = align.zip_map(frame.size(), FixedAlignment::position);
// Apply the transform.
let ts = Transform::translate(x, y)
.pre_concat(transform)
.pre_concat(Transform::translate(-x, -y));
// Compute the bounding box and offset and wrap in a new frame.
let (offset, size) = compute_bounding_box(&frame, ts);
frame.transform(ts);
frame.translate(offset);
frame.set_size(size);
Ok(frame)
}
/// Computes the bounding box and offset of a transformed frame.

View File

@ -26,7 +26,7 @@
//! [evaluate]: eval::eval
//! [module]: foundations::Module
//! [content]: foundations::Content
//! [layouted]: layout::LayoutRoot
//! [layouted]: foundations::Content::layout_document
//! [document]: model::Document
//! [frame]: layout::Frame
@ -70,7 +70,7 @@ use crate::foundations::{
Array, Bytes, Content, Datetime, Dict, Module, Scope, StyleChain, Styles, Value,
};
use crate::introspection::{Introspector, Locator};
use crate::layout::{Alignment, Dir, LayoutRoot};
use crate::layout::{Alignment, Dir};
use crate::model::Document;
use crate::syntax::package::PackageSpec;
use crate::syntax::{FileId, Source, Span};
@ -139,7 +139,7 @@ fn typeset(
};
// Layout!
document = content.layout_root(&mut engine, styles)?;
document = content.layout_document(&mut engine, styles)?;
document.introspector.rebuild(&document.pages);
iter += 1;

View File

@ -12,7 +12,7 @@ use unicode_segmentation::UnicodeSegmentation;
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{Content, Packed, StyleChain};
use crate::layout::{Abs, Axes, BoxElem, Em, Frame, LayoutMultiple, Regions, Size};
use crate::layout::{Abs, Axes, BoxElem, Em, Frame, Regions, Size};
use crate::math::{
scaled_font_size, styled_char, EquationElem, FrameFragment, GlyphFragment,
LayoutMath, MathFragment, MathRun, MathSize, THICK,
@ -65,7 +65,7 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> {
pub fn new(
engine: &'v mut Engine<'b>,
styles: StyleChain<'a>,
regions: Regions,
base: Size,
font: &'a Font,
) -> Self {
let math_table = font.ttf().tables().math.unwrap();
@ -102,7 +102,7 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> {
Self {
engine,
regions: Regions::one(regions.base(), Axes::splat(false)),
regions: Regions::one(base, Axes::splat(false)),
font,
ttf: font.ttf(),
table: math_table,
@ -173,7 +173,7 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> {
) -> SourceResult<Frame> {
let local =
TextElem::set_size(TextSize(scaled_font_size(self, styles).into())).wrap();
boxed.layout(self.engine, styles.chain(&local), self.regions)
boxed.layout(self.engine, styles.chain(&local), self.regions.base())
}
/// Layout the given [`Content`] into a [`Frame`].

View File

@ -5,13 +5,14 @@ use unicode_math_class::MathClass;
use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
elem, Content, NativeElement, Packed, Resolve, ShowSet, Smart, StyleChain, Styles,
Synthesize,
elem, Content, NativeElement, Packed, Resolve, Show, ShowSet, Smart, StyleChain,
Styles, Synthesize,
};
use crate::introspection::{Count, Counter, CounterUpdate, Locatable};
use crate::layout::{
Abs, AlignElem, Alignment, Axes, BlockElem, Em, FixedAlignment, Fragment, Frame,
LayoutMultiple, OuterHAlignment, Point, Regions, Size, SpecificAlignment, VAlignment,
InlineElem, InlineItem, OuterHAlignment, Point, Regions, Size, SpecificAlignment,
VAlignment,
};
use crate::math::{
scaled_font_size, LayoutMath, MathContext, MathRunFrameBuilder, MathSize, MathVariant,
@ -48,14 +49,7 @@ use crate::World;
/// horizontally. For more details about math syntax, see the
/// [main math page]($category/math).
#[elem(
Locatable,
Synthesize,
ShowSet,
LayoutMultiple,
LayoutMath,
Count,
LocalName,
Refable,
Locatable, Synthesize, Show, ShowSet, LayoutMath, Count, LocalName, Refable,
Outlinable
)]
pub struct EquationElem {
@ -169,6 +163,16 @@ impl Synthesize for Packed<EquationElem> {
}
}
impl Show for Packed<EquationElem> {
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
if self.block(styles) {
Ok(BlockElem::multi_layouter(self.clone(), layout_equation_block).pack())
} else {
Ok(InlineElem::layouter(self.clone(), layout_equation_inline).pack())
}
}
}
impl ShowSet for Packed<EquationElem> {
fn show_set(&self, styles: StyleChain) -> Styles {
let mut out = Styles::new();
@ -187,178 +191,6 @@ impl ShowSet for Packed<EquationElem> {
}
}
/// Layouted items suitable for placing in a paragraph.
#[derive(Debug, Clone)]
pub enum MathParItem {
Space(Abs),
Frame(Frame),
}
impl Packed<EquationElem> {
pub fn layout_inline(
&self,
engine: &mut Engine<'_>,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Vec<MathParItem>> {
assert!(!self.block(styles));
let font = find_math_font(engine, styles, self.span())?;
let mut ctx = MathContext::new(engine, styles, regions, &font);
let run = ctx.layout_into_run(self, styles)?;
let mut items = if run.row_count() == 1 {
run.into_par_items()
} else {
vec![MathParItem::Frame(run.into_fragment(&ctx, styles).into_frame())]
};
// An empty equation should have a height, so we still create a frame
// (which is then resized in the loop).
if items.is_empty() {
items.push(MathParItem::Frame(Frame::soft(Size::zero())));
}
for item in &mut items {
let MathParItem::Frame(frame) = item else { continue };
let font_size = scaled_font_size(&ctx, styles);
let slack = ParElem::leading_in(styles) * 0.7;
let top_edge = TextElem::top_edge_in(styles).resolve(font_size, &font, None);
let bottom_edge =
-TextElem::bottom_edge_in(styles).resolve(font_size, &font, None);
let ascent = top_edge.max(frame.ascent() - slack);
let descent = bottom_edge.max(frame.descent() - slack);
frame.translate(Point::with_y(ascent - frame.baseline()));
frame.size_mut().y = ascent + descent;
}
Ok(items)
}
}
impl LayoutMultiple for Packed<EquationElem> {
#[typst_macros::time(name = "math.equation", span = self.span())]
fn layout(
&self,
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
assert!(self.block(styles));
let span = self.span();
let font = find_math_font(engine, styles, span)?;
let mut ctx = MathContext::new(engine, styles, regions, &font);
let full_equation_builder = ctx
.layout_into_run(self, styles)?
.multiline_frame_builder(&ctx, styles);
let width = full_equation_builder.size.x;
let equation_builders = if BlockElem::breakable_in(styles) {
let mut rows = full_equation_builder.frames.into_iter().peekable();
let mut equation_builders = vec![];
let mut last_first_pos = Point::zero();
for region in regions.iter() {
// Keep track of the position of the first row in this region,
// so that the offset can be reverted later.
let Some(&(_, first_pos)) = rows.peek() else { break };
last_first_pos = first_pos;
let mut frames = vec![];
let mut height = Abs::zero();
while let Some((sub, pos)) = rows.peek() {
let mut pos = *pos;
pos.y -= first_pos.y;
// Finish this region if the line doesn't fit. Only do it if
// we placed at least one line _or_ we still have non-last
// regions. Crucially, we don't want to infinitely create
// new regions which are too small.
if !region.y.fits(sub.height() + pos.y)
&& (!frames.is_empty() || !regions.in_last())
{
break;
}
let (sub, _) = rows.next().unwrap();
height = height.max(pos.y + sub.height());
frames.push((sub, pos));
}
equation_builders
.push(MathRunFrameBuilder { frames, size: Size::new(width, height) });
}
// Append remaining rows to the equation builder of the last region.
if let Some(equation_builder) = equation_builders.last_mut() {
equation_builder.frames.extend(rows.map(|(frame, mut pos)| {
pos.y -= last_first_pos.y;
(frame, pos)
}));
let height = equation_builder
.frames
.iter()
.map(|(frame, pos)| frame.height() + pos.y)
.max()
.unwrap_or(equation_builder.size.y);
equation_builder.size.y = height;
}
equation_builders
} else {
vec![full_equation_builder]
};
let Some(numbering) = (**self).numbering(styles) else {
let frames = equation_builders
.into_iter()
.map(MathRunFrameBuilder::build)
.collect();
return Ok(Fragment::frames(frames));
};
let pod = Regions::one(regions.base(), Axes::splat(false));
let number = Counter::of(EquationElem::elem())
.display_at_loc(engine, self.location().unwrap(), styles, numbering)?
.spanned(span)
.layout(engine, styles, pod)?
.into_frame();
static NUMBER_GUTTER: Em = Em::new(0.5);
let full_number_width = number.width() + NUMBER_GUTTER.resolve(styles);
let number_align = match self.number_align(styles) {
SpecificAlignment::H(h) => SpecificAlignment::Both(h, VAlignment::Horizon),
SpecificAlignment::V(v) => SpecificAlignment::Both(OuterHAlignment::End, v),
SpecificAlignment::Both(h, v) => SpecificAlignment::Both(h, v),
};
// Add equation numbers to each equation region.
let frames = equation_builders
.into_iter()
.map(|builder| {
add_equation_number(
builder,
number.clone(),
number_align.resolve(styles),
AlignElem::alignment_in(styles).resolve(styles).x,
regions.size.x,
full_number_width,
)
})
.collect();
Ok(Fragment::frames(frames))
}
}
impl Count for Packed<EquationElem> {
fn update(&self) -> Option<CounterUpdate> {
(self.block(StyleChain::default()) && self.numbering().is_some())
@ -429,6 +261,170 @@ impl LayoutMath for Packed<EquationElem> {
}
}
/// Layout an inline equation (in a paragraph).
#[typst_macros::time(span = elem.span())]
fn layout_equation_inline(
elem: &Packed<EquationElem>,
engine: &mut Engine<'_>,
styles: StyleChain,
region: Size,
) -> SourceResult<Vec<InlineItem>> {
assert!(!elem.block(styles));
let font = find_math_font(engine, styles, elem.span())?;
let mut ctx = MathContext::new(engine, styles, region, &font);
let run = ctx.layout_into_run(elem, styles)?;
let mut items = if run.row_count() == 1 {
run.into_par_items()
} else {
vec![InlineItem::Frame(run.into_fragment(&ctx, styles).into_frame())]
};
// An empty equation should have a height, so we still create a frame
// (which is then resized in the loop).
if items.is_empty() {
items.push(InlineItem::Frame(Frame::soft(Size::zero())));
}
for item in &mut items {
let InlineItem::Frame(frame) = item else { continue };
let font_size = scaled_font_size(&ctx, styles);
let slack = ParElem::leading_in(styles) * 0.7;
let top_edge = TextElem::top_edge_in(styles).resolve(font_size, &font, None);
let bottom_edge =
-TextElem::bottom_edge_in(styles).resolve(font_size, &font, None);
let ascent = top_edge.max(frame.ascent() - slack);
let descent = bottom_edge.max(frame.descent() - slack);
frame.translate(Point::with_y(ascent - frame.baseline()));
frame.size_mut().y = ascent + descent;
}
Ok(items)
}
/// Layout a block-level equation (in a flow).
#[typst_macros::time(span = elem.span())]
fn layout_equation_block(
elem: &Packed<EquationElem>,
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
assert!(elem.block(styles));
let span = elem.span();
let font = find_math_font(engine, styles, span)?;
let mut ctx = MathContext::new(engine, styles, regions.base(), &font);
let full_equation_builder = ctx
.layout_into_run(elem, styles)?
.multiline_frame_builder(&ctx, styles);
let width = full_equation_builder.size.x;
let equation_builders = if BlockElem::breakable_in(styles) {
let mut rows = full_equation_builder.frames.into_iter().peekable();
let mut equation_builders = vec![];
let mut last_first_pos = Point::zero();
for region in regions.iter() {
// Keep track of the position of the first row in this region,
// so that the offset can be reverted later.
let Some(&(_, first_pos)) = rows.peek() else { break };
last_first_pos = first_pos;
let mut frames = vec![];
let mut height = Abs::zero();
while let Some((sub, pos)) = rows.peek() {
let mut pos = *pos;
pos.y -= first_pos.y;
// Finish this region if the line doesn't fit. Only do it if
// we placed at least one line _or_ we still have non-last
// regions. Crucially, we don't want to infinitely create
// new regions which are too small.
if !region.y.fits(sub.height() + pos.y)
&& (!frames.is_empty() || !regions.in_last())
{
break;
}
let (sub, _) = rows.next().unwrap();
height = height.max(pos.y + sub.height());
frames.push((sub, pos));
}
equation_builders
.push(MathRunFrameBuilder { frames, size: Size::new(width, height) });
}
// Append remaining rows to the equation builder of the last region.
if let Some(equation_builder) = equation_builders.last_mut() {
equation_builder.frames.extend(rows.map(|(frame, mut pos)| {
pos.y -= last_first_pos.y;
(frame, pos)
}));
let height = equation_builder
.frames
.iter()
.map(|(frame, pos)| frame.height() + pos.y)
.max()
.unwrap_or(equation_builder.size.y);
equation_builder.size.y = height;
}
equation_builders
} else {
vec![full_equation_builder]
};
let Some(numbering) = (**elem).numbering(styles) else {
let frames = equation_builders
.into_iter()
.map(MathRunFrameBuilder::build)
.collect();
return Ok(Fragment::frames(frames));
};
let pod = Regions::one(regions.base(), Axes::splat(false));
let number = Counter::of(EquationElem::elem())
.display_at_loc(engine, elem.location().unwrap(), styles, numbering)?
.spanned(span)
.layout(engine, styles, pod)?
.into_frame();
static NUMBER_GUTTER: Em = Em::new(0.5);
let full_number_width = number.width() + NUMBER_GUTTER.resolve(styles);
let number_align = match elem.number_align(styles) {
SpecificAlignment::H(h) => SpecificAlignment::Both(h, VAlignment::Horizon),
SpecificAlignment::V(v) => SpecificAlignment::Both(OuterHAlignment::End, v),
SpecificAlignment::Both(h, v) => SpecificAlignment::Both(h, v),
};
// Add equation numbers to each equation region.
let frames = equation_builders
.into_iter()
.map(|builder| {
add_equation_number(
builder,
number.clone(),
number_align.resolve(styles),
AlignElem::alignment_in(styles).resolve(styles).x,
regions.size.x,
full_number_width,
)
})
.collect();
Ok(Fragment::frames(frames))
}
fn find_math_font(
engine: &mut Engine<'_>,
styles: StyleChain,

View File

@ -3,10 +3,10 @@ use std::iter::once;
use unicode_math_class::MathClass;
use crate::foundations::{Resolve, StyleChain};
use crate::layout::{Abs, AlignElem, Em, Frame, Point, Size};
use crate::layout::{Abs, AlignElem, Em, Frame, InlineItem, Point, Size};
use crate::math::{
alignments, scaled_font_size, spacing, EquationElem, FrameFragment, MathContext,
MathFragment, MathParItem, MathSize,
MathFragment, MathSize,
};
use crate::model::ParElem;
@ -251,7 +251,7 @@ impl MathRun {
frame
}
pub fn into_par_items(self) -> Vec<MathParItem> {
pub fn into_par_items(self) -> Vec<InlineItem> {
let mut items = vec![];
let mut x = Abs::zero();
@ -279,7 +279,7 @@ impl MathRun {
match fragment {
MathFragment::Space(width)
| MathFragment::Spacing(SpacingFragment { width, .. }) => {
items.push(MathParItem::Space(width));
items.push(InlineItem::Space(width, true));
continue;
}
_ => {}
@ -305,7 +305,7 @@ impl MathRun {
std::mem::replace(&mut frame, Frame::soft(Size::zero()));
finalize_frame(&mut frame_prev, x, ascent, descent);
items.push(MathParItem::Frame(frame_prev));
items.push(InlineItem::Frame(frame_prev));
empty = true;
x = Abs::zero();
@ -315,7 +315,7 @@ impl MathRun {
space_is_visible = true;
if let Some(f_next) = iter.peek() {
if !is_space(f_next) {
items.push(MathParItem::Space(Abs::zero()));
items.push(InlineItem::Space(Abs::zero(), true));
}
}
} else {
@ -327,7 +327,7 @@ impl MathRun {
// contribute width (if it had hidden content).
if !empty {
finalize_frame(&mut frame, x, ascent, descent);
items.push(MathParItem::Frame(frame));
items.push(InlineItem::Frame(frame));
}
items

View File

@ -29,8 +29,8 @@ use crate::foundations::{
};
use crate::introspection::{Introspector, Locatable, Location};
use crate::layout::{
BlockElem, Em, GridCell, GridChild, GridElem, GridItem, HElem, PadElem, Sizing,
TrackSizings, VElem,
BlockChild, BlockElem, Em, GridCell, GridChild, GridElem, GridItem, HElem, PadElem,
Sizing, TrackSizings, VElem,
};
use crate::model::{
CitationForm, CiteGroup, Destination, FootnoteElem, HeadingElem, LinkElem, ParElem,
@ -926,8 +926,10 @@ impl ElemRenderer<'_> {
match elem.display {
Some(Display::Block) => {
content =
BlockElem::new().with_body(Some(content)).pack().spanned(self.span);
content = BlockElem::new()
.with_body(Some(BlockChild::Content(content)))
.pack()
.spanned(self.span);
}
Some(Display::Indent) => {
content = PadElem::new(content).pack().spanned(self.span);

View File

@ -7,7 +7,7 @@ use crate::foundations::{
StyledElem, Value,
};
use crate::introspection::{Introspector, ManualPageCounter};
use crate::layout::{LayoutRoot, Page, PageElem};
use crate::layout::{Page, PageElem};
/// The root element of a document and its metadata.
///
@ -25,7 +25,7 @@ use crate::layout::{LayoutRoot, Page, PageElem};
///
/// Note that metadata set with this function is not rendered within the
/// document. Instead, it is embedded in the compiled PDF file.
#[elem(Construct, LayoutRoot)]
#[elem(Construct)]
pub struct DocumentElem {
/// The document's title. This is often rendered as the title of the
/// PDF viewer window.
@ -69,9 +69,10 @@ impl Construct for DocumentElem {
}
}
impl LayoutRoot for Packed<DocumentElem> {
impl Packed<DocumentElem> {
/// Layout this document.
#[typst_macros::time(name = "document", span = self.span())]
fn layout_root(
pub fn layout(
&self,
engine: &mut Engine,
styles: StyleChain,

View File

@ -6,11 +6,12 @@ use smallvec::{smallvec, SmallVec};
use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
cast, elem, scope, Array, Content, Context, Packed, Smart, StyleChain,
cast, elem, scope, Array, Content, Context, NativeElement, Packed, Show, Smart,
StyleChain,
};
use crate::layout::{
Alignment, Axes, BlockElem, Cell, CellGrid, Em, Fragment, GridLayouter, HAlignment,
LayoutMultiple, Length, Regions, Sizing, Spacing, VAlignment,
Length, Regions, Sizing, Spacing, VAlignment, VElem,
};
use crate::model::{Numbering, NumberingPattern, ParElem};
use crate::text::TextElem;
@ -71,7 +72,7 @@ use crate::text::TextElem;
/// Enumeration items can contain multiple paragraphs and other block-level
/// content. All content that is indented more than an item's marker becomes
/// part of that item.
#[elem(scope, title = "Numbered List", LayoutMultiple)]
#[elem(scope, title = "Numbered List", Show)]
pub struct EnumElem {
/// If this is `{false}`, the items are spaced apart with
/// [enum spacing]($enum.spacing). If it is `{true}`, they use normal
@ -212,85 +213,97 @@ impl EnumElem {
type EnumItem;
}
impl LayoutMultiple for Packed<EnumElem> {
#[typst_macros::time(name = "enum", span = self.span())]
fn layout(
&self,
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
let numbering = self.numbering(styles);
let indent = self.indent(styles);
let body_indent = self.body_indent(styles);
let gutter = if self.tight(styles) {
ParElem::leading_in(styles).into()
} else {
self.spacing(styles)
.unwrap_or_else(|| *BlockElem::below_in(styles).amount())
};
impl Show for Packed<EnumElem> {
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let mut realized = BlockElem::multi_layouter(self.clone(), layout_enum).pack();
let mut cells = vec![];
let mut number = self.start(styles);
let mut parents = EnumElem::parents_in(styles);
let full = self.full(styles);
// Horizontally align based on the given respective parameter.
// Vertically align to the top to avoid inheriting `horizon` or `bottom`
// alignment from the context and having the number be displaced in
// relation to the item it refers to.
let number_align = self.number_align(styles);
for item in self.children() {
number = item.number(styles).unwrap_or(number);
let context = Context::new(None, Some(styles));
let resolved = if full {
parents.push(number);
let content =
numbering.apply(engine, context.track(), &parents)?.display();
parents.pop();
content
} else {
match numbering {
Numbering::Pattern(pattern) => {
TextElem::packed(pattern.apply_kth(parents.len(), number))
}
other => other.apply(engine, context.track(), &[number])?.display(),
}
};
// Disable overhang as a workaround to end-aligned dots glitching
// and decreasing spacing between numbers and items.
let resolved =
resolved.aligned(number_align).styled(TextElem::set_overhang(false));
cells.push(Cell::from(Content::empty()));
cells.push(Cell::from(resolved));
cells.push(Cell::from(Content::empty()));
cells.push(Cell::from(
item.body().clone().styled(EnumElem::set_parents(smallvec![number])),
));
number = number.saturating_add(1);
if self.tight(styles) {
let leading = ParElem::leading_in(styles);
let spacing = VElem::list_attach(leading.into()).pack();
realized = spacing + realized;
}
let grid = CellGrid::new(
Axes::with_x(&[
Sizing::Rel(indent.into()),
Sizing::Auto,
Sizing::Rel(body_indent.into()),
Sizing::Auto,
]),
Axes::with_y(&[gutter.into()]),
cells,
);
let layouter = GridLayouter::new(&grid, regions, styles, self.span());
layouter.layout(engine)
Ok(realized)
}
}
/// Layout the enumeration.
#[typst_macros::time(span = elem.span())]
fn layout_enum(
elem: &Packed<EnumElem>,
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
let numbering = elem.numbering(styles);
let indent = elem.indent(styles);
let body_indent = elem.body_indent(styles);
let gutter = if elem.tight(styles) {
ParElem::leading_in(styles).into()
} else {
elem.spacing(styles)
.unwrap_or_else(|| *BlockElem::below_in(styles).amount())
};
let mut cells = vec![];
let mut number = elem.start(styles);
let mut parents = EnumElem::parents_in(styles);
let full = elem.full(styles);
// Horizontally align based on the given respective parameter.
// Vertically align to the top to avoid inheriting `horizon` or `bottom`
// alignment from the context and having the number be displaced in
// relation to the item it refers to.
let number_align = elem.number_align(styles);
for item in elem.children() {
number = item.number(styles).unwrap_or(number);
let context = Context::new(None, Some(styles));
let resolved = if full {
parents.push(number);
let content = numbering.apply(engine, context.track(), &parents)?.display();
parents.pop();
content
} else {
match numbering {
Numbering::Pattern(pattern) => {
TextElem::packed(pattern.apply_kth(parents.len(), number))
}
other => other.apply(engine, context.track(), &[number])?.display(),
}
};
// Disable overhang as a workaround to end-aligned dots glitching
// and decreasing spacing between numbers and items.
let resolved =
resolved.aligned(number_align).styled(TextElem::set_overhang(false));
cells.push(Cell::from(Content::empty()));
cells.push(Cell::from(resolved));
cells.push(Cell::from(Content::empty()));
cells.push(Cell::from(
item.body().clone().styled(EnumElem::set_parents(smallvec![number])),
));
number = number.saturating_add(1);
}
let grid = CellGrid::new(
Axes::with_x(&[
Sizing::Rel(indent.into()),
Sizing::Auto,
Sizing::Rel(body_indent.into()),
Sizing::Auto,
]),
Axes::with_y(&[gutter.into()]),
cells,
);
let layouter = GridLayouter::new(&grid, regions, styles, elem.span());
layouter.layout(engine)
}
/// An enumeration item.
#[elem(name = "item", title = "Numbered List Item")]
pub struct EnumItem {

View File

@ -14,8 +14,8 @@ use crate::introspection::{
Count, Counter, CounterKey, CounterUpdate, Locatable, Location,
};
use crate::layout::{
AlignElem, Alignment, BlockElem, Em, HAlignment, Length, OuterVAlignment, PlaceElem,
VAlignment, VElem,
AlignElem, Alignment, BlockChild, BlockElem, Em, HAlignment, Length, OuterVAlignment,
PlaceElem, VAlignment, VElem,
};
use crate::model::{Numbering, NumberingPattern, Outlinable, Refable, Supplement};
use crate::text::{Lang, Region, TextElem};
@ -317,7 +317,10 @@ impl Show for Packed<FigureElem> {
}
// Wrap the contents in a block.
realized = BlockElem::new().with_body(Some(realized)).pack().spanned(self.span());
realized = BlockElem::new()
.with_body(Some(BlockChild::Content(realized)))
.pack()
.spanned(self.span());
// Wrap in a float.
if let Some(align) = self.placement(styles) {

View File

@ -8,7 +8,7 @@ use crate::foundations::{
};
use crate::introspection::{Count, Counter, CounterUpdate, Locatable};
use crate::layout::{
Abs, Axes, BlockElem, Em, HElem, LayoutMultiple, Length, Regions, VElem,
Abs, Axes, BlockChild, BlockElem, Em, HElem, Length, Regions, VElem,
};
use crate::model::{Numbering, Outlinable, ParElem, Refable, Supplement};
use crate::text::{FontWeight, LocalName, SpaceElem, TextElem, TextSize};
@ -248,7 +248,10 @@ impl Show for Packed<HeadingElem> {
realized = realized.styled(ParElem::set_hanging_indent(indent.into()));
}
Ok(BlockElem::new().with_body(Some(realized)).pack().spanned(span))
Ok(BlockElem::new()
.with_body(Some(BlockChild::Content(realized)))
.pack()
.spanned(span))
}
}

View File

@ -3,12 +3,12 @@ use comemo::Track;
use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
cast, elem, scope, Array, Content, Context, Depth, Func, Packed, Smart, StyleChain,
Value,
cast, elem, scope, Array, Content, Context, Depth, Func, NativeElement, Packed, Show,
Smart, StyleChain, Value,
};
use crate::layout::{
Axes, BlockElem, Cell, CellGrid, Em, Fragment, GridLayouter, HAlignment,
LayoutMultiple, Length, Regions, Sizing, Spacing, VAlignment,
Axes, BlockElem, Cell, CellGrid, Em, Fragment, GridLayouter, HAlignment, Length,
Regions, Sizing, Spacing, VAlignment, VElem,
};
use crate::model::ParElem;
use crate::text::TextElem;
@ -44,7 +44,7 @@ use crate::text::TextElem;
/// followed by a space to create a list item. A list item can contain multiple
/// paragraphs and other block-level content. All content that is indented
/// more than an item's marker becomes part of that item.
#[elem(scope, title = "Bullet List", LayoutMultiple)]
#[elem(scope, title = "Bullet List", Show)]
pub struct ListElem {
/// If this is `{false}`, the items are spaced apart with
/// [list spacing]($list.spacing). If it is `{true}`, they use normal
@ -137,56 +137,67 @@ impl ListElem {
type ListItem;
}
impl LayoutMultiple for Packed<ListElem> {
#[typst_macros::time(name = "list", span = self.span())]
fn layout(
&self,
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
let indent = self.indent(styles);
let body_indent = self.body_indent(styles);
let gutter = if self.tight(styles) {
ParElem::leading_in(styles).into()
} else {
self.spacing(styles)
.unwrap_or_else(|| *BlockElem::below_in(styles).amount())
};
impl Show for Packed<ListElem> {
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let mut realized = BlockElem::multi_layouter(self.clone(), layout_list).pack();
let Depth(depth) = ListElem::depth_in(styles);
let marker = self
.marker(styles)
.resolve(engine, styles, depth)?
// avoid '#set align' interference with the list
.aligned(HAlignment::Start + VAlignment::Top);
let mut cells = vec![];
for item in self.children() {
cells.push(Cell::from(Content::empty()));
cells.push(Cell::from(marker.clone()));
cells.push(Cell::from(Content::empty()));
cells.push(Cell::from(
item.body().clone().styled(ListElem::set_depth(Depth(1))),
));
if self.tight(styles) {
let leading = ParElem::leading_in(styles);
let spacing = VElem::list_attach(leading.into()).pack();
realized = spacing + realized;
}
let grid = CellGrid::new(
Axes::with_x(&[
Sizing::Rel(indent.into()),
Sizing::Auto,
Sizing::Rel(body_indent.into()),
Sizing::Auto,
]),
Axes::with_y(&[gutter.into()]),
cells,
);
let layouter = GridLayouter::new(&grid, regions, styles, self.span());
layouter.layout(engine)
Ok(realized)
}
}
/// Layout the list.
#[typst_macros::time(span = elem.span())]
fn layout_list(
elem: &Packed<ListElem>,
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
let indent = elem.indent(styles);
let body_indent = elem.body_indent(styles);
let gutter = if elem.tight(styles) {
ParElem::leading_in(styles).into()
} else {
elem.spacing(styles)
.unwrap_or_else(|| *BlockElem::below_in(styles).amount())
};
let Depth(depth) = ListElem::depth_in(styles);
let marker = elem
.marker(styles)
.resolve(engine, styles, depth)?
// avoid '#set align' interference with the list
.aligned(HAlignment::Start + VAlignment::Top);
let mut cells = vec![];
for item in elem.children() {
cells.push(Cell::from(Content::empty()));
cells.push(Cell::from(marker.clone()));
cells.push(Cell::from(Content::empty()));
cells.push(Cell::from(item.body().clone().styled(ListElem::set_depth(Depth(1)))));
}
let grid = CellGrid::new(
Axes::with_x(&[
Sizing::Rel(indent.into()),
Sizing::Auto,
Sizing::Rel(body_indent.into()),
Sizing::Auto,
]),
Axes::with_y(&[gutter.into()]),
cells,
);
let layouter = GridLayouter::new(&grid, regions, styles, elem.span());
layouter.layout(engine)
}
/// A bullet list item.
#[elem(name = "item", title = "Bullet List Item")]
pub struct ListItem {

View File

@ -4,7 +4,9 @@ use crate::foundations::{
cast, elem, Content, Depth, Label, NativeElement, Packed, Show, ShowSet, Smart,
StyleChain, Styles,
};
use crate::layout::{Alignment, BlockElem, Em, HElem, PadElem, Spacing, VElem};
use crate::layout::{
Alignment, BlockChild, BlockElem, Em, HElem, PadElem, Spacing, VElem,
};
use crate::model::{CitationForm, CiteElem};
use crate::text::{SmartQuoteElem, SmartQuotes, SpaceElem, TextElem};
@ -181,8 +183,10 @@ impl Show for Packed<QuoteElem> {
}
if block {
realized =
BlockElem::new().with_body(Some(realized)).pack().spanned(self.span());
realized = BlockElem::new()
.with_body(Some(BlockChild::Content(realized)))
.pack()
.spanned(self.span());
if let Some(attribution) = self.attribution(styles).as_ref() {
let mut seq = vec![TextElem::packed('—'), SpaceElem::new().pack()];

View File

@ -6,11 +6,11 @@ use ecow::{eco_format, EcoString};
use crate::diag::{bail, SourceResult, StrResult, Trace, Tracepoint};
use crate::engine::Engine;
use crate::foundations::{
cast, elem, scope, Content, Fold, Packed, Show, Smart, StyleChain,
cast, elem, scope, Content, Fold, NativeElement, Packed, Show, Smart, StyleChain,
};
use crate::layout::{
show_grid_cell, Abs, Alignment, Axes, Cell, CellGrid, Celled, Dir, Fragment,
GridCell, GridFooter, GridHLine, GridHeader, GridLayouter, GridVLine, LayoutMultiple,
show_grid_cell, Abs, Alignment, Axes, BlockElem, Cell, CellGrid, Celled, Dir,
Fragment, GridCell, GridFooter, GridHLine, GridHeader, GridLayouter, GridVLine,
Length, LinePosition, OuterHAlignment, OuterVAlignment, Regions, Rel, ResolvableCell,
ResolvableGridChild, ResolvableGridItem, Sides, TrackSizings,
};
@ -120,7 +120,7 @@ use crate::visualize::{Paint, Stroke};
/// [Robert], b, a, b,
/// )
/// ```
#[elem(scope, LayoutMultiple, LocalName, Figurable)]
#[elem(scope, Show, LocalName, Figurable)]
pub struct TableElem {
/// The column sizes. See the [grid documentation]($grid) for more
/// information on track sizing.
@ -260,62 +260,65 @@ impl TableElem {
type TableFooter;
}
impl LayoutMultiple for Packed<TableElem> {
#[typst_macros::time(name = "table", span = self.span())]
fn layout(
&self,
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
let inset = self.inset(styles);
let align = self.align(styles);
let columns = self.columns(styles);
let rows = self.rows(styles);
let column_gutter = self.column_gutter(styles);
let row_gutter = self.row_gutter(styles);
let fill = self.fill(styles);
let stroke = self.stroke(styles);
let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice());
let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice());
// Use trace to link back to the table when a specific cell errors
let tracepoint = || Tracepoint::Call(Some(eco_format!("table")));
let resolve_item = |item: &TableItem| item.to_resolvable(styles);
let children = self.children().iter().map(|child| match child {
TableChild::Header(header) => ResolvableGridChild::Header {
repeat: header.repeat(styles),
span: header.span(),
items: header.children().iter().map(resolve_item),
},
TableChild::Footer(footer) => ResolvableGridChild::Footer {
repeat: footer.repeat(styles),
span: footer.span(),
items: footer.children().iter().map(resolve_item),
},
TableChild::Item(item) => {
ResolvableGridChild::Item(item.to_resolvable(styles))
}
});
let grid = CellGrid::resolve(
tracks,
gutter,
children,
fill,
align,
&inset,
&stroke,
engine,
styles,
self.span(),
)
.trace(engine.world, tracepoint, self.span())?;
let layouter = GridLayouter::new(&grid, regions, styles, self.span());
layouter.layout(engine)
impl Show for Packed<TableElem> {
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::multi_layouter(self.clone(), layout_table).pack())
}
}
/// Layout the table.
#[typst_macros::time(span = elem.span())]
fn layout_table(
elem: &Packed<TableElem>,
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
let inset = elem.inset(styles);
let align = elem.align(styles);
let columns = elem.columns(styles);
let rows = elem.rows(styles);
let column_gutter = elem.column_gutter(styles);
let row_gutter = elem.row_gutter(styles);
let fill = elem.fill(styles);
let stroke = elem.stroke(styles);
let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice());
let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice());
// Use trace to link back to the table when a specific cell errors
let tracepoint = || Tracepoint::Call(Some(eco_format!("table")));
let resolve_item = |item: &TableItem| item.to_resolvable(styles);
let children = elem.children().iter().map(|child| match child {
TableChild::Header(header) => ResolvableGridChild::Header {
repeat: header.repeat(styles),
span: header.span(),
items: header.children().iter().map(resolve_item),
},
TableChild::Footer(footer) => ResolvableGridChild::Footer {
repeat: footer.repeat(styles),
span: footer.span(),
items: footer.children().iter().map(resolve_item),
},
TableChild::Item(item) => ResolvableGridChild::Item(item.to_resolvable(styles)),
});
let grid = CellGrid::resolve(
tracks,
gutter,
children,
fill,
align,
&inset,
&stroke,
engine,
styles,
elem.span(),
)
.trace(engine.world, tracepoint, elem.span())?;
let layouter = GridLayouter::new(&grid, regions, styles, elem.span());
layouter.layout(engine)
}
impl LocalName for Packed<TableElem> {
const KEY: &'static str = "table";
}

View File

@ -1,11 +1,10 @@
use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
cast, elem, scope, Array, Content, NativeElement, Packed, Smart, StyleChain,
cast, elem, scope, Array, Content, NativeElement, Packed, Show, Smart, StyleChain,
};
use crate::layout::{
BlockElem, Dir, Em, Fragment, HElem, LayoutMultiple, Length, Regions, Sides, Spacing,
StackChild, StackElem,
BlockElem, Dir, Em, HElem, Length, Sides, Spacing, StackChild, StackElem, VElem,
};
use crate::model::ParElem;
use crate::text::TextElem;
@ -27,7 +26,7 @@ use crate::utils::Numeric;
/// # Syntax
/// This function also has dedicated syntax: Starting a line with a slash,
/// followed by a term, a colon and a description creates a term list item.
#[elem(scope, title = "Term List", LayoutMultiple)]
#[elem(scope, title = "Term List", Show)]
pub struct TermsElem {
/// If this is `{false}`, the items are spaced apart with
/// [term list spacing]($terms.spacing). If it is `{true}`, they use normal
@ -109,14 +108,8 @@ impl TermsElem {
type TermItem;
}
impl LayoutMultiple for Packed<TermsElem> {
#[typst_macros::time(name = "terms", span = self.span())]
fn layout(
&self,
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
impl Show for Packed<TermsElem> {
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let separator = self.separator(styles);
let indent = self.indent(styles);
let hanging_indent = self.hanging_indent(styles);
@ -148,11 +141,18 @@ impl LayoutMultiple for Packed<TermsElem> {
padding.right = pad.into();
}
StackElem::new(children)
let mut realized = StackElem::new(children)
.with_spacing(Some(gutter))
.pack()
.padded(padding)
.layout(engine, styles, regions)
.padded(padding);
if self.tight(styles) {
let leading = ParElem::leading_in(styles);
let spacing = VElem::list_attach(leading.into()).pack();
realized = spacing + realized;
}
Ok(realized)
}
}

View File

@ -23,8 +23,8 @@ use crate::foundations::{
};
use crate::introspection::TagElem;
use crate::layout::{
AlignElem, BlockElem, BoxElem, ColbreakElem, FlowElem, FlushElem, HElem,
LayoutMultiple, LayoutSingle, PageElem, PagebreakElem, Parity, PlaceElem, VElem,
AlignElem, BlockElem, BoxElem, ColbreakElem, FlowElem, FlushElem, HElem, InlineElem,
PageElem, PagebreakElem, Parity, PlaceElem, VElem,
};
use crate::math::{EquationElem, LayoutMath};
use crate::model::{
@ -377,8 +377,14 @@ impl<'a> FlowBuilder<'a> {
let last_was_parbreak = self.1;
self.1 = false;
if content.is::<VElem>()
|| content.is::<ColbreakElem>()
if let Some(elem) = content.to_packed::<VElem>() {
if !elem.attach(styles) || !last_was_parbreak {
self.0.push(content, styles);
}
return true;
}
if content.is::<ColbreakElem>()
|| content.is::<TagElem>()
|| content.is::<PlaceElem>()
|| content.is::<FlushElem>()
@ -387,35 +393,17 @@ impl<'a> FlowBuilder<'a> {
return true;
}
if content.can::<dyn LayoutSingle>()
|| content.can::<dyn LayoutMultiple>()
|| content.is::<ParElem>()
{
let is_tight_list = if let Some(elem) = content.to_packed::<ListElem>() {
elem.tight(styles)
} else if let Some(elem) = content.to_packed::<EnumElem>() {
elem.tight(styles)
} else if let Some(elem) = content.to_packed::<TermsElem>() {
elem.tight(styles)
} else {
false
};
if !last_was_parbreak && is_tight_list {
let leading = ParElem::leading_in(styles);
let spacing = VElem::list_attach(leading.into());
self.0.push(arenas.store(spacing.pack()), styles);
}
let (above, below) = if let Some(block) = content.to_packed::<BlockElem>() {
(block.above(styles), block.below(styles))
} else {
(BlockElem::above_in(styles), BlockElem::below_in(styles))
};
self.0.push(arenas.store(above.pack()), styles);
if let Some(elem) = content.to_packed::<BlockElem>() {
self.0.push(arenas.store(elem.above(styles).pack()), styles);
self.0.push(content, styles);
self.0.push(arenas.store(below.pack()), styles);
self.0.push(arenas.store(elem.below(styles).pack()), styles);
return true;
}
if content.is::<ParElem>() {
self.0.push(arenas.store(BlockElem::above_in(styles).pack()), styles);
self.0.push(content, styles);
self.0.push(arenas.store(BlockElem::below_in(styles).pack()), styles);
return true;
}
@ -452,9 +440,7 @@ impl<'a> ParBuilder<'a> {
|| content.is::<HElem>()
|| content.is::<LinebreakElem>()
|| content.is::<SmartQuoteElem>()
|| content
.to_packed::<EquationElem>()
.is_some_and(|elem| !elem.block(styles))
|| content.is::<InlineElem>()
|| content.is::<BoxElem>()
{
self.0.push(content, styles);

View File

@ -423,7 +423,7 @@ pub(crate) fn decorate(
{
let (top, bottom) = determine_edges(text, *top_edge, *bottom_edge);
let size = Size::new(width + 2.0 * deco.extent, top - bottom);
let rects = styled_rect(size, *radius, fill.clone(), stroke.clone());
let rects = styled_rect(size, radius, fill.clone(), stroke);
let origin = Point::new(pos.x - deco.extent, pos.y - top - shift);
frame.prepend_multiple(
rects

View File

@ -16,7 +16,7 @@ use crate::foundations::{
cast, elem, scope, Args, Array, Bytes, Content, Fold, NativeElement, Packed,
PlainText, Show, ShowSet, Smart, StyleChain, Styles, Synthesize, Value,
};
use crate::layout::{BlockElem, Em, HAlignment};
use crate::layout::{BlockChild, BlockElem, Em, HAlignment};
use crate::model::{Figurable, ParElem};
use crate::syntax::{split_newlines, LinkedNode, Span, Spanned};
use crate::text::{
@ -444,8 +444,10 @@ impl Show for Packed<RawElem> {
if self.block(styles) {
// Align the text before inserting it into the block.
realized = realized.aligned(self.align(styles).into());
realized =
BlockElem::new().with_body(Some(realized)).pack().spanned(self.span());
realized = BlockElem::new()
.with_body(Some(BlockChild::Content(realized)))
.pack()
.spanned(self.span());
}
Ok(realized)

View File

@ -16,12 +16,12 @@ use ecow::EcoString;
use crate::diag::{bail, At, SourceResult, StrResult};
use crate::engine::Engine;
use crate::foundations::{
cast, elem, func, scope, Bytes, Cast, Content, NativeElement, Packed, Resolve, Smart,
cast, elem, func, scope, Bytes, Cast, Content, NativeElement, Packed, Show, Smart,
StyleChain,
};
use crate::layout::{
Abs, Axes, FixedAlignment, Frame, FrameItem, LayoutSingle, Length, Point, Regions,
Rel, Size,
Abs, Axes, BlockElem, FixedAlignment, Frame, FrameItem, Length, Point, Region, Rel,
Size,
};
use crate::loading::Readable;
use crate::model::Figurable;
@ -51,7 +51,7 @@ use crate::World;
/// ```
///
/// [gh-svg]: https://github.com/typst/typst/issues?q=is%3Aopen+is%3Aissue+label%3Asvg
#[elem(scope, LayoutSingle, LocalName, Figurable)]
#[elem(scope, Show, LocalName, Figurable)]
pub struct ImageElem {
/// Path to an image file.
#[required]
@ -154,112 +154,12 @@ impl ImageElem {
}
}
impl LayoutSingle for Packed<ImageElem> {
#[typst_macros::time(name = "image", span = self.span())]
fn layout(
&self,
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Frame> {
// Take the format that was explicitly defined, or parse the extension,
// or try to detect the format.
let data = self.data();
let format = match self.format(styles) {
Smart::Custom(v) => v,
Smart::Auto => {
let ext = std::path::Path::new(self.path().as_str())
.extension()
.and_then(OsStr::to_str)
.unwrap_or_default()
.to_lowercase();
match ext.as_str() {
"png" => ImageFormat::Raster(RasterFormat::Png),
"jpg" | "jpeg" => ImageFormat::Raster(RasterFormat::Jpg),
"gif" => ImageFormat::Raster(RasterFormat::Gif),
"svg" | "svgz" => ImageFormat::Vector(VectorFormat::Svg),
_ => match &data {
Readable::Str(_) => ImageFormat::Vector(VectorFormat::Svg),
Readable::Bytes(bytes) => match RasterFormat::detect(bytes) {
Some(f) => ImageFormat::Raster(f),
None => bail!(self.span(), "unknown image format"),
},
},
}
}
};
let image = Image::with_fonts(
data.clone().into(),
format,
self.alt(styles),
engine.world,
&families(styles).map(|s| s.into()).collect::<Vec<_>>(),
)
.at(self.span())?;
let sizing = Axes::new(self.width(styles), self.height(styles));
let region = sizing
.zip_map(regions.base(), |s, r| s.map(|v| v.resolve(styles).relative_to(r)))
.unwrap_or(regions.base());
let expand = sizing.as_ref().map(Smart::is_custom) | regions.expand;
let region_ratio = region.x / region.y;
// Find out whether the image is wider or taller than the target size.
let pxw = image.width();
let pxh = image.height();
let px_ratio = pxw / pxh;
let wide = px_ratio > region_ratio;
// The space into which the image will be placed according to its fit.
let target = if expand.x && expand.y {
// If both width and height are forced, take them.
region
} else if expand.x {
// If just width is forced, take it.
Size::new(region.x, region.y.min(region.x / px_ratio))
} else if expand.y {
// If just height is forced, take it.
Size::new(region.x.min(region.y * px_ratio), region.y)
} else {
// If neither is forced, take the natural image size at the image's
// DPI bounded by the available space.
let dpi = image.dpi().unwrap_or(Image::DEFAULT_DPI);
let natural = Axes::new(pxw, pxh).map(|v| Abs::inches(v / dpi));
Size::new(
natural.x.min(region.x).min(region.y * px_ratio),
natural.y.min(region.y).min(region.x / px_ratio),
)
};
// Compute the actual size of the fitted image.
let fit = self.fit(styles);
let fitted = match fit {
ImageFit::Cover | ImageFit::Contain => {
if wide == (fit == ImageFit::Contain) {
Size::new(target.x, target.x / px_ratio)
} else {
Size::new(target.y * px_ratio, target.y)
}
}
ImageFit::Stretch => target,
};
// First, place the image in a frame of exactly its size and then resize
// the frame to the target size, center aligning the image in the
// process.
let mut frame = Frame::soft(fitted);
frame.push(Point::zero(), FrameItem::Image(image, fitted, self.span()));
frame.resize(target, Axes::splat(FixedAlignment::Center));
// Create a clipping group if only part of the image should be visible.
if fit == ImageFit::Cover && !target.fits(fitted) {
frame.clip(Path::rect(frame.size()));
}
Ok(frame)
impl Show for Packed<ImageElem> {
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::single_layouter(self.clone(), layout_image)
.with_width(self.width(styles))
.with_height(self.height(styles))
.pack())
}
}
@ -269,6 +169,117 @@ impl LocalName for Packed<ImageElem> {
impl Figurable for Packed<ImageElem> {}
/// Layout the image.
#[typst_macros::time(span = elem.span())]
fn layout_image(
elem: &Packed<ImageElem>,
engine: &mut Engine,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame> {
let span = elem.span();
// Take the format that was explicitly defined, or parse the extension,
// or try to detect the format.
let data = elem.data();
let format = match elem.format(styles) {
Smart::Custom(v) => v,
Smart::Auto => determine_format(elem.path().as_str(), data).at(span)?,
};
// Construct the image itself.
let image = Image::with_fonts(
data.clone().into(),
format,
elem.alt(styles),
engine.world,
&families(styles).map(|s| s.into()).collect::<Vec<_>>(),
)
.at(span)?;
// Determine the image's pixel aspect ratio.
let pxw = image.width();
let pxh = image.height();
let px_ratio = pxw / pxh;
// Determine the region's aspect ratio.
let region_ratio = region.size.x / region.size.y;
// Find out whether the image is wider or taller than the region.
let wide = px_ratio > region_ratio;
// The space into which the image will be placed according to its fit.
let target = if region.expand.x && region.expand.y {
// If both width and height are forced, take them.
region.size
} else if region.expand.x {
// If just width is forced, take it.
Size::new(region.size.x, region.size.y.min(region.size.x / px_ratio))
} else if region.expand.y {
// If just height is forced, take it.
Size::new(region.size.x.min(region.size.y * px_ratio), region.size.y)
} else {
// If neither is forced, take the natural image size at the image's
// DPI bounded by the available space.
let dpi = image.dpi().unwrap_or(Image::DEFAULT_DPI);
let natural = Axes::new(pxw, pxh).map(|v| Abs::inches(v / dpi));
Size::new(
natural.x.min(region.size.x).min(region.size.y * px_ratio),
natural.y.min(region.size.y).min(region.size.x / px_ratio),
)
};
// Compute the actual size of the fitted image.
let fit = elem.fit(styles);
let fitted = match fit {
ImageFit::Cover | ImageFit::Contain => {
if wide == (fit == ImageFit::Contain) {
Size::new(target.x, target.x / px_ratio)
} else {
Size::new(target.y * px_ratio, target.y)
}
}
ImageFit::Stretch => target,
};
// First, place the image in a frame of exactly its size and then resize
// the frame to the target size, center aligning the image in the
// process.
let mut frame = Frame::soft(fitted);
frame.push(Point::zero(), FrameItem::Image(image, fitted, span));
frame.resize(target, Axes::splat(FixedAlignment::Center));
// Create a clipping group if only part of the image should be visible.
if fit == ImageFit::Cover && !target.fits(fitted) {
frame.clip(Path::rect(frame.size()));
}
Ok(frame)
}
/// Determine the image format based on path and data.
fn determine_format(path: &str, data: &Readable) -> StrResult<ImageFormat> {
let ext = std::path::Path::new(path)
.extension()
.and_then(OsStr::to_str)
.unwrap_or_default()
.to_lowercase();
Ok(match ext.as_str() {
"png" => ImageFormat::Raster(RasterFormat::Png),
"jpg" | "jpeg" => ImageFormat::Raster(RasterFormat::Jpg),
"gif" => ImageFormat::Raster(RasterFormat::Gif),
"svg" | "svgz" => ImageFormat::Vector(VectorFormat::Svg),
_ => match &data {
Readable::Str(_) => ImageFormat::Vector(VectorFormat::Svg),
Readable::Bytes(bytes) => match RasterFormat::detect(bytes) {
Some(f) => ImageFormat::Raster(f),
None => bail!("unknown image format"),
},
},
})
}
/// How an image should adjust itself to a given area,
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum ImageFit {

View File

@ -1,8 +1,8 @@
use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
use crate::foundations::{elem, Packed, StyleChain};
use crate::foundations::{elem, Content, NativeElement, Packed, Show, StyleChain};
use crate::layout::{
Abs, Angle, Axes, Frame, FrameItem, LayoutSingle, Length, Regions, Rel, Size,
Abs, Angle, Axes, BlockElem, Frame, FrameItem, Length, Region, Rel, Size,
};
use crate::utils::Numeric;
use crate::visualize::{Geometry, Stroke};
@ -20,7 +20,7 @@ use crate::visualize::{Geometry, Stroke};
/// stroke: 2pt + maroon,
/// )
/// ```
#[elem(LayoutSingle)]
#[elem(Show)]
pub struct LineElem {
/// The start point of the line.
///
@ -58,37 +58,39 @@ pub struct LineElem {
pub stroke: Stroke,
}
impl LayoutSingle for Packed<LineElem> {
#[typst_macros::time(name = "line", span = self.span())]
fn layout(
&self,
_: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Frame> {
let resolve =
|axes: Axes<Rel<Abs>>| axes.zip_map(regions.base(), Rel::relative_to);
let start = resolve(self.start(styles));
let delta =
self.end(styles).map(|end| resolve(end) - start).unwrap_or_else(|| {
let length = self.length(styles);
let angle = self.angle(styles);
let x = angle.cos() * length;
let y = angle.sin() * length;
resolve(Axes::new(x, y))
});
let stroke = self.stroke(styles).unwrap_or_default();
let size = start.max(start + delta).max(Size::zero());
let target = regions.expand.select(regions.size, size);
if !target.is_finite() {
bail!(self.span(), "cannot create line with infinite length");
}
let mut frame = Frame::soft(target);
let shape = Geometry::Line(delta.to_point()).stroked(stroke);
frame.push(start.to_point(), FrameItem::Shape(shape, self.span()));
Ok(frame)
impl Show for Packed<LineElem> {
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::single_layouter(self.clone(), layout_line).pack())
}
}
/// Layout the line.
#[typst_macros::time(span = elem.span())]
fn layout_line(
elem: &Packed<LineElem>,
_: &mut Engine,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame> {
let resolve = |axes: Axes<Rel<Abs>>| axes.zip_map(region.size, Rel::relative_to);
let start = resolve(elem.start(styles));
let delta = elem.end(styles).map(|end| resolve(end) - start).unwrap_or_else(|| {
let length = elem.length(styles);
let angle = elem.angle(styles);
let x = angle.cos() * length;
let y = angle.sin() * length;
resolve(Axes::new(x, y))
});
let stroke = elem.stroke(styles).unwrap_or_default();
let size = start.max(start + delta).max(Size::zero());
if !size.is_finite() {
bail!(elem.span(), "cannot create line with infinite length");
}
let mut frame = Frame::soft(size);
let shape = Geometry::Line(delta.to_point()).stroked(stroke);
frame.push(start.to_point(), FrameItem::Shape(shape, elem.span()));
Ok(frame)
}

View File

@ -3,10 +3,11 @@ use kurbo::{CubicBez, ParamCurveExtrema};
use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
array, cast, elem, Array, Packed, Reflect, Resolve, Smart, StyleChain,
array, cast, elem, Array, Content, NativeElement, Packed, Reflect, Resolve, Show,
Smart, StyleChain,
};
use crate::layout::{
Abs, Axes, Frame, FrameItem, LayoutSingle, Length, Point, Regions, Rel, Size,
Abs, Axes, BlockElem, Frame, FrameItem, Length, Point, Region, Rel, Size,
};
use crate::visualize::{FixedStroke, Geometry, Paint, Shape, Stroke};
@ -25,7 +26,7 @@ use PathVertex::{AllControlPoints, MirroredControlPoint, Vertex};
/// ((50%, 0pt), (40pt, 0pt)),
/// )
/// ```
#[elem(LayoutSingle)]
#[elem(Show)]
pub struct PathElem {
/// How to fill the path.
///
@ -69,88 +70,91 @@ pub struct PathElem {
pub vertices: Vec<PathVertex>,
}
impl LayoutSingle for Packed<PathElem> {
#[typst_macros::time(name = "path", span = self.span())]
fn layout(
&self,
_: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Frame> {
let resolve = |axes: Axes<Rel<Length>>| {
axes.resolve(styles)
.zip_map(regions.base(), Rel::relative_to)
.to_point()
};
let vertices = self.vertices();
let points: Vec<Point> = vertices.iter().map(|c| resolve(c.vertex())).collect();
let mut size = Size::zero();
if points.is_empty() {
return Ok(Frame::soft(size));
}
// Only create a path if there are more than zero points.
// Construct a closed path given all points.
let mut path = Path::new();
path.move_to(points[0]);
let mut add_cubic =
|from_point: Point, to_point: Point, from: PathVertex, to: PathVertex| {
let from_control_point = resolve(from.control_point_from()) + from_point;
let to_control_point = resolve(to.control_point_to()) + to_point;
path.cubic_to(from_control_point, to_control_point, to_point);
let p0 = kurbo::Point::new(from_point.x.to_raw(), from_point.y.to_raw());
let p1 = kurbo::Point::new(
from_control_point.x.to_raw(),
from_control_point.y.to_raw(),
);
let p2 = kurbo::Point::new(
to_control_point.x.to_raw(),
to_control_point.y.to_raw(),
);
let p3 = kurbo::Point::new(to_point.x.to_raw(), to_point.y.to_raw());
let extrema = CubicBez::new(p0, p1, p2, p3).bounding_box();
size.x.set_max(Abs::raw(extrema.x1));
size.y.set_max(Abs::raw(extrema.y1));
};
for (vertex_window, point_window) in vertices.windows(2).zip(points.windows(2)) {
let from = vertex_window[0];
let to = vertex_window[1];
let from_point = point_window[0];
let to_point = point_window[1];
add_cubic(from_point, to_point, from, to);
}
if self.closed(styles) {
let from = *vertices.last().unwrap(); // We checked that we have at least one element.
let to = vertices[0];
let from_point = *points.last().unwrap();
let to_point = points[0];
add_cubic(from_point, to_point, from, to);
path.close_path();
}
// Prepare fill and stroke.
let fill = self.fill(styles);
let stroke = match self.stroke(styles) {
Smart::Auto if fill.is_none() => Some(FixedStroke::default()),
Smart::Auto => None,
Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default),
};
let mut frame = Frame::soft(size);
let shape = Shape { geometry: Geometry::Path(path), stroke, fill };
frame.push(Point::zero(), FrameItem::Shape(shape, self.span()));
Ok(frame)
impl Show for Packed<PathElem> {
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::single_layouter(self.clone(), layout_path).pack())
}
}
/// Layout the path.
#[typst_macros::time(span = elem.span())]
fn layout_path(
elem: &Packed<PathElem>,
_: &mut Engine,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame> {
let resolve = |axes: Axes<Rel<Length>>| {
axes.resolve(styles).zip_map(region.size, Rel::relative_to).to_point()
};
let vertices = elem.vertices();
let points: Vec<Point> = vertices.iter().map(|c| resolve(c.vertex())).collect();
let mut size = Size::zero();
if points.is_empty() {
return Ok(Frame::soft(size));
}
// Only create a path if there are more than zero points.
// Construct a closed path given all points.
let mut path = Path::new();
path.move_to(points[0]);
let mut add_cubic = |from_point: Point,
to_point: Point,
from: PathVertex,
to: PathVertex| {
let from_control_point = resolve(from.control_point_from()) + from_point;
let to_control_point = resolve(to.control_point_to()) + to_point;
path.cubic_to(from_control_point, to_control_point, to_point);
let p0 = kurbo::Point::new(from_point.x.to_raw(), from_point.y.to_raw());
let p1 = kurbo::Point::new(
from_control_point.x.to_raw(),
from_control_point.y.to_raw(),
);
let p2 =
kurbo::Point::new(to_control_point.x.to_raw(), to_control_point.y.to_raw());
let p3 = kurbo::Point::new(to_point.x.to_raw(), to_point.y.to_raw());
let extrema = CubicBez::new(p0, p1, p2, p3).bounding_box();
size.x.set_max(Abs::raw(extrema.x1));
size.y.set_max(Abs::raw(extrema.y1));
};
for (vertex_window, point_window) in vertices.windows(2).zip(points.windows(2)) {
let from = vertex_window[0];
let to = vertex_window[1];
let from_point = point_window[0];
let to_point = point_window[1];
add_cubic(from_point, to_point, from, to);
}
if elem.closed(styles) {
let from = *vertices.last().unwrap(); // We checked that we have at least one element.
let to = vertices[0];
let from_point = *points.last().unwrap();
let to_point = points[0];
add_cubic(from_point, to_point, from, to);
path.close_path();
}
// Prepare fill and stroke.
let fill = elem.fill(styles);
let stroke = match elem.stroke(styles) {
Smart::Auto if fill.is_none() => Some(FixedStroke::default()),
Smart::Auto => None,
Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default),
};
let mut frame = Frame::soft(size);
let shape = Shape { geometry: Geometry::Path(path), stroke, fill };
frame.push(Point::zero(), FrameItem::Shape(shape, elem.span()));
Ok(frame)
}
/// A component used for path creation.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum PathVertex {

View File

@ -6,7 +6,7 @@ use ecow::{eco_format, EcoString};
use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
use crate::foundations::{func, repr, scope, ty, Content, Smart, StyleChain};
use crate::layout::{Abs, Axes, Frame, LayoutMultiple, Length, Regions, Size};
use crate::layout::{Abs, Axes, Frame, Length, Regions, Size};
use crate::syntax::{Span, Spanned};
use crate::utils::{LazyHash, Numeric};
use crate::visualize::RelativeTo;

View File

@ -3,11 +3,9 @@ use std::f64::consts::PI;
use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
elem, func, scope, Content, NativeElement, Packed, Resolve, Smart, StyleChain,
};
use crate::layout::{
Axes, Em, Frame, FrameItem, LayoutSingle, Length, Point, Regions, Rel,
elem, func, scope, Content, NativeElement, Packed, Resolve, Show, Smart, StyleChain,
};
use crate::layout::{Axes, BlockElem, Em, Frame, FrameItem, Length, Point, Region, Rel};
use crate::syntax::Span;
use crate::utils::Numeric;
use crate::visualize::{FixedStroke, Geometry, Paint, Path, Shape, Stroke};
@ -27,7 +25,7 @@ use crate::visualize::{FixedStroke, Geometry, Paint, Path, Shape, Stroke};
/// (0%, 2cm),
/// )
/// ```
#[elem(scope, LayoutSingle)]
#[elem(scope, Show)]
pub struct PolygonElem {
/// How to fill the polygon.
///
@ -125,52 +123,55 @@ impl PolygonElem {
}
}
impl LayoutSingle for Packed<PolygonElem> {
#[typst_macros::time(name = "polygon", span = self.span())]
fn layout(
&self,
_: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Frame> {
let points: Vec<Point> = self
.vertices()
.iter()
.map(|c| {
c.resolve(styles).zip_map(regions.base(), Rel::relative_to).to_point()
})
.collect();
let size = points.iter().fold(Point::zero(), |max, c| c.max(max)).to_size();
if !size.is_finite() {
bail!(self.span(), "cannot create polygon with infinite size");
}
let mut frame = Frame::hard(size);
// Only create a path if there are more than zero points.
if points.is_empty() {
return Ok(frame);
}
// Prepare fill and stroke.
let fill = self.fill(styles);
let stroke = match self.stroke(styles) {
Smart::Auto if fill.is_none() => Some(FixedStroke::default()),
Smart::Auto => None,
Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default),
};
// Construct a closed path given all points.
let mut path = Path::new();
path.move_to(points[0]);
for &point in &points[1..] {
path.line_to(point);
}
path.close_path();
let shape = Shape { geometry: Geometry::Path(path), stroke, fill };
frame.push(Point::zero(), FrameItem::Shape(shape, self.span()));
Ok(frame)
impl Show for Packed<PolygonElem> {
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::single_layouter(self.clone(), layout_polygon).pack())
}
}
/// Layout the polygon.
#[typst_macros::time(span = elem.span())]
fn layout_polygon(
elem: &Packed<PolygonElem>,
_: &mut Engine,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame> {
let points: Vec<Point> = elem
.vertices()
.iter()
.map(|c| c.resolve(styles).zip_map(region.size, Rel::relative_to).to_point())
.collect();
let size = points.iter().fold(Point::zero(), |max, c| c.max(max)).to_size();
if !size.is_finite() {
bail!(elem.span(), "cannot create polygon with infinite size");
}
let mut frame = Frame::hard(size);
// Only create a path if there are more than zero points.
if points.is_empty() {
return Ok(frame);
}
// Prepare fill and stroke.
let fill = elem.fill(styles);
let stroke = match elem.stroke(styles) {
Smart::Auto if fill.is_none() => Some(FixedStroke::default()),
Smart::Auto => None,
Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default),
};
// Construct a closed path given all points.
let mut path = Path::new();
path.move_to(points[0]);
for &point in &points[1..] {
path.line_to(point);
}
path.close_path();
let shape = Shape { geometry: Geometry::Path(path), stroke, fill };
frame.push(Point::zero(), FrameItem::Shape(shape, elem.span()));
Ok(frame)
}

View File

@ -2,10 +2,10 @@ use std::f64::consts::SQRT_2;
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{elem, Content, Packed, Resolve, Smart, StyleChain};
use crate::foundations::{elem, Content, NativeElement, Packed, Show, Smart, StyleChain};
use crate::layout::{
Abs, Axes, Corner, Corners, Frame, FrameItem, LayoutMultiple, LayoutSingle, Length,
Point, Ratio, Regions, Rel, Sides, Size,
Abs, Axes, BlockElem, Corner, Corners, Frame, FrameItem, Length, Point, Ratio,
Region, Regions, Rel, Sides, Size,
};
use crate::syntax::Span;
use crate::utils::Get;
@ -24,7 +24,7 @@ use crate::visualize::{FixedStroke, Paint, Path, Stroke};
/// to fit the content.
/// ]
/// ```
#[elem(title = "Rectangle", LayoutSingle)]
#[elem(title = "Rectangle", Show)]
pub struct RectElem {
/// The rectangle's width, relative to its parent container.
pub width: Smart<Rel<Length>>,
@ -128,31 +128,30 @@ pub struct RectElem {
/// When this is omitted, the rectangle takes on a default size of at most
/// `{45pt}` by `{30pt}`.
#[positional]
#[borrowed]
pub body: Option<Content>,
}
impl LayoutSingle for Packed<RectElem> {
#[typst_macros::time(name = "rect", span = self.span())]
fn layout(
&self,
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Frame> {
layout(
engine,
styles,
regions,
ShapeKind::Rect,
&self.body(styles),
Axes::new(self.width(styles), self.height(styles)),
self.fill(styles),
self.stroke(styles),
self.inset(styles),
self.outset(styles),
self.radius(styles),
self.span(),
)
impl Show for Packed<RectElem> {
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::single_layouter(self.clone(), |elem, engine, styles, region| {
layout_shape(
engine,
styles,
region,
ShapeKind::Rect,
elem.body(styles),
elem.fill(styles),
elem.stroke(styles),
elem.inset(styles),
elem.outset(styles),
elem.radius(styles),
elem.span(),
)
})
.with_width(self.width(styles))
.with_height(self.height(styles))
.pack())
}
}
@ -169,7 +168,7 @@ impl LayoutSingle for Packed<RectElem> {
/// sized to fit.
/// ]
/// ```
#[elem(LayoutSingle)]
#[elem(Show)]
pub struct SquareElem {
/// The square's side length. This is mutually exclusive with `width` and
/// `height`.
@ -234,31 +233,30 @@ pub struct SquareElem {
/// When this is omitted, the square takes on a default size of at most
/// `{30pt}`.
#[positional]
#[borrowed]
pub body: Option<Content>,
}
impl LayoutSingle for Packed<SquareElem> {
#[typst_macros::time(name = "square", span = self.span())]
fn layout(
&self,
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Frame> {
layout(
engine,
styles,
regions,
ShapeKind::Square,
&self.body(styles),
Axes::new(self.width(styles), self.height(styles)),
self.fill(styles),
self.stroke(styles),
self.inset(styles),
self.outset(styles),
self.radius(styles),
self.span(),
)
impl Show for Packed<SquareElem> {
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::single_layouter(self.clone(), |elem, engine, styles, regions| {
layout_shape(
engine,
styles,
regions,
ShapeKind::Square,
elem.body(styles),
elem.fill(styles),
elem.stroke(styles),
elem.inset(styles),
elem.outset(styles),
elem.radius(styles),
elem.span(),
)
})
.with_width(self.width(styles))
.with_height(self.height(styles))
.pack())
}
}
@ -276,7 +274,7 @@ impl LayoutSingle for Packed<SquareElem> {
/// to fit the content.
/// ]
/// ```
#[elem(LayoutSingle)]
#[elem(Show)]
pub struct EllipseElem {
/// The ellipse's width, relative to its parent container.
pub width: Smart<Rel<Length>>,
@ -312,31 +310,30 @@ pub struct EllipseElem {
/// When this is omitted, the ellipse takes on a default size of at most
/// `{45pt}` by `{30pt}`.
#[positional]
#[borrowed]
pub body: Option<Content>,
}
impl LayoutSingle for Packed<EllipseElem> {
#[typst_macros::time(name = "ellipse", span = self.span())]
fn layout(
&self,
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Frame> {
layout(
engine,
styles,
regions,
ShapeKind::Ellipse,
&self.body(styles),
Axes::new(self.width(styles), self.height(styles)),
self.fill(styles),
self.stroke(styles).map(|s| Sides::splat(Some(s))),
self.inset(styles),
self.outset(styles),
Corners::splat(None),
self.span(),
)
impl Show for Packed<EllipseElem> {
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::single_layouter(self.clone(), |elem, engine, styles, regions| {
layout_shape(
engine,
styles,
regions,
ShapeKind::Ellipse,
elem.body(styles),
elem.fill(styles),
elem.stroke(styles).map(|s| Sides::splat(Some(s))),
elem.inset(styles),
elem.outset(styles),
Corners::splat(None),
elem.span(),
)
})
.with_width(self.width(styles))
.with_height(self.height(styles))
.pack())
}
}
@ -354,7 +351,7 @@ impl LayoutSingle for Packed<EllipseElem> {
/// sized to fit.
/// ]
/// ```
#[elem(LayoutSingle)]
#[elem(Show)]
pub struct CircleElem {
/// The circle's radius. This is mutually exclusive with `width` and
/// `height`.
@ -415,43 +412,42 @@ pub struct CircleElem {
/// The content to place into the circle. The circle expands to fit this
/// content, keeping the 1-1 aspect ratio.
#[positional]
#[borrowed]
pub body: Option<Content>,
}
impl LayoutSingle for Packed<CircleElem> {
#[typst_macros::time(name = "circle", span = self.span())]
fn layout(
&self,
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Frame> {
layout(
engine,
styles,
regions,
ShapeKind::Circle,
&self.body(styles),
Axes::new(self.width(styles), self.height(styles)),
self.fill(styles),
self.stroke(styles).map(|s| Sides::splat(Some(s))),
self.inset(styles),
self.outset(styles),
Corners::splat(None),
self.span(),
)
impl Show for Packed<CircleElem> {
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::single_layouter(self.clone(), |elem, engine, styles, regions| {
layout_shape(
engine,
styles,
regions,
ShapeKind::Circle,
elem.body(styles),
elem.fill(styles),
elem.stroke(styles).map(|s| Sides::splat(Some(s))),
elem.inset(styles),
elem.outset(styles),
Corners::splat(None),
elem.span(),
)
})
.with_width(self.width(styles))
.with_height(self.height(styles))
.pack())
}
}
/// Layout a shape.
#[typst_macros::time(span = span)]
#[allow(clippy::too_many_arguments)]
fn layout(
fn layout_shape(
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
region: Region,
kind: ShapeKind,
body: &Option<Content>,
sizing: Axes<Smart<Rel<Length>>>,
fill: Option<Paint>,
stroke: Smart<Sides<Option<Option<Stroke<Abs>>>>>,
inset: Sides<Option<Rel<Abs>>>,
@ -459,47 +455,41 @@ fn layout(
radius: Corners<Option<Rel<Abs>>>,
span: Span,
) -> SourceResult<Frame> {
let resolved = sizing
.zip_map(regions.base(), |s, r| s.map(|v| v.resolve(styles).relative_to(r)));
let mut frame;
let mut inset = inset.unwrap_or_default();
if let Some(child) = body {
let region = resolved.unwrap_or(regions.base());
let mut inset = inset.unwrap_or_default();
if kind.is_round() {
inset = inset.map(|side| side + Ratio::new(0.5 - SQRT_2 / 4.0));
// Apply extra inset to round shapes.
inset = inset.map(|v| v + Ratio::new(0.5 - SQRT_2 / 4.0));
}
let has_inset = !inset.is_zero();
// Take the inset, if any, into account.
let mut pod = region;
if has_inset {
pod.size = crate::layout::shrink(region.size, &inset);
}
// Pad the child.
let child = child.clone().padded(inset.map(|side| side.map(Length::from)));
let expand = sizing.as_ref().map(Smart::is_custom);
let pod = Regions::one(region, expand);
frame = child.layout(engine, styles, pod)?.into_frame();
// Layout the child.
frame = child.layout(engine, styles, pod.into_regions())?.into_frame();
// Enforce correct size.
*frame.size_mut() = expand.select(region, frame.size());
// Relayout with full expansion into square region to make sure
// the result is really a square or circle.
// If the child is a square or circle, relayout with full expansion into
// square region to make sure the result is really quadratic.
if kind.is_quadratic() {
frame.set_size(Size::splat(frame.size().max_by_side()));
let length = frame.size().max_by_side().min(region.min_by_side());
let pod = Regions::one(Size::splat(length), Axes::splat(true));
frame = child.layout(engine, styles, pod)?.into_frame();
let length = frame.size().max_by_side().min(pod.size.min_by_side());
let quad_pod = Regions::one(Size::splat(length), Axes::splat(true));
frame = child.layout(engine, styles, quad_pod)?.into_frame();
}
// Enforce correct size again.
*frame.size_mut() = expand.select(region, frame.size());
if kind.is_quadratic() {
frame.set_size(Size::splat(frame.size().max_by_side()));
// Apply the inset.
if has_inset {
crate::layout::grow(&mut frame, &inset);
}
} else {
// The default size that a shape takes on if it has no child and
// enough space.
let default = Size::new(Abs::pt(45.0), Abs::pt(30.0));
let mut size = resolved.unwrap_or(default.min(regions.base()));
let mut size = region.expand.select(region.size, default.min(region.size));
if kind.is_quadratic() {
size = Size::splat(size.min_by_side());
}
@ -526,9 +516,9 @@ fn layout(
} else {
frame.fill_and_stroke(
fill,
stroke,
outset.unwrap_or_default(),
radius.unwrap_or_default(),
&stroke,
&outset.unwrap_or_default(),
&radius.unwrap_or_default(),
span,
);
}
@ -633,7 +623,7 @@ pub(crate) fn ellipse(
/// Creates a new rectangle as a path.
pub(crate) fn clip_rect(
size: Size,
radius: Corners<Rel<Abs>>,
radius: &Corners<Rel<Abs>>,
stroke: &Sides<Option<FixedStroke>>,
) -> Path {
let stroke_widths = stroke
@ -644,8 +634,7 @@ pub(crate) fn clip_rect(
+ stroke_widths.iter().cloned().min().unwrap_or(Abs::zero());
let radius = radius.map(|side| side.relative_to(max_radius * 2.0).min(max_radius));
let corners = corners_control_points(size, radius, stroke, stroke_widths);
let corners = corners_control_points(size, &radius, stroke, &stroke_widths);
let mut path = Path::new();
if corners.top_left.arc_inner() {
@ -674,12 +663,12 @@ pub(crate) fn clip_rect(
/// - use fill for sides for best looks
pub(crate) fn styled_rect(
size: Size,
radius: Corners<Rel<Abs>>,
radius: &Corners<Rel<Abs>>,
fill: Option<Paint>,
stroke: Sides<Option<FixedStroke>>,
stroke: &Sides<Option<FixedStroke>>,
) -> Vec<Shape> {
if stroke.is_uniform() && radius.iter().cloned().all(Rel::is_zero) {
simple_rect(size, fill, stroke.top)
simple_rect(size, fill, stroke.top.clone())
} else {
segmented_rect(size, radius, fill, stroke)
}
@ -696,9 +685,9 @@ fn simple_rect(
fn corners_control_points(
size: Size,
radius: Corners<Abs>,
radius: &Corners<Abs>,
strokes: &Sides<Option<FixedStroke>>,
stroke_widths: Sides<Abs>,
stroke_widths: &Sides<Abs>,
) -> Corners<ControlPoints> {
Corners {
top_left: Corner::TopLeft,
@ -726,9 +715,9 @@ fn corners_control_points(
/// Use stroke and fill for the rectangle
fn segmented_rect(
size: Size,
radius: Corners<Rel<Abs>>,
radius: &Corners<Rel<Abs>>,
fill: Option<Paint>,
strokes: Sides<Option<FixedStroke>>,
strokes: &Sides<Option<FixedStroke>>,
) -> Vec<Shape> {
let mut res = vec![];
let stroke_widths = strokes
@ -739,8 +728,7 @@ fn segmented_rect(
+ stroke_widths.iter().cloned().min().unwrap_or(Abs::zero());
let radius = radius.map(|side| side.relative_to(max_radius * 2.0).min(max_radius));
let corners = corners_control_points(size, radius, &strokes, stroke_widths);
let corners = corners_control_points(size, &radius, strokes, &stroke_widths);
// insert stroked sides below filled sides
let mut stroke_insert = 0;
@ -786,10 +774,7 @@ fn segmented_rect(
let start = last;
let end = current;
last = current;
let stroke = match strokes.get_ref(start.side_cw()) {
None => continue,
Some(stroke) => stroke.clone(),
};
let Some(stroke) = strokes.get_ref(start.side_cw()) else { continue };
let (shape, ontop) = segment(start, end, &corners, stroke);
if ontop {
res.push(shape);
@ -798,7 +783,7 @@ fn segmented_rect(
stroke_insert += 1;
}
}
} else if let Some(stroke) = strokes.top {
} else if let Some(stroke) = &strokes.top {
// single segment
let (shape, _) = segment(Corner::TopLeft, Corner::TopLeft, &corners, stroke);
res.push(shape);
@ -848,7 +833,7 @@ fn segment(
start: Corner,
end: Corner,
corners: &Corners<ControlPoints>,
stroke: FixedStroke,
stroke: &FixedStroke,
) -> (Shape, bool) {
fn fill_corner(corner: &ControlPoints) -> bool {
corner.stroke_before != corner.stroke_after
@ -883,12 +868,12 @@ fn segment(
.unwrap_or(true);
let use_fill = solid && fill_corners(start, end, corners);
let shape = if use_fill {
fill_segment(start, end, corners, stroke)
} else {
stroke_segment(start, end, corners, stroke)
stroke_segment(start, end, corners, stroke.clone())
};
(shape, use_fill)
}
@ -899,7 +884,7 @@ fn stroke_segment(
corners: &Corners<ControlPoints>,
stroke: FixedStroke,
) -> Shape {
// create start corner
// Create start corner.
let mut path = Path::new();
path_segment(start, end, corners, &mut path);
@ -915,7 +900,7 @@ fn fill_segment(
start: Corner,
end: Corner,
corners: &Corners<ControlPoints>,
stroke: FixedStroke,
stroke: &FixedStroke,
) -> Shape {
let mut path = Path::new();
@ -1004,7 +989,7 @@ fn fill_segment(
Shape {
geometry: Geometry::Path(path),
stroke: None,
fill: Some(stroke.paint),
fill: Some(stroke.paint.clone()),
}
}

View File

@ -14,19 +14,15 @@ use once_cell::sync::Lazy;
use serde::Deserialize;
use serde_yaml as yaml;
use typst::diag::{bail, StrResult};
use typst::foundations::AutoValue;
use typst::foundations::Bytes;
use typst::foundations::NoneValue;
use typst::foundations::{
CastInfo, Category, Func, Module, ParamInfo, Repr, Scope, Smart, Type, Value,
FOUNDATIONS,
AutoValue, Bytes, CastInfo, Category, Func, Module, NoneValue, ParamInfo, Repr,
Scope, Smart, Type, Value, FOUNDATIONS,
};
use typst::introspection::INTROSPECTION;
use typst::layout::{Abs, Margin, PageElem, LAYOUT};
use typst::loading::DATA_LOADING;
use typst::math::MATH;
use typst::model::Document;
use typst::model::MODEL;
use typst::model::{Document, MODEL};
use typst::symbols::SYMBOLS;
use typst::text::{Font, FontBook, TEXT};
use typst::utils::LazyHash;

Binary file not shown.

After

Width:  |  Height:  |  Size: 870 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 879 B

After

Width:  |  Height:  |  Size: 881 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 321 B

After

Width:  |  Height:  |  Size: 333 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 305 B

After

Width:  |  Height:  |  Size: 289 B

View File

@ -96,6 +96,18 @@ Paragraph
lorem(8) + colbreak(),
)
--- block-consistent-width ---
// Test that block enforces consistent width across regions. Also use some
// introspection to check that measurement is working correctly.
#block(stroke: 1pt, inset: 5pt)[
#align(right)[Hi]
#colbreak()
Hello @netwok
]
#show bibliography: none
#bibliography("/assets/bib/works.bib")
--- box-clip-rect ---
// Test box clipping with a rectangle
Hello #box(width: 1em, height: 1em, clip: false)[#rect(width: 3em, height: 3em, fill: red)]

View File

@ -31,7 +31,7 @@ Placed item in the first region.
// In-flow item with size zero in the first region.
#set page(height: 5cm, margin: 1cm)
In-flow, zero-sized item.
#block(breakable: true, stroke: 1pt, inset: 0.5cm)[
#block(breakable: true, stroke: 1pt, inset: 0.4cm)[
#set block(spacing: 0pt)
#line(length: 0pt)
#rect(height: 2cm, fill: gray)

View File

@ -45,11 +45,10 @@
fill: gradient.linear(red, purple, space: color.hsl)
)
--- gradient-linear-relative-parent ---
// The image should look as if there is a single gradient that is being used for
// both the page and the rectangles.
#let grad = gradient.linear(red, blue, green, purple, relative: "parent");
#let grad = gradient.linear(red, blue, green, purple, relative: "parent")
#let my-rect = rect(width: 50%, height: 50%, fill: grad)
#set page(
height: 50pt,
@ -64,7 +63,7 @@
--- gradient-linear-relative-self ---
// The image should look as if there are multiple gradients, one for each
// rectangle.
#let grad = gradient.linear(red, blue, green, purple, relative: "self");
#let grad = gradient.linear(red, blue, green, purple, relative: "self")
#let my-rect = rect(width: 50%, height: 50%, fill: grad)
#set page(
height: 50pt,
@ -76,6 +75,29 @@
#place(top + right, my-rect)
#place(bottom + center, rotate(45deg, my-rect))
--- gradient-linear-relative-parent-block ---
// The image should look as if there are two nested gradients, one for the page
// and one for a nested block. The rotated rectangles are not visible because
// they are relative to the block.
#let grad = gradient.linear(red, blue, green, purple, relative: "parent")
#let my-rect = rect(width: 50%, height: 50%, fill: grad)
#set page(
height: 50pt,
width: 50pt,
margin: 5pt,
fill: grad,
background: place(top + left, my-rect),
)
#block(
width: 40pt,
height: 40pt,
inset: 2.5pt,
fill: grad,
)[
#place(top + right, my-rect)
#place(bottom + center, rotate(45deg, my-rect))
]
--- gradient-linear-repeat-and-mirror-1 ---
// Test repeated gradients.
#rect(

View File

@ -69,7 +69,7 @@
dir: ltr,
spacing: 2pt,
square(width: 20pt, height: 40pt),
circle(width: 20%, height: 100pt),
circle(width: 20%, height: 40pt),
)
--- square-height-limited-stack ---