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 ecow::{eco_format, EcoString};
use crate::{ use crate::diag::StrResult;
diag::StrResult, use crate::foundations::{cast, func, repr, scope, ty, Repr, Str, Value};
foundations::{cast, func, repr, scope, ty, Repr, Str, Value},
};
/// A whole number. /// A whole number.
/// ///

View File

@ -25,9 +25,9 @@ pub use self::state::*;
use crate::diag::{bail, SourceResult}; use crate::diag::{bail, SourceResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::NativeElement;
use crate::foundations::{ 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}; use crate::realize::{Behave, Behaviour};

View File

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

View File

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

View File

@ -4,7 +4,7 @@ use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Deref, Not};
use crate::diag::bail; use crate::diag::bail;
use crate::foundations::{array, cast, Array, Resolve, Smart, StyleChain}; 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; use crate::utils::Get;
/// A container with a horizontal and vertical component. /// 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> { impl<T> Get<Axis> for Axes<T> {
type Component = T; type Component = T;

View File

@ -2,10 +2,9 @@ use std::num::NonZeroUsize;
use crate::diag::SourceResult; use crate::diag::SourceResult;
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{elem, Content, Packed, StyleChain}; use crate::foundations::{elem, Content, NativeElement, Packed, Show, StyleChain};
use crate::layout::{ use crate::layout::{
Abs, Axes, Dir, Fragment, Frame, LayoutMultiple, Length, Point, Ratio, Regions, Rel, Abs, Axes, BlockElem, Dir, Fragment, Frame, Length, Point, Ratio, Regions, Rel, Size,
Size,
}; };
use crate::realize::{Behave, Behaviour}; use crate::realize::{Behave, Behaviour};
use crate::text::TextElem; use crate::text::TextElem;
@ -42,7 +41,7 @@ use crate::utils::Numeric;
/// increasingly been used to solve a /// increasingly been used to solve a
/// variety of problems. /// variety of problems.
/// ``` /// ```
#[elem(LayoutMultiple)] #[elem(Show)]
pub struct ColumnsElem { pub struct ColumnsElem {
/// The number of columns. /// The number of columns.
#[positional] #[positional]
@ -59,15 +58,23 @@ pub struct ColumnsElem {
pub body: Content, pub body: Content,
} }
impl LayoutMultiple for Packed<ColumnsElem> { impl Show for Packed<ColumnsElem> {
#[typst_macros::time(name = "columns", span = self.span())] fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
fn layout( Ok(BlockElem::multi_layouter(self.clone(), layout_columns)
&self, .with_rootable(true)
.pack())
}
}
/// Layout the columns.
#[typst_macros::time(span = elem.span())]
fn layout_columns(
elem: &Packed<ColumnsElem>,
engine: &mut Engine, engine: &mut Engine,
styles: StyleChain, styles: StyleChain,
regions: Regions, regions: Regions,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
let body = self.body(); let body = elem.body();
// Separating the infinite space into infinite columns does not make // Separating the infinite space into infinite columns does not make
// much sense. // much sense.
@ -76,8 +83,8 @@ impl LayoutMultiple for Packed<ColumnsElem> {
} }
// Determine the width of the gutter and each column. // Determine the width of the gutter and each column.
let columns = self.count(styles).get(); let columns = elem.count(styles).get();
let gutter = self.gutter(styles).relative_to(regions.base().x); let gutter = elem.gutter(styles).relative_to(regions.base().x);
let width = (regions.size.x - gutter * (columns - 1) as f64) / columns as f64; let width = (regions.size.x - gutter * (columns - 1) as f64) / columns as f64;
let backlog: Vec<_> = std::iter::once(&regions.size.y) let backlog: Vec<_> = std::iter::once(&regions.size.y)
@ -120,11 +127,8 @@ impl LayoutMultiple for Packed<ColumnsElem> {
} }
let width = frame.width(); let width = frame.width();
let x = if dir == Dir::LTR { let x =
cursor if dir == Dir::LTR { cursor } else { regions.size.x - cursor - width };
} else {
regions.size.x - cursor - width
};
output.push_frame(Point::with_x(x), frame); output.push_frame(Point::with_x(x), frame);
cursor += width + gutter; cursor += width + gutter;
@ -134,7 +138,6 @@ impl LayoutMultiple for Packed<ColumnsElem> {
} }
Ok(Fragment::frames(finished)) Ok(Fragment::frames(finished))
}
} }
/// Forces a column break. /// 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::engine::Engine;
use crate::foundations::{ 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::{ use crate::layout::{
Abs, Axes, Corners, Em, Fr, Fragment, Frame, FrameKind, LayoutMultiple, Length, Abs, Axes, Corners, Em, Fr, Fragment, Frame, FrameKind, Length, Region, Regions, Rel,
Ratio, Regions, Rel, Sides, Size, Spacing, VElem, Sides, Size, Spacing, VElem,
}; };
use crate::utils::Numeric; use crate::utils::Numeric;
use crate::visualize::{clip_rect, Paint, Stroke}; use crate::visualize::{clip_rect, Paint, Stroke};
@ -106,47 +110,53 @@ pub struct BoxElem {
/// The contents of the box. /// The contents of the box.
#[positional] #[positional]
#[borrowed]
pub body: Option<Content>, pub body: Option<Content>,
} }
impl Packed<BoxElem> { impl Packed<BoxElem> {
/// Layout this box as part of a paragraph.
#[typst_macros::time(name = "box", span = self.span())] #[typst_macros::time(name = "box", span = self.span())]
pub fn layout( pub fn layout(
&self, &self,
engine: &mut Engine, engine: &mut Engine,
styles: StyleChain, styles: StyleChain,
regions: Regions, region: Size,
) -> SourceResult<Frame> { ) -> SourceResult<Frame> {
let width = match self.width(styles) { // Fetch sizing properties.
Sizing::Auto => Smart::Auto, let width = self.width(styles);
Sizing::Rel(rel) => Smart::Custom(rel), let height = self.height(styles);
Sizing::Fr(_) => Smart::Custom(Ratio::one().into()), 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. // Enforce a correct frame size on the expanded axes. Do this before
let sizing = Axes::new(width, self.height(styles)); // applying the inset, since the pod shrunk.
let expand = sizing.as_ref().map(Smart::is_custom); frame.set_size(pod.expand.select(pod.size, frame.size()));
let size = sizing
.resolve(styles)
.zip_map(regions.base(), |s, b| s.map(|v| v.relative_to(b)))
.unwrap_or(regions.base());
// Apply inset. // Apply the inset.
let mut body = self.body(styles).unwrap_or_default(); if !inset.is_zero() {
let inset = self.inset(styles).unwrap_or_default(); crate::layout::grow(&mut frame, &inset);
if inset.iter().any(|v| !v.is_zero()) {
body = body.padded(inset.map(|side| side.map(Length::from)));
} }
// Select the appropriate base and expansion for the child depending // Apply baseline shift. Do this after setting the size and applying the
// on whether it is automatically or relatively sized. // inset, so that a relative shift is resolved relative to the final
let pod = Regions::one(size, expand); // height.
let mut frame = body.layout(engine, styles, pod)?.into_frame();
// Enforce correct size.
*frame.size_mut() = expand.select(size, frame.size());
// Apply baseline shift.
let shift = self.baseline(styles).relative_to(frame.height()); let shift = self.baseline(styles).relative_to(frame.height());
if !shift.is_zero() { if !shift.is_zero() {
frame.set_baseline(frame.baseline() - shift); frame.set_baseline(frame.baseline() - shift);
@ -159,27 +169,115 @@ impl Packed<BoxElem> {
.unwrap_or_default() .unwrap_or_default()
.map(|s| s.map(Stroke::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) { if self.clip(styles) {
let outset = let size = frame.size() + outset.relative_to(frame.size()).sum_by_axis();
self.outset(styles).unwrap_or_default().relative_to(frame.size()); frame.clip(clip_rect(size, &radius, &stroke));
let size = frame.size() + outset.sum_by_axis();
let radius = self.radius(styles).unwrap_or_default();
frame.clip(clip_rect(size, radius, &stroke));
} }
// Add fill and/or stroke. // Add fill and/or stroke.
if fill.is_some() || stroke.iter().any(Option::is_some) { if fill.is_some() || stroke.iter().any(Option::is_some) {
let outset = self.outset(styles).unwrap_or_default(); frame.fill_and_stroke(fill, &stroke, &outset, &radius, self.span());
let radius = self.radius(styles).unwrap_or_default();
frame.fill_and_stroke(fill, stroke, outset, radius, self.span());
} }
// Apply metadata.
frame.set_kind(FrameKind::Hard);
Ok(frame) 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. /// A block-level container.
@ -211,7 +309,7 @@ impl Packed<BoxElem> {
/// = Blocky /// = Blocky
/// More text. /// More text.
/// ``` /// ```
#[elem(LayoutMultiple)] #[elem]
pub struct BlockElem { pub struct BlockElem {
/// The block's width. /// The block's width.
/// ///
@ -332,93 +430,155 @@ pub struct BlockElem {
#[default(false)] #[default(false)]
pub clip: bool, pub clip: bool,
/// The contents of the block.
#[positional]
pub body: Option<Content>,
/// Whether this block must stick to the following one. /// Whether this block must stick to the following one.
/// ///
/// Use this to prevent page breaks between e.g. a heading and its body. /// Use this to prevent page breaks between e.g. a heading and its body.
#[internal] #[internal]
#[default(false)] #[default(false)]
#[ghost] #[parse(None)]
pub sticky: bool, 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())] #[typst_macros::time(name = "block", span = self.span())]
fn layout( pub fn layout(
&self, &self,
engine: &mut Engine, engine: &mut Engine,
styles: StyleChain, styles: StyleChain,
regions: Regions, regions: Regions,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
// Apply inset. // Fetch sizing properties.
let mut body = self.body(styles).unwrap_or_default(); let width = self.width(styles);
let height = self.height(styles);
let inset = self.inset(styles).unwrap_or_default(); let inset = self.inset(styles).unwrap_or_default();
if inset.iter().any(|v| !v.is_zero()) { let breakable = self.breakable(styles);
body = body.clone().padded(inset.map(|side| side.map(Length::from)));
}
// Resolve the sizing to a concrete size. // Allocate a small vector for backlogs.
let sizing = Axes::new(self.width(styles), self.height(styles)); let mut buf = SmallVec::<[Abs; 2]>::new();
let mut expand = sizing.as_ref().map(Smart::is_custom);
let mut size = sizing
.resolve(styles)
.zip_map(regions.base(), |s, b| s.map(|v| v.relative_to(b)))
.unwrap_or(regions.base());
// Layout the child. // Build the pod regions.
let mut frames = if self.breakable(styles) { let pod =
// Measure to ensure frames for all regions have the same width. Self::pod(&width, &height, &inset, breakable, styles, regions, &mut buf);
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;
}
let mut pod = regions; // Layout the body.
pod.size.x = size.x; let body = self.body(styles);
pod.expand = expand; let mut fragment = match body {
// If we have no body, just create one frame plus one per backlog
if expand.y { // region. We create them zero-sized; if necessary, their size will
pod.full = size.y; // be adjusted below.
} None => {
let mut frames = vec![];
// Generate backlog for fixed height. frames.push(Frame::hard(Size::zero()));
let mut heights = vec![]; if pod.expand.y {
if sizing.y.is_custom() { let mut iter = pod;
let mut remaining = size.y; while !iter.backlog.is_empty() {
for region in regions.iter() { frames.push(Frame::hard(Size::zero()));
let limited = region.y.min(remaining); iter.next();
heights.push(limited);
remaining -= limited;
if Abs::zero().fits(remaining) {
break;
} }
} }
Fragment::frames(frames)
if let Some(last) = heights.last_mut() {
*last += remaining;
} }
pod.size.y = heights[0]; // If we have content as our body, just layout it.
pod.backlog = &heights[1..]; Some(BlockChild::Content(body)) => {
pod.last = None; let mut fragment = body.measure(engine, styles, pod)?;
}
let mut frames = body.layout(engine, styles, pod)?.into_frames(); // If the body is automatically sized and produced more than one
for (frame, &height) in frames.iter_mut().zip(&heights) { // fragment, ensure that the width was consistent across all
*frame.size_mut() = // regions. If it wasn't, we need to relayout with expansion.
expand.select(Size::new(size.x, height), frame.size()); if !pod.expand.x
} && fragment
frames .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 { } else {
let pod = Regions::one(size, expand); // Apply the side effect to turn the `measure` into a
let mut frames = body.layout(engine, styles, pod)?.into_frames(); // `layout`.
*frames[0].size_mut() = expand.select(size, frames[0].size()); engine.locator.visit_frames(&fragment);
frames }
fragment
}
// 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)?
}
}; };
// Prepare fill and stroke. // Prepare fill and stroke.
@ -428,60 +588,219 @@ impl LayoutMultiple for Packed<BlockElem> {
.unwrap_or_default() .unwrap_or_default()
.map(|s| s.map(Stroke::unwrap_or_default)); .map(|s| s.map(Stroke::unwrap_or_default));
// Clip the contents // Only fetch these if necessary (for clipping or filling/stroking).
if self.clip(styles) { let outset = Lazy::new(|| self.outset(styles).unwrap_or_default());
for frame in frames.iter_mut() { let radius = Lazy::new(|| self.radius(styles).unwrap_or_default());
let outset =
self.outset(styles).unwrap_or_default().relative_to(frame.size()); // Fetch/compute these outside of the loop.
let size = frame.size() + outset.sum_by_axis(); let clip = self.clip(styles);
let radius = self.radius(styles).unwrap_or_default(); let has_fill_or_stroke = fill.is_some() || stroke.iter().any(Option::is_some);
frame.clip(clip_rect(size, radius, &stroke)); 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());
} }
// Post-process to apply insets, clipping, fills, and strokes.
for (i, (frame, region)) in fragment.iter_mut().zip(pod.iter()).enumerate() {
// Explicit blocks are boundaries for gradient relativeness.
if is_explicit {
frame.set_kind(FrameKind::Hard);
}
// Enforce a correct frame size on the expanded axes. Do this before
// applying the inset, since the pod shrunk.
frame.set_size(pod.expand.select(region, frame.size()));
// Apply the inset.
if has_inset {
crate::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. // Add fill and/or stroke.
if fill.is_some() || stroke.iter().any(Option::is_some) { if has_fill_or_stroke && (i > 0 || !skip_first) {
let mut skip = false;
if let [first, rest @ ..] = frames.as_slice() {
skip = first.is_empty() && rest.iter().any(|frame| !frame.is_empty());
}
let outset = self.outset(styles).unwrap_or_default();
let radius = self.radius(styles).unwrap_or_default();
for frame in frames.iter_mut().skip(skip as usize) {
frame.fill_and_stroke( frame.fill_and_stroke(
fill.clone(), fill.clone(),
stroke.clone(), &stroke,
outset, &outset,
radius, &radius,
self.span(), self.span(),
); );
} }
} }
// Apply metadata. Ok(fragment)
for frame in &mut frames {
frame.set_kind(FrameKind::Hard);
} }
Ok(Fragment::frames(frames)) /// 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,
);
}
// 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)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum Sizing { pub enum Sizing {
/// A track that fits its cell's contents. /// A track that fits its item's contents.
Auto, Auto,
/// A track size specified in absolute terms and relative to the parent's /// A size specified in absolute terms and relative to the parent's size.
/// size. Rel(Rel),
Rel(Rel<Length>), /// A size specified as a fraction of the remaining free space in the
/// A track size specified as a fraction of the remaining free space in the
/// parent. /// parent.
Fr(Fr), Fr(Fr),
} }
impl Sizing { impl Sizing {
/// Whether this is an automatic sizing.
pub fn is_auto(self) -> bool {
matches!(self, Self::Auto)
}
/// Whether this is fractional sizing. /// Whether this is fractional sizing.
pub fn is_fractional(self) -> bool { pub fn is_fractional(self) -> bool {
matches!(self, Self::Fr(_)) 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 { impl<T: Into<Spacing>> From<T> for Sizing {
fn from(spacing: T) -> Self { fn from(spacing: T) -> Self {
match spacing.into() { match spacing.into() {
@ -514,3 +842,109 @@ cast! {
v: Rel<Length> => Self::Rel(v), v: Rel<Length> => Self::Rel(v),
v: Fr => Self::Fr(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::introspection::TagElem;
use crate::layout::{ use crate::layout::{
Abs, AlignElem, Axes, BlockElem, ColbreakElem, ColumnsElem, FixedAlignment, Abs, AlignElem, Axes, BlockElem, ColbreakElem, FixedAlignment, FlushElem, Fr,
FlushElem, Fr, Fragment, Frame, FrameItem, LayoutMultiple, LayoutSingle, PlaceElem, Fragment, Frame, FrameItem, PlaceElem, Point, Regions, Rel, Size, Spacing, VElem,
Point, Regions, Rel, Size, Spacing, VElem,
}; };
use crate::model::{FootnoteElem, FootnoteEntry, ParElem}; use crate::model::{FootnoteElem, FootnoteEntry, ParElem};
use crate::utils::Numeric; use crate::utils::Numeric;
@ -24,16 +23,16 @@ use crate::utils::Numeric;
/// ///
/// This element is responsible for layouting both the top-level content flow /// This element is responsible for layouting both the top-level content flow
/// and the contents of boxes. /// and the contents of boxes.
#[elem(Debug, LayoutMultiple)] #[elem(Debug)]
pub struct FlowElem { pub struct FlowElem {
/// The children that will be arranged into a flow. /// The children that will be arranged into a flow.
#[variadic] #[variadic]
pub children: Vec<Content>, pub children: Vec<Content>,
} }
impl LayoutMultiple for Packed<FlowElem> { impl Packed<FlowElem> {
#[typst_macros::time(name = "flow", span = self.span())] #[typst_macros::time(name = "flow", span = self.span())]
fn layout( pub fn layout(
&self, &self,
engine: &mut Engine, engine: &mut Engine,
styles: StyleChain, styles: StyleChain,
@ -59,12 +58,13 @@ impl LayoutMultiple for Packed<FlowElem> {
alone = child alone = child
.to_packed::<StyledElem>() .to_packed::<StyledElem>()
.map_or(child, |styled| &styled.child) .map_or(child, |styled| &styled.child)
.can::<dyn LayoutMultiple>(); .is::<BlockElem>();
} }
let outer = styles;
let mut layouter = FlowLayouter::new(regions, styles, alone); let mut layouter = FlowLayouter::new(regions, styles, alone);
for mut child in self.children().iter() { for mut child in self.children().iter() {
let outer = styles;
let mut styles = styles; let mut styles = styles;
if let Some(styled) = child.to_packed::<StyledElem>() { if let Some(styled) = child.to_packed::<StyledElem>() {
child = &styled.child; child = &styled.child;
@ -77,6 +77,10 @@ impl LayoutMultiple for Packed<FlowElem> {
layouter.flush(engine)?; layouter.flush(engine)?;
} else if let Some(elem) = child.to_packed::<VElem>() { } else if let Some(elem) = child.to_packed::<VElem>() {
layouter.layout_spacing(engine, elem, styles)?; 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>() { } else if let Some(placed) = child.to_packed::<PlaceElem>() {
layouter.layout_placed(engine, placed, styles)?; layouter.layout_placed(engine, placed, styles)?;
} else if child.is::<ColbreakElem>() { } else if child.is::<ColbreakElem>() {
@ -84,12 +88,6 @@ impl LayoutMultiple for Packed<FlowElem> {
{ {
layouter.finish_region(engine, true)?; 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 { } else {
bail!(child.span(), "unexpected flow child"); bail!(child.span(), "unexpected flow child");
} }
@ -199,6 +197,7 @@ impl<'a> FlowLayouter<'a> {
/// Create a new flow layouter. /// Create a new flow layouter.
fn new(mut regions: Regions<'a>, styles: StyleChain<'a>, alone: bool) -> Self { fn new(mut regions: Regions<'a>, styles: StyleChain<'a>, alone: bool) -> Self {
let expand = regions.expand; let expand = regions.expand;
let root = std::mem::replace(&mut regions.root, false);
// Disable vertical expansion when there are multiple or not directly // Disable vertical expansion when there are multiple or not directly
// layoutable children. // layoutable children.
@ -206,9 +205,6 @@ impl<'a> FlowLayouter<'a> {
regions.expand.y = false; regions.expand.y = false;
} }
// Disable root.
let root = std::mem::replace(&mut regions.root, false);
Self { Self {
root, root,
regions, 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. /// Layout a paragraph.
fn layout_par( fn layout_par(
&mut self, &mut self,
@ -337,63 +312,33 @@ impl<'a> FlowLayouter<'a> {
Ok(()) 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. /// Layout into multiple regions.
fn layout_multiple( fn layout_block(
&mut self, &mut self,
engine: &mut Engine, engine: &mut Engine,
child: &Content, block: &'a Packed<BlockElem>,
layoutable: &dyn LayoutMultiple, styles: StyleChain<'a>,
styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
// Temporarily delegerate rootness to the columns. // Temporarily delegate rootness to the columns.
let is_root = self.root; let is_root = self.root;
if is_root && child.is::<ColumnsElem>() { if is_root && block.rootable(styles) {
self.root = false; self.root = false;
self.regions.root = true; self.regions.root = true;
} }
let mut notes = Vec::new();
if self.regions.is_full() { if self.regions.is_full() {
// Skip directly if region is already full. // Skip directly if region is already full.
self.finish_region(engine, false)?; 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. // Layout the block itself.
let sticky = BlockElem::sticky_in(styles); let sticky = block.sticky(styles);
let fragment = layoutable.layout(engine, styles, self.regions)?; 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() { for (i, mut frame) in fragment.into_iter().enumerate() {
// Find footnotes in the frame. // Find footnotes in the frame.
if self.root { if self.root {
@ -421,6 +366,27 @@ impl<'a> FlowLayouter<'a> {
Ok(()) 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. /// Attach currently pending metadata to the frame.
fn drain_tag(&mut self, frame: &mut Frame) { fn drain_tag(&mut self, frame: &mut Frame) {
if !self.pending_tags.is_empty() && !frame.is_empty() { if !self.pending_tags.is_empty() && !frame.is_empty() {
@ -444,13 +410,13 @@ impl<'a> FlowLayouter<'a> {
&& !self && !self
.items .items
.iter() .iter()
.any(|item| matches!(item, FlowItem::Frame { .. })) .any(|item| matches!(item, FlowItem::Frame { .. },))
{ {
return Ok(()); return Ok(());
} }
self.regions.size.y -= v self.regions.size.y -= v
} }
FlowItem::Fractional(_) => {} FlowItem::Fractional(..) => {}
FlowItem::Frame { ref frame, movable, .. } => { FlowItem::Frame { ref frame, movable, .. } => {
let height = frame.height(); let height = frame.height();
while !self.regions.size.y.fits(height) && !self.regions.in_last() { while !self.regions.size.y.fits(height) && !self.regions.in_last() {
@ -615,7 +581,8 @@ impl<'a> FlowLayouter<'a> {
} }
FlowItem::Fractional(v) => { FlowItem::Fractional(v) => {
let remaining = self.initial.y - used.y; let remaining = self.initial.y - used.y;
offset += v.share(fr, remaining); let length = v.share(fr, remaining);
offset += length;
} }
FlowItem::Frame { frame, align, .. } => { FlowItem::Frame { frame, align, .. } => {
ruler = ruler.max(align.y); ruler = ruler.max(align.y);

View File

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

View File

@ -30,6 +30,8 @@ pub struct Frame {
/// The items composing this layout. /// The items composing this layout.
items: Arc<LazyHash<Vec<(Point, FrameItem)>>>, items: Arc<LazyHash<Vec<(Point, FrameItem)>>>,
/// The hardness of this frame. /// The hardness of this frame.
///
/// Determines whether it is a boundary for gradient drawing.
kind: FrameKind, kind: FrameKind,
} }
@ -70,6 +72,12 @@ impl Frame {
self.kind = kind; 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. /// Whether the frame is hard or soft.
pub fn kind(&self) -> FrameKind { pub fn kind(&self) -> FrameKind {
self.kind self.kind
@ -217,6 +225,11 @@ impl Frame {
/// Inline a frame at the given layer. /// Inline a frame at the given layer.
fn inline(&mut self, layer: usize, pos: Point, frame: Frame) { 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. // Try to just reuse the items.
if pos.is_zero() && self.items.is_empty() { if pos.is_zero() && self.items.is_empty() {
self.items = frame.items; self.items = frame.items;
@ -354,9 +367,9 @@ impl Frame {
pub fn fill_and_stroke( pub fn fill_and_stroke(
&mut self, &mut self,
fill: Option<Paint>, fill: Option<Paint>,
stroke: Sides<Option<FixedStroke>>, stroke: &Sides<Option<FixedStroke>>,
outset: Sides<Rel<Abs>>, outset: &Sides<Rel<Abs>>,
radius: Corners<Rel<Abs>>, radius: &Corners<Rel<Abs>>,
span: Span, span: Span,
) { ) {
let outset = outset.relative_to(self.size()); let outset = outset.relative_to(self.size());
@ -479,7 +492,7 @@ pub enum FrameKind {
Soft, Soft,
/// A container which uses its own size. /// 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, Hard,
} }

View File

@ -14,10 +14,7 @@ use crate::foundations::{
Array, CastInfo, Content, Context, Fold, FromValue, Func, IntoValue, Reflect, Array, CastInfo, Content, Context, Fold, FromValue, Func, IntoValue, Reflect,
Resolve, Smart, StyleChain, Value, Resolve, Smart, StyleChain, Value,
}; };
use crate::layout::{ use crate::layout::{Abs, Alignment, Axes, Length, LinePosition, Rel, Sides, Sizing};
Abs, Alignment, Axes, Fragment, LayoutMultiple, Length, LinePosition, Regions, Rel,
Sides, Sizing,
};
use crate::syntax::Span; use crate::syntax::Span;
use crate::utils::NonZeroExt; use crate::utils::NonZeroExt;
use crate::visualize::{Paint, Stroke}; 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. /// A grid entry.
#[derive(Clone)] #[derive(Clone)]
pub(super) enum Entry { pub(super) enum Entry {

View File

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

View File

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

View File

@ -3,9 +3,7 @@ use super::repeated::Repeatable;
use crate::diag::SourceResult; use crate::diag::SourceResult;
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::Resolve; use crate::foundations::Resolve;
use crate::layout::{ use crate::layout::{Abs, Axes, Cell, Frame, GridLayouter, Point, Regions, Size, Sizing};
Abs, Axes, Cell, Frame, GridLayouter, LayoutMultiple, Point, Regions, Size, Sizing,
};
use crate::utils::MaybeReverseIter; use crate::utils::MaybeReverseIter;
/// All information needed to layout a single rowspan. /// 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. // 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(); let (current_region, current_rrows) = current_region_data.unzip();
for ((i, finished), frame) in self for ((i, finished), frame) in self
.finished .finished

View File

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

View File

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

View File

@ -5,7 +5,7 @@ use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
dict, func, Content, Context, Dict, Resolve, Smart, StyleChain, Styles, 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; use crate::syntax::Span;
/// Measures the layouted size of content. /// Measures the layouted size of content.

View File

@ -58,7 +58,7 @@ pub use self::page::*;
pub use self::place::*; pub use self::place::*;
pub use self::point::*; pub use self::point::*;
pub use self::ratio::*; pub use self::ratio::*;
pub use self::regions::Regions; pub use self::regions::*;
pub use self::rel::*; pub use self::rel::*;
pub use self::repeat::*; pub use self::repeat::*;
pub use self::sides::*; pub use self::sides::*;
@ -119,72 +119,15 @@ pub fn define(global: &mut Scope) {
global.define_func::<layout>(); global.define_func::<layout>();
} }
/// Root-level layout. impl Content {
/// /// Layout the content into a document.
/// 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.
/// ///
/// This element must be layouted again in the same order for the results to /// This first realizes the content into a
/// be valid. /// [`DocumentElem`][crate::model::DocumentElem], which is then laid out. In
fn measure( /// contrast to [`layout`](Self::layout()), this does not take regions since
&self, /// the regions are defined by the page configuration in the content and
engine: &mut Engine, /// style chain.
styles: StyleChain, pub fn layout_document(
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(
&self, &self,
engine: &mut Engine, engine: &mut Engine,
styles: StyleChain, styles: StyleChain,
@ -209,7 +152,7 @@ impl LayoutRoot for Content {
}; };
let arenas = Arenas::default(); let arenas = Arenas::default();
let (document, styles) = realize_doc(&mut engine, &arenas, content, styles)?; let (document, styles) = realize_doc(&mut engine, &arenas, content, styles)?;
document.layout_root(&mut engine, styles) document.layout(&mut engine, styles)
} }
cached( cached(
@ -222,10 +165,25 @@ impl LayoutRoot for Content {
styles, styles,
) )
} }
}
impl LayoutMultiple for Content { /// Layout the content into the given regions.
fn layout( 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, &self,
engine: &mut Engine, engine: &mut Engine,
styles: StyleChain, styles: StyleChain,
@ -271,7 +229,7 @@ impl LayoutMultiple for Content {
flow.layout(&mut engine, styles, regions) flow.layout(&mut engine, styles, regions)
} }
let fragment = cached( cached(
self, self,
engine.world, engine.world,
engine.introspector, engine.introspector,
@ -280,9 +238,6 @@ impl LayoutMultiple for Content {
TrackedMut::reborrow_mut(&mut engine.tracer), TrackedMut::reborrow_mut(&mut engine.tracer),
styles, styles,
regions, regions,
)?; )
engine.locator.visit_frames(&fragment);
Ok(fragment)
} }
} }

View File

@ -1,8 +1,10 @@
use crate::diag::SourceResult; use crate::diag::SourceResult;
use crate::engine::Engine; 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::{ 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. /// Adds spacing around content.
@ -18,7 +20,7 @@ use crate::layout::{
/// _Typing speeds can be /// _Typing speeds can be
/// measured in words per minute._ /// measured in words per minute._
/// ``` /// ```
#[elem(title = "Padding", LayoutMultiple)] #[elem(title = "Padding", Show)]
pub struct PadElem { pub struct PadElem {
/// The padding at the left side. /// The padding at the left side.
#[parse( #[parse(
@ -60,49 +62,64 @@ pub struct PadElem {
pub body: Content, pub body: Content,
} }
impl LayoutMultiple for Packed<PadElem> { impl Show for Packed<PadElem> {
#[typst_macros::time(name = "pad", span = self.span())] fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
fn layout( Ok(BlockElem::multi_layouter(self.clone(), layout_pad).pack())
&self, }
}
/// Layout the padded content.
#[typst_macros::time(span = elem.span())]
fn layout_pad(
elem: &Packed<PadElem>,
engine: &mut Engine, engine: &mut Engine,
styles: StyleChain, styles: StyleChain,
regions: Regions, regions: Regions,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
let sides = Sides::new( let padding = Sides::new(
self.left(styles), elem.left(styles).resolve(styles),
self.top(styles), elem.top(styles).resolve(styles),
self.right(styles), elem.right(styles).resolve(styles),
self.bottom(styles), elem.bottom(styles).resolve(styles),
); );
// Layout child into padded regions.
let mut backlog = vec![]; let mut backlog = vec![];
let padding = sides.resolve(styles); let pod = regions.map(&mut backlog, |size| shrink(size, &padding));
let pod = regions.map(&mut backlog, |size| shrink(size, padding));
let mut fragment = self.body().layout(engine, styles, pod)?; // Layout child into padded regions.
let mut fragment = elem.body().layout(engine, styles, pod)?;
for frame in &mut fragment { for frame in &mut fragment {
// Apply the padding inversely such that the grown size padded grow(frame, &padding);
// 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) Ok(fragment)
}
/// 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));
} }
/// Shrink a size by padding relative to the size itself. /// Grow a frame's size by an inset relative to the grown size.
fn shrink(size: Size, padding: Sides<Rel<Abs>>) -> Size {
size - padding.relative_to(size).sum_by_axis()
}
/// Grow a size by padding relative to the grown size.
/// This is the inverse operation to `shrink()`. /// This is the inverse operation to `shrink()`.
/// ///
/// For the horizontal axis the derivation looks as follows. /// 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, /// Let w be the grown target width,
/// s be the given width, /// s be the given width,
/// l be the left padding, /// l be the left inset,
/// r be the right padding, /// r be the right inset,
/// p = l + r. /// p = l + r.
/// ///
/// We want that: w - l.resolve(w) - r.resolve(w) = s /// 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 /// <=> w - p.rel * w - p.abs = s
/// <=> (1 - p.rel) * w = s + p.abs /// <=> (1 - p.rel) * w = s + p.abs
/// <=> w = (s + p.abs) / (1 - p.rel) /// <=> w = (s + p.abs) / (1 - p.rel)
fn grow(size: Size, padding: Sides<Rel<Abs>>) -> Size { pub(crate) fn grow(frame: &mut Frame, inset: &Sides<Rel<Abs>>) {
size.zip_map(padding.sum_by_axis(), |s, p| (s + p.abs) / (1.0 - p.rel.get())) // 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::introspection::{Counter, CounterDisplayElem, CounterKey, ManualPageCounter};
use crate::layout::{ use crate::layout::{
Abs, AlignElem, Alignment, Axes, ColumnsElem, Dir, Frame, HAlignment, LayoutMultiple, Abs, AlignElem, Alignment, Axes, ColumnsElem, Dir, Frame, HAlignment, Length,
Length, OuterVAlignment, Point, Ratio, Regions, Rel, Sides, Size, SpecificAlignment, OuterVAlignment, Point, Ratio, Regions, Rel, Sides, Size, SpecificAlignment,
VAlignment, VAlignment,
}; };

View File

@ -2,7 +2,7 @@ use crate::diag::{bail, At, Hint, SourceResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{elem, scope, Content, Packed, Smart, StyleChain, Unlabellable}; use crate::foundations::{elem, scope, Content, Packed, Smart, StyleChain, Unlabellable};
use crate::layout::{ 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}; use crate::realize::{Behave, Behaviour};

View File

@ -2,6 +2,28 @@ use std::fmt::{self, Debug, Formatter};
use crate::layout::{Abs, Axes, Size}; 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 sequence of regions to layout into.
/// ///
/// A *region* is a contiguous rectangular space in which elements /// A *region* is a contiguous rectangular space in which elements
@ -80,7 +102,7 @@ impl Regions<'_> {
backlog, backlog,
last: self.last.map(|y| f(Size::new(x, y)).y), last: self.last.map(|y| f(Size::new(x, y)).y),
expand: self.expand, expand: self.expand,
root: false, root: self.root,
} }
} }

View File

@ -1,8 +1,10 @@
use crate::diag::{bail, SourceResult}; use crate::diag::{bail, SourceResult};
use crate::engine::Engine; 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::{ 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; use crate::utils::Numeric;
@ -27,23 +29,29 @@ use crate::utils::Numeric;
/// Berlin, the 22nd of December, 2022 /// Berlin, the 22nd of December, 2022
/// ] /// ]
/// ``` /// ```
#[elem(LayoutMultiple)] #[elem(Show)]
pub struct RepeatElem { pub struct RepeatElem {
/// The content to repeat. /// The content to repeat.
#[required] #[required]
pub body: Content, pub body: Content,
} }
impl LayoutMultiple for Packed<RepeatElem> { impl Show for Packed<RepeatElem> {
#[typst_macros::time(name = "repeat", span = self.span())] fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
fn layout( Ok(BlockElem::multi_layouter(self.clone(), layout_repeat).pack())
&self, }
}
/// Layout the repeated content.
#[typst_macros::time(span = elem.span())]
fn layout_repeat(
elem: &Packed<RepeatElem>,
engine: &mut Engine, engine: &mut Engine,
styles: StyleChain, styles: StyleChain,
regions: Regions, regions: Regions,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
let pod = Regions::one(regions.size, Axes::new(false, false)); let pod = Regions::one(regions.size, Axes::new(false, false));
let piece = self.body().layout(engine, styles, pod)?.into_frame(); let piece = elem.body().layout(engine, styles, pod)?.into_frame();
let align = AlignElem::alignment_in(styles).resolve(styles); let align = AlignElem::alignment_in(styles).resolve(styles);
let fill = regions.size.x; let fill = regions.size.x;
@ -55,7 +63,7 @@ impl LayoutMultiple for Packed<RepeatElem> {
let size = Size::new(regions.size.x, piece.height()); let size = Size::new(regions.size.x, piece.height());
if !size.is_finite() { if !size.is_finite() {
bail!(self.span(), "repeat with no size restrictions"); bail!(elem.span(), "repeat with no size restrictions");
} }
let mut frame = Frame::soft(size); let mut frame = Frame::soft(size);
@ -76,5 +84,4 @@ impl LayoutMultiple for Packed<RepeatElem> {
} }
Ok(Fragment::frame(frame)) Ok(Fragment::frame(frame))
}
} }

View File

@ -107,7 +107,7 @@ impl<T> Sides<Option<T>> {
impl Sides<Rel<Abs>> { impl Sides<Rel<Abs>> {
/// Evaluate the sides relative to the given `size`. /// 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 { Sides {
left: self.left.relative_to(size.x), left: self.left.relative_to(size.x),
top: self.top.relative_to(size.y), top: self.top.relative_to(size.y),
@ -115,6 +115,14 @@ impl Sides<Rel<Abs>> {
bottom: self.bottom.relative_to(size.y), 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> { impl<T> Get<Side> for Sides<T> {

View File

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

View File

@ -3,10 +3,12 @@ use typst_syntax::Span;
use crate::diag::{bail, SourceResult}; use crate::diag::{bail, SourceResult};
use crate::engine::Engine; 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::{ use crate::layout::{
Abs, AlignElem, Axes, Axis, Dir, FixedAlignment, Fr, Fragment, Frame, HElem, Abs, AlignElem, Axes, Axis, BlockElem, Dir, FixedAlignment, Fr, Fragment, Frame,
LayoutMultiple, Point, Regions, Size, Spacing, VElem, HElem, Point, Regions, Size, Spacing, VElem,
}; };
use crate::utils::{Get, Numeric}; use crate::utils::{Get, Numeric};
@ -24,7 +26,7 @@ use crate::utils::{Get, Numeric};
/// rect(width: 90pt), /// rect(width: 90pt),
/// ) /// )
/// ``` /// ```
#[elem(LayoutMultiple)] #[elem(Show)]
pub struct StackElem { pub struct StackElem {
/// The direction along which the items are stacked. Possible values are: /// The direction along which the items are stacked. Possible values are:
/// ///
@ -52,23 +54,56 @@ pub struct StackElem {
pub children: Vec<StackChild>, pub children: Vec<StackChild>,
} }
impl LayoutMultiple for Packed<StackElem> { impl Show for Packed<StackElem> {
#[typst_macros::time(name = "stack", span = self.span())] fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
fn layout( Ok(BlockElem::multi_layouter(self.clone(), layout_stack).pack())
&self, }
}
/// A child of a stack element.
#[derive(Clone, PartialEq, Hash)]
pub enum StackChild {
/// Spacing between other children.
Spacing(Spacing),
/// Arbitrary block-level content.
Block(Content),
}
impl Debug for StackChild {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::Spacing(kind) => kind.fmt(f),
Self::Block(block) => block.fmt(f),
}
}
}
cast! {
StackChild,
self => match self {
Self::Spacing(spacing) => spacing.into_value(),
Self::Block(content) => content.into_value(),
},
v: Spacing => Self::Spacing(v),
v: Content => Self::Block(v),
}
/// Layout the stack.
#[typst_macros::time(span = elem.span())]
fn layout_stack(
elem: &Packed<StackElem>,
engine: &mut Engine, engine: &mut Engine,
styles: StyleChain, styles: StyleChain,
regions: Regions, regions: Regions,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
let mut layouter = let mut layouter = StackLayouter::new(elem.span(), elem.dir(styles), regions, styles);
StackLayouter::new(self.span(), self.dir(styles), regions, styles);
let axis = layouter.dir.axis(); let axis = layouter.dir.axis();
// Spacing to insert before the next block. // Spacing to insert before the next block.
let spacing = self.spacing(styles); let spacing = elem.spacing(styles);
let mut deferred = None; let mut deferred = None;
for child in self.children() { for child in elem.children() {
match child { match child {
StackChild::Spacing(kind) => { StackChild::Spacing(kind) => {
layouter.layout_spacing(*kind); layouter.layout_spacing(*kind);
@ -100,35 +135,6 @@ impl LayoutMultiple for Packed<StackElem> {
} }
layouter.finish() layouter.finish()
}
}
/// A child of a stack element.
#[derive(Clone, PartialEq, Hash)]
pub enum StackChild {
/// Spacing between other children.
Spacing(Spacing),
/// Arbitrary block-level content.
Block(Content),
}
impl Debug for StackChild {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::Spacing(kind) => kind.fmt(f),
Self::Block(block) => block.fmt(f),
}
}
}
cast! {
StackChild,
self => match self {
Self::Spacing(spacing) => spacing.into_value(),
Self::Block(content) => content.into_value(),
},
v: Spacing => Self::Spacing(v),
v: Content => Self::Block(v),
} }
/// Performs stack layout. /// Performs stack layout.
@ -231,7 +237,7 @@ impl<'a> StackLayouter<'a> {
self.finish_region()?; 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>() { let align = if let Some(align) = block.to_packed::<AlignElem>() {
align.alignment(styles) align.alignment(styles)
} else if let Some(styled) = block.to_packed::<StyledElem>() { } else if let Some(styled) = block.to_packed::<StyledElem>() {

View File

@ -1,9 +1,11 @@
use crate::diag::SourceResult; use crate::diag::SourceResult;
use crate::engine::Engine; 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::{ use crate::layout::{
Abs, Alignment, Angle, Axes, FixedAlignment, Frame, HAlignment, LayoutMultiple, Abs, Alignment, Angle, Axes, BlockElem, FixedAlignment, Frame, HAlignment, Length,
LayoutSingle, Length, Point, Ratio, Regions, Rel, Size, VAlignment, Point, Ratio, Region, Regions, Rel, Size, VAlignment,
}; };
/// Moves content without affecting layout. /// Moves content without affecting layout.
@ -24,7 +26,7 @@ use crate::layout::{
/// ) /// )
/// )) /// ))
/// ``` /// ```
#[elem(LayoutSingle)] #[elem(Show)]
pub struct MoveElem { pub struct MoveElem {
/// The horizontal displacement of the content. /// The horizontal displacement of the content.
pub dx: Rel<Length>, pub dx: Rel<Length>,
@ -37,21 +39,28 @@ pub struct MoveElem {
pub body: Content, pub body: Content,
} }
impl LayoutSingle for Packed<MoveElem> { impl Show for Packed<MoveElem> {
#[typst_macros::time(name = "move", span = self.span())] fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
fn layout( Ok(BlockElem::single_layouter(self.clone(), layout_move).pack())
&self, }
}
/// Layout the moved content.
#[typst_macros::time(span = elem.span())]
fn layout_move(
elem: &Packed<MoveElem>,
engine: &mut Engine, engine: &mut Engine,
styles: StyleChain, styles: StyleChain,
regions: Regions, region: Region,
) -> SourceResult<Frame> { ) -> SourceResult<Frame> {
let pod = Regions::one(regions.base(), Axes::splat(false)); let mut frame = elem
let mut frame = self.body().layout(engine, styles, pod)?.into_frame(); .body()
let delta = Axes::new(self.dx(styles), self.dy(styles)).resolve(styles); .layout(engine, styles, region.into_regions())?
let delta = delta.zip_map(regions.base(), Rel::relative_to); .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()); frame.translate(delta.to_point());
Ok(frame) Ok(frame)
}
} }
/// Rotates content without affecting layout. /// Rotates content without affecting layout.
@ -68,7 +77,7 @@ impl LayoutSingle for Packed<MoveElem> {
/// .map(i => rotate(24deg * i)[X]), /// .map(i => rotate(24deg * i)[X]),
/// ) /// )
/// ``` /// ```
#[elem(LayoutSingle)] #[elem(Show)]
pub struct RotateElem { pub struct RotateElem {
/// The amount of rotation. /// The amount of rotation.
/// ///
@ -115,20 +124,26 @@ pub struct RotateElem {
pub body: Content, pub body: Content,
} }
impl LayoutSingle for Packed<RotateElem> { impl Show for Packed<RotateElem> {
#[typst_macros::time(name = "rotate", span = self.span())] fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
fn layout( Ok(BlockElem::single_layouter(self.clone(), layout_rotate).pack())
&self, }
}
/// Layout the rotated content.
#[typst_macros::time(span = elem.span())]
fn layout_rotate(
elem: &Packed<RotateElem>,
engine: &mut Engine, engine: &mut Engine,
styles: StyleChain, styles: StyleChain,
regions: Regions, region: Region,
) -> SourceResult<Frame> { ) -> SourceResult<Frame> {
let angle = self.angle(styles); let angle = elem.angle(styles);
let align = self.origin(styles).resolve(styles); let align = elem.origin(styles).resolve(styles);
// Compute the new region's approximate size. // Compute the new region's approximate size.
let size = regions let size = region
.base() .size
.to_point() .to_point()
.transform_inf(Transform::rotate(angle)) .transform_inf(Transform::rotate(angle))
.map(Abs::abs) .map(Abs::abs)
@ -136,15 +151,14 @@ impl LayoutSingle for Packed<RotateElem> {
measure_and_layout( measure_and_layout(
engine, engine,
regions.base(), region,
size, size,
styles, styles,
self.body(), elem.body(),
Transform::rotate(angle), Transform::rotate(angle),
align, align,
self.reflow(styles), elem.reflow(styles),
) )
}
} }
/// Scales content without affecting layout. /// Scales content without affecting layout.
@ -157,7 +171,7 @@ impl LayoutSingle for Packed<RotateElem> {
/// #scale(x: -100%)[This is mirrored.] /// #scale(x: -100%)[This is mirrored.]
/// #scale(x: -100%, reflow: true)[This is mirrored.] /// #scale(x: -100%, reflow: true)[This is mirrored.]
/// ``` /// ```
#[elem(LayoutSingle)] #[elem(Show)]
pub struct ScaleElem { pub struct ScaleElem {
/// The horizontal scaling factor. /// The horizontal scaling factor.
/// ///
@ -203,35 +217,37 @@ pub struct ScaleElem {
pub body: Content, pub body: Content,
} }
impl LayoutSingle for Packed<ScaleElem> { impl Show for Packed<ScaleElem> {
#[typst_macros::time(name = "scale", span = self.span())] fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
fn layout( Ok(BlockElem::single_layouter(self.clone(), layout_scale).pack())
&self, }
}
/// Layout the scaled content.
#[typst_macros::time(span = elem.span())]
fn layout_scale(
elem: &Packed<ScaleElem>,
engine: &mut Engine, engine: &mut Engine,
styles: StyleChain, styles: StyleChain,
regions: Regions, region: Region,
) -> SourceResult<Frame> { ) -> SourceResult<Frame> {
let sx = self.x(styles); let sx = elem.x(styles);
let sy = self.y(styles); let sy = elem.y(styles);
let align = self.origin(styles).resolve(styles); let align = elem.origin(styles).resolve(styles);
// Compute the new region's approximate size. // Compute the new region's approximate size.
let size = regions let size = region.size.zip_map(Axes::new(sx, sy), |r, s| s.of(r)).map(Abs::abs);
.base()
.zip_map(Axes::new(sx, sy), |r, s| s.of(r))
.map(Abs::abs);
measure_and_layout( measure_and_layout(
engine, engine,
regions.base(), region,
size, size,
styles, styles,
self.body(), elem.body(),
Transform::scale(sx, sy), Transform::scale(sx, sy),
align, align,
self.reflow(styles), elem.reflow(styles),
) )
}
} }
/// A scale-skew-translate transformation. /// A scale-skew-translate transformation.
@ -363,7 +379,7 @@ impl Default for Transform {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn measure_and_layout( fn measure_and_layout(
engine: &mut Engine, engine: &mut Engine,
base_size: Size, region: Region,
size: Size, size: Size,
styles: StyleChain, styles: StyleChain,
body: &Content, body: &Content,
@ -371,21 +387,7 @@ fn measure_and_layout(
align: Axes<FixedAlignment>, align: Axes<FixedAlignment>,
reflow: bool, reflow: bool,
) -> SourceResult<Frame> { ) -> SourceResult<Frame> {
if !reflow { if reflow {
// Layout the body.
let pod = Regions::one(base_size, Axes::splat(false));
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));
frame.transform(ts);
return Ok(frame);
}
// Measure the size of the body. // Measure the size of the body.
let pod = Regions::one(size, Axes::splat(false)); let pod = Regions::one(size, Axes::splat(false));
let frame = body.measure(engine, styles, pod)?.into_frame(); let frame = body.measure(engine, styles, pod)?.into_frame();
@ -395,7 +397,7 @@ fn measure_and_layout(
let mut frame = body.layout(engine, styles, pod)?.into_frame(); let mut frame = body.layout(engine, styles, pod)?.into_frame();
let Axes { x, y } = align.zip_map(frame.size(), FixedAlignment::position); let Axes { x, y } = align.zip_map(frame.size(), FixedAlignment::position);
// Apply the transform. // Compute the transform.
let ts = Transform::translate(x, y) let ts = Transform::translate(x, y)
.pre_concat(transform) .pre_concat(transform)
.pre_concat(Transform::translate(-x, -y)); .pre_concat(Transform::translate(-x, -y));
@ -406,6 +408,20 @@ fn measure_and_layout(
frame.translate(offset); frame.translate(offset);
frame.set_size(size); frame.set_size(size);
Ok(frame) 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);
// 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)
}
} }
/// Computes the bounding box and offset of a transformed frame. /// Computes the bounding box and offset of a transformed frame.

View File

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

View File

@ -12,7 +12,7 @@ use unicode_segmentation::UnicodeSegmentation;
use crate::diag::SourceResult; use crate::diag::SourceResult;
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{Content, Packed, StyleChain}; 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::{ use crate::math::{
scaled_font_size, styled_char, EquationElem, FrameFragment, GlyphFragment, scaled_font_size, styled_char, EquationElem, FrameFragment, GlyphFragment,
LayoutMath, MathFragment, MathRun, MathSize, THICK, LayoutMath, MathFragment, MathRun, MathSize, THICK,
@ -65,7 +65,7 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> {
pub fn new( pub fn new(
engine: &'v mut Engine<'b>, engine: &'v mut Engine<'b>,
styles: StyleChain<'a>, styles: StyleChain<'a>,
regions: Regions, base: Size,
font: &'a Font, font: &'a Font,
) -> Self { ) -> Self {
let math_table = font.ttf().tables().math.unwrap(); let math_table = font.ttf().tables().math.unwrap();
@ -102,7 +102,7 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> {
Self { Self {
engine, engine,
regions: Regions::one(regions.base(), Axes::splat(false)), regions: Regions::one(base, Axes::splat(false)),
font, font,
ttf: font.ttf(), ttf: font.ttf(),
table: math_table, table: math_table,
@ -173,7 +173,7 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> {
) -> SourceResult<Frame> { ) -> SourceResult<Frame> {
let local = let local =
TextElem::set_size(TextSize(scaled_font_size(self, styles).into())).wrap(); 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`]. /// 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::diag::{bail, SourceResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
elem, Content, NativeElement, Packed, Resolve, ShowSet, Smart, StyleChain, Styles, elem, Content, NativeElement, Packed, Resolve, Show, ShowSet, Smart, StyleChain,
Synthesize, Styles, Synthesize,
}; };
use crate::introspection::{Count, Counter, CounterUpdate, Locatable}; use crate::introspection::{Count, Counter, CounterUpdate, Locatable};
use crate::layout::{ use crate::layout::{
Abs, AlignElem, Alignment, Axes, BlockElem, Em, FixedAlignment, Fragment, Frame, 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::{ use crate::math::{
scaled_font_size, LayoutMath, MathContext, MathRunFrameBuilder, MathSize, MathVariant, scaled_font_size, LayoutMath, MathContext, MathRunFrameBuilder, MathSize, MathVariant,
@ -48,14 +49,7 @@ use crate::World;
/// horizontally. For more details about math syntax, see the /// horizontally. For more details about math syntax, see the
/// [main math page]($category/math). /// [main math page]($category/math).
#[elem( #[elem(
Locatable, Locatable, Synthesize, Show, ShowSet, LayoutMath, Count, LocalName, Refable,
Synthesize,
ShowSet,
LayoutMultiple,
LayoutMath,
Count,
LocalName,
Refable,
Outlinable Outlinable
)] )]
pub struct EquationElem { 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> { impl ShowSet for Packed<EquationElem> {
fn show_set(&self, styles: StyleChain) -> Styles { fn show_set(&self, styles: StyleChain) -> Styles {
let mut out = Styles::new(); 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> { impl Count for Packed<EquationElem> {
fn update(&self) -> Option<CounterUpdate> { fn update(&self) -> Option<CounterUpdate> {
(self.block(StyleChain::default()) && self.numbering().is_some()) (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( fn find_math_font(
engine: &mut Engine<'_>, engine: &mut Engine<'_>,
styles: StyleChain, styles: StyleChain,

View File

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

View File

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

View File

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

View File

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

View File

@ -14,8 +14,8 @@ use crate::introspection::{
Count, Counter, CounterKey, CounterUpdate, Locatable, Location, Count, Counter, CounterKey, CounterUpdate, Locatable, Location,
}; };
use crate::layout::{ use crate::layout::{
AlignElem, Alignment, BlockElem, Em, HAlignment, Length, OuterVAlignment, PlaceElem, AlignElem, Alignment, BlockChild, BlockElem, Em, HAlignment, Length, OuterVAlignment,
VAlignment, VElem, PlaceElem, VAlignment, VElem,
}; };
use crate::model::{Numbering, NumberingPattern, Outlinable, Refable, Supplement}; use crate::model::{Numbering, NumberingPattern, Outlinable, Refable, Supplement};
use crate::text::{Lang, Region, TextElem}; use crate::text::{Lang, Region, TextElem};
@ -317,7 +317,10 @@ impl Show for Packed<FigureElem> {
} }
// Wrap the contents in a block. // 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. // Wrap in a float.
if let Some(align) = self.placement(styles) { 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::introspection::{Count, Counter, CounterUpdate, Locatable};
use crate::layout::{ 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::model::{Numbering, Outlinable, ParElem, Refable, Supplement};
use crate::text::{FontWeight, LocalName, SpaceElem, TextElem, TextSize}; 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())); 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::diag::{bail, SourceResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
cast, elem, scope, Array, Content, Context, Depth, Func, Packed, Smart, StyleChain, cast, elem, scope, Array, Content, Context, Depth, Func, NativeElement, Packed, Show,
Value, Smart, StyleChain, Value,
}; };
use crate::layout::{ use crate::layout::{
Axes, BlockElem, Cell, CellGrid, Em, Fragment, GridLayouter, HAlignment, Axes, BlockElem, Cell, CellGrid, Em, Fragment, GridLayouter, HAlignment, Length,
LayoutMultiple, Length, Regions, Sizing, Spacing, VAlignment, Regions, Sizing, Spacing, VAlignment, VElem,
}; };
use crate::model::ParElem; use crate::model::ParElem;
use crate::text::TextElem; 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 /// 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 /// paragraphs and other block-level content. All content that is indented
/// more than an item's marker becomes part of that item. /// 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 { pub struct ListElem {
/// If this is `{false}`, the items are spaced apart with /// If this is `{false}`, the items are spaced apart with
/// [list spacing]($list.spacing). If it is `{true}`, they use normal /// [list spacing]($list.spacing). If it is `{true}`, they use normal
@ -137,38 +137,50 @@ impl ListElem {
type ListItem; type ListItem;
} }
impl LayoutMultiple for Packed<ListElem> { impl Show for Packed<ListElem> {
#[typst_macros::time(name = "list", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
fn layout( let mut realized = BlockElem::multi_layouter(self.clone(), layout_list).pack();
&self,
if self.tight(styles) {
let leading = ParElem::leading_in(styles);
let spacing = VElem::list_attach(leading.into()).pack();
realized = spacing + realized;
}
Ok(realized)
}
}
/// Layout the list.
#[typst_macros::time(span = elem.span())]
fn layout_list(
elem: &Packed<ListElem>,
engine: &mut Engine, engine: &mut Engine,
styles: StyleChain, styles: StyleChain,
regions: Regions, regions: Regions,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
let indent = self.indent(styles); let indent = elem.indent(styles);
let body_indent = self.body_indent(styles); let body_indent = elem.body_indent(styles);
let gutter = if self.tight(styles) { let gutter = if elem.tight(styles) {
ParElem::leading_in(styles).into() ParElem::leading_in(styles).into()
} else { } else {
self.spacing(styles) elem.spacing(styles)
.unwrap_or_else(|| *BlockElem::below_in(styles).amount()) .unwrap_or_else(|| *BlockElem::below_in(styles).amount())
}; };
let Depth(depth) = ListElem::depth_in(styles); let Depth(depth) = ListElem::depth_in(styles);
let marker = self let marker = elem
.marker(styles) .marker(styles)
.resolve(engine, styles, depth)? .resolve(engine, styles, depth)?
// avoid '#set align' interference with the list // avoid '#set align' interference with the list
.aligned(HAlignment::Start + VAlignment::Top); .aligned(HAlignment::Start + VAlignment::Top);
let mut cells = vec![]; let mut cells = vec![];
for item in self.children() { for item in elem.children() {
cells.push(Cell::from(Content::empty())); cells.push(Cell::from(Content::empty()));
cells.push(Cell::from(marker.clone())); cells.push(Cell::from(marker.clone()));
cells.push(Cell::from(Content::empty())); cells.push(Cell::from(Content::empty()));
cells.push(Cell::from( cells.push(Cell::from(item.body().clone().styled(ListElem::set_depth(Depth(1)))));
item.body().clone().styled(ListElem::set_depth(Depth(1))),
));
} }
let grid = CellGrid::new( let grid = CellGrid::new(
@ -181,10 +193,9 @@ impl LayoutMultiple for Packed<ListElem> {
Axes::with_y(&[gutter.into()]), Axes::with_y(&[gutter.into()]),
cells, cells,
); );
let layouter = GridLayouter::new(&grid, regions, styles, self.span()); let layouter = GridLayouter::new(&grid, regions, styles, elem.span());
layouter.layout(engine) layouter.layout(engine)
}
} }
/// A bullet list item. /// A bullet list item.

View File

@ -4,7 +4,9 @@ use crate::foundations::{
cast, elem, Content, Depth, Label, NativeElement, Packed, Show, ShowSet, Smart, cast, elem, Content, Depth, Label, NativeElement, Packed, Show, ShowSet, Smart,
StyleChain, Styles, 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::model::{CitationForm, CiteElem};
use crate::text::{SmartQuoteElem, SmartQuotes, SpaceElem, TextElem}; use crate::text::{SmartQuoteElem, SmartQuotes, SpaceElem, TextElem};
@ -181,8 +183,10 @@ impl Show for Packed<QuoteElem> {
} }
if block { if block {
realized = realized = BlockElem::new()
BlockElem::new().with_body(Some(realized)).pack().spanned(self.span()); .with_body(Some(BlockChild::Content(realized)))
.pack()
.spanned(self.span());
if let Some(attribution) = self.attribution(styles).as_ref() { if let Some(attribution) = self.attribution(styles).as_ref() {
let mut seq = vec![TextElem::packed('—'), SpaceElem::new().pack()]; 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::diag::{bail, SourceResult, StrResult, Trace, Tracepoint};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ 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::{ use crate::layout::{
show_grid_cell, Abs, Alignment, Axes, Cell, CellGrid, Celled, Dir, Fragment, show_grid_cell, Abs, Alignment, Axes, BlockElem, Cell, CellGrid, Celled, Dir,
GridCell, GridFooter, GridHLine, GridHeader, GridLayouter, GridVLine, LayoutMultiple, Fragment, GridCell, GridFooter, GridHLine, GridHeader, GridLayouter, GridVLine,
Length, LinePosition, OuterHAlignment, OuterVAlignment, Regions, Rel, ResolvableCell, Length, LinePosition, OuterHAlignment, OuterVAlignment, Regions, Rel, ResolvableCell,
ResolvableGridChild, ResolvableGridItem, Sides, TrackSizings, ResolvableGridChild, ResolvableGridItem, Sides, TrackSizings,
}; };
@ -120,7 +120,7 @@ use crate::visualize::{Paint, Stroke};
/// [Robert], b, a, b, /// [Robert], b, a, b,
/// ) /// )
/// ``` /// ```
#[elem(scope, LayoutMultiple, LocalName, Figurable)] #[elem(scope, Show, LocalName, Figurable)]
pub struct TableElem { pub struct TableElem {
/// The column sizes. See the [grid documentation]($grid) for more /// The column sizes. See the [grid documentation]($grid) for more
/// information on track sizing. /// information on track sizing.
@ -260,29 +260,35 @@ impl TableElem {
type TableFooter; type TableFooter;
} }
impl LayoutMultiple for Packed<TableElem> { impl Show for Packed<TableElem> {
#[typst_macros::time(name = "table", span = self.span())] fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
fn layout( Ok(BlockElem::multi_layouter(self.clone(), layout_table).pack())
&self, }
}
/// Layout the table.
#[typst_macros::time(span = elem.span())]
fn layout_table(
elem: &Packed<TableElem>,
engine: &mut Engine, engine: &mut Engine,
styles: StyleChain, styles: StyleChain,
regions: Regions, regions: Regions,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
let inset = self.inset(styles); let inset = elem.inset(styles);
let align = self.align(styles); let align = elem.align(styles);
let columns = self.columns(styles); let columns = elem.columns(styles);
let rows = self.rows(styles); let rows = elem.rows(styles);
let column_gutter = self.column_gutter(styles); let column_gutter = elem.column_gutter(styles);
let row_gutter = self.row_gutter(styles); let row_gutter = elem.row_gutter(styles);
let fill = self.fill(styles); let fill = elem.fill(styles);
let stroke = self.stroke(styles); let stroke = elem.stroke(styles);
let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice()); 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()); 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 // Use trace to link back to the table when a specific cell errors
let tracepoint = || Tracepoint::Call(Some(eco_format!("table"))); let tracepoint = || Tracepoint::Call(Some(eco_format!("table")));
let resolve_item = |item: &TableItem| item.to_resolvable(styles); let resolve_item = |item: &TableItem| item.to_resolvable(styles);
let children = self.children().iter().map(|child| match child { let children = elem.children().iter().map(|child| match child {
TableChild::Header(header) => ResolvableGridChild::Header { TableChild::Header(header) => ResolvableGridChild::Header {
repeat: header.repeat(styles), repeat: header.repeat(styles),
span: header.span(), span: header.span(),
@ -293,9 +299,7 @@ impl LayoutMultiple for Packed<TableElem> {
span: footer.span(), span: footer.span(),
items: footer.children().iter().map(resolve_item), items: footer.children().iter().map(resolve_item),
}, },
TableChild::Item(item) => { TableChild::Item(item) => ResolvableGridChild::Item(item.to_resolvable(styles)),
ResolvableGridChild::Item(item.to_resolvable(styles))
}
}); });
let grid = CellGrid::resolve( let grid = CellGrid::resolve(
tracks, tracks,
@ -307,13 +311,12 @@ impl LayoutMultiple for Packed<TableElem> {
&stroke, &stroke,
engine, engine,
styles, styles,
self.span(), elem.span(),
) )
.trace(engine.world, tracepoint, self.span())?; .trace(engine.world, tracepoint, elem.span())?;
let layouter = GridLayouter::new(&grid, regions, styles, self.span()); let layouter = GridLayouter::new(&grid, regions, styles, elem.span());
layouter.layout(engine) layouter.layout(engine)
}
} }
impl LocalName for Packed<TableElem> { impl LocalName for Packed<TableElem> {

View File

@ -1,11 +1,10 @@
use crate::diag::{bail, SourceResult}; use crate::diag::{bail, SourceResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ 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::{ use crate::layout::{
BlockElem, Dir, Em, Fragment, HElem, LayoutMultiple, Length, Regions, Sides, Spacing, BlockElem, Dir, Em, HElem, Length, Sides, Spacing, StackChild, StackElem, VElem,
StackChild, StackElem,
}; };
use crate::model::ParElem; use crate::model::ParElem;
use crate::text::TextElem; use crate::text::TextElem;
@ -27,7 +26,7 @@ use crate::utils::Numeric;
/// # Syntax /// # Syntax
/// This function also has dedicated syntax: Starting a line with a slash, /// 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. /// 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 { pub struct TermsElem {
/// If this is `{false}`, the items are spaced apart with /// If this is `{false}`, the items are spaced apart with
/// [term list spacing]($terms.spacing). If it is `{true}`, they use normal /// [term list spacing]($terms.spacing). If it is `{true}`, they use normal
@ -109,14 +108,8 @@ impl TermsElem {
type TermItem; type TermItem;
} }
impl LayoutMultiple for Packed<TermsElem> { impl Show for Packed<TermsElem> {
#[typst_macros::time(name = "terms", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
fn layout(
&self,
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
let separator = self.separator(styles); let separator = self.separator(styles);
let indent = self.indent(styles); let indent = self.indent(styles);
let hanging_indent = self.hanging_indent(styles); let hanging_indent = self.hanging_indent(styles);
@ -148,11 +141,18 @@ impl LayoutMultiple for Packed<TermsElem> {
padding.right = pad.into(); padding.right = pad.into();
} }
StackElem::new(children) let mut realized = StackElem::new(children)
.with_spacing(Some(gutter)) .with_spacing(Some(gutter))
.pack() .pack()
.padded(padding) .padded(padding);
.layout(engine, styles, regions)
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::introspection::TagElem;
use crate::layout::{ use crate::layout::{
AlignElem, BlockElem, BoxElem, ColbreakElem, FlowElem, FlushElem, HElem, AlignElem, BlockElem, BoxElem, ColbreakElem, FlowElem, FlushElem, HElem, InlineElem,
LayoutMultiple, LayoutSingle, PageElem, PagebreakElem, Parity, PlaceElem, VElem, PageElem, PagebreakElem, Parity, PlaceElem, VElem,
}; };
use crate::math::{EquationElem, LayoutMath}; use crate::math::{EquationElem, LayoutMath};
use crate::model::{ use crate::model::{
@ -377,8 +377,14 @@ impl<'a> FlowBuilder<'a> {
let last_was_parbreak = self.1; let last_was_parbreak = self.1;
self.1 = false; self.1 = false;
if content.is::<VElem>() if let Some(elem) = content.to_packed::<VElem>() {
|| content.is::<ColbreakElem>() if !elem.attach(styles) || !last_was_parbreak {
self.0.push(content, styles);
}
return true;
}
if content.is::<ColbreakElem>()
|| content.is::<TagElem>() || content.is::<TagElem>()
|| content.is::<PlaceElem>() || content.is::<PlaceElem>()
|| content.is::<FlushElem>() || content.is::<FlushElem>()
@ -387,35 +393,17 @@ impl<'a> FlowBuilder<'a> {
return true; return true;
} }
if content.can::<dyn LayoutSingle>() if let Some(elem) = content.to_packed::<BlockElem>() {
|| content.can::<dyn LayoutMultiple>() self.0.push(arenas.store(elem.above(styles).pack()), styles);
|| content.is::<ParElem>() self.0.push(content, styles);
{ self.0.push(arenas.store(elem.below(styles).pack()), styles);
let is_tight_list = if let Some(elem) = content.to_packed::<ListElem>() { return true;
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>() { if content.is::<ParElem>() {
(block.above(styles), block.below(styles)) self.0.push(arenas.store(BlockElem::above_in(styles).pack()), styles);
} else {
(BlockElem::above_in(styles), BlockElem::below_in(styles))
};
self.0.push(arenas.store(above.pack()), styles);
self.0.push(content, styles); self.0.push(content, styles);
self.0.push(arenas.store(below.pack()), styles); self.0.push(arenas.store(BlockElem::below_in(styles).pack()), styles);
return true; return true;
} }
@ -452,9 +440,7 @@ impl<'a> ParBuilder<'a> {
|| content.is::<HElem>() || content.is::<HElem>()
|| content.is::<LinebreakElem>() || content.is::<LinebreakElem>()
|| content.is::<SmartQuoteElem>() || content.is::<SmartQuoteElem>()
|| content || content.is::<InlineElem>()
.to_packed::<EquationElem>()
.is_some_and(|elem| !elem.block(styles))
|| content.is::<BoxElem>() || content.is::<BoxElem>()
{ {
self.0.push(content, styles); 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 (top, bottom) = determine_edges(text, *top_edge, *bottom_edge);
let size = Size::new(width + 2.0 * deco.extent, top - bottom); 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); let origin = Point::new(pos.x - deco.extent, pos.y - top - shift);
frame.prepend_multiple( frame.prepend_multiple(
rects rects

View File

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

View File

@ -16,12 +16,12 @@ use ecow::EcoString;
use crate::diag::{bail, At, SourceResult, StrResult}; use crate::diag::{bail, At, SourceResult, StrResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ 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, StyleChain,
}; };
use crate::layout::{ use crate::layout::{
Abs, Axes, FixedAlignment, Frame, FrameItem, LayoutSingle, Length, Point, Regions, Abs, Axes, BlockElem, FixedAlignment, Frame, FrameItem, Length, Point, Region, Rel,
Rel, Size, Size,
}; };
use crate::loading::Readable; use crate::loading::Readable;
use crate::model::Figurable; 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 /// [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 { pub struct ImageElem {
/// Path to an image file. /// Path to an image file.
#[required] #[required]
@ -154,88 +154,83 @@ impl ImageElem {
} }
} }
impl LayoutSingle for Packed<ImageElem> { impl Show for Packed<ImageElem> {
#[typst_macros::time(name = "image", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
fn layout( Ok(BlockElem::single_layouter(self.clone(), layout_image)
&self, .with_width(self.width(styles))
.with_height(self.height(styles))
.pack())
}
}
impl LocalName for Packed<ImageElem> {
const KEY: &'static str = "figure";
}
impl Figurable for Packed<ImageElem> {}
/// Layout the image.
#[typst_macros::time(span = elem.span())]
fn layout_image(
elem: &Packed<ImageElem>,
engine: &mut Engine, engine: &mut Engine,
styles: StyleChain, styles: StyleChain,
regions: Regions, region: Region,
) -> SourceResult<Frame> { ) -> SourceResult<Frame> {
let span = elem.span();
// Take the format that was explicitly defined, or parse the extension, // Take the format that was explicitly defined, or parse the extension,
// or try to detect the format. // or try to detect the format.
let data = self.data(); let data = elem.data();
let format = match self.format(styles) { let format = match elem.format(styles) {
Smart::Custom(v) => v, Smart::Custom(v) => v,
Smart::Auto => { Smart::Auto => determine_format(elem.path().as_str(), data).at(span)?,
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"),
},
},
}
}
}; };
// Construct the image itself.
let image = Image::with_fonts( let image = Image::with_fonts(
data.clone().into(), data.clone().into(),
format, format,
self.alt(styles), elem.alt(styles),
engine.world, engine.world,
&families(styles).map(|s| s.into()).collect::<Vec<_>>(), &families(styles).map(|s| s.into()).collect::<Vec<_>>(),
) )
.at(self.span())?; .at(span)?;
let sizing = Axes::new(self.width(styles), self.height(styles)); // Determine the image's pixel aspect ratio.
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 pxw = image.width();
let pxh = image.height(); let pxh = image.height();
let px_ratio = pxw / pxh; 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; let wide = px_ratio > region_ratio;
// The space into which the image will be placed according to its fit. // The space into which the image will be placed according to its fit.
let target = if expand.x && expand.y { let target = if region.expand.x && region.expand.y {
// If both width and height are forced, take them. // If both width and height are forced, take them.
region region.size
} else if expand.x { } else if region.expand.x {
// If just width is forced, take it. // If just width is forced, take it.
Size::new(region.x, region.y.min(region.x / px_ratio)) Size::new(region.size.x, region.size.y.min(region.size.x / px_ratio))
} else if expand.y { } else if region.expand.y {
// If just height is forced, take it. // If just height is forced, take it.
Size::new(region.x.min(region.y * px_ratio), region.y) Size::new(region.size.x.min(region.size.y * px_ratio), region.size.y)
} else { } else {
// If neither is forced, take the natural image size at the image's // If neither is forced, take the natural image size at the image's
// DPI bounded by the available space. // DPI bounded by the available space.
let dpi = image.dpi().unwrap_or(Image::DEFAULT_DPI); let dpi = image.dpi().unwrap_or(Image::DEFAULT_DPI);
let natural = Axes::new(pxw, pxh).map(|v| Abs::inches(v / dpi)); let natural = Axes::new(pxw, pxh).map(|v| Abs::inches(v / dpi));
Size::new( Size::new(
natural.x.min(region.x).min(region.y * px_ratio), natural.x.min(region.size.x).min(region.size.y * px_ratio),
natural.y.min(region.y).min(region.x / px_ratio), natural.y.min(region.size.y).min(region.size.x / px_ratio),
) )
}; };
// Compute the actual size of the fitted image. // Compute the actual size of the fitted image.
let fit = self.fit(styles); let fit = elem.fit(styles);
let fitted = match fit { let fitted = match fit {
ImageFit::Cover | ImageFit::Contain => { ImageFit::Cover | ImageFit::Contain => {
if wide == (fit == ImageFit::Contain) { if wide == (fit == ImageFit::Contain) {
@ -251,7 +246,7 @@ impl LayoutSingle for Packed<ImageElem> {
// the frame to the target size, center aligning the image in the // the frame to the target size, center aligning the image in the
// process. // process.
let mut frame = Frame::soft(fitted); let mut frame = Frame::soft(fitted);
frame.push(Point::zero(), FrameItem::Image(image, fitted, self.span())); frame.push(Point::zero(), FrameItem::Image(image, fitted, span));
frame.resize(target, Axes::splat(FixedAlignment::Center)); frame.resize(target, Axes::splat(FixedAlignment::Center));
// Create a clipping group if only part of the image should be visible. // Create a clipping group if only part of the image should be visible.
@ -260,14 +255,30 @@ impl LayoutSingle for Packed<ImageElem> {
} }
Ok(frame) Ok(frame)
}
} }
impl LocalName for Packed<ImageElem> { /// Determine the image format based on path and data.
const KEY: &'static str = "figure"; 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();
impl Figurable for Packed<ImageElem> {} 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, /// How an image should adjust itself to a given area,
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]

View File

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

View File

@ -3,10 +3,11 @@ use kurbo::{CubicBez, ParamCurveExtrema};
use crate::diag::{bail, SourceResult}; use crate::diag::{bail, SourceResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ 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::{ 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}; use crate::visualize::{FixedStroke, Geometry, Paint, Shape, Stroke};
@ -25,7 +26,7 @@ use PathVertex::{AllControlPoints, MirroredControlPoint, Vertex};
/// ((50%, 0pt), (40pt, 0pt)), /// ((50%, 0pt), (40pt, 0pt)),
/// ) /// )
/// ``` /// ```
#[elem(LayoutSingle)] #[elem(Show)]
pub struct PathElem { pub struct PathElem {
/// How to fill the path. /// How to fill the path.
/// ///
@ -69,21 +70,25 @@ pub struct PathElem {
pub vertices: Vec<PathVertex>, pub vertices: Vec<PathVertex>,
} }
impl LayoutSingle for Packed<PathElem> { impl Show for Packed<PathElem> {
#[typst_macros::time(name = "path", span = self.span())] fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
fn layout( Ok(BlockElem::single_layouter(self.clone(), layout_path).pack())
&self, }
}
/// Layout the path.
#[typst_macros::time(span = elem.span())]
fn layout_path(
elem: &Packed<PathElem>,
_: &mut Engine, _: &mut Engine,
styles: StyleChain, styles: StyleChain,
regions: Regions, region: Region,
) -> SourceResult<Frame> { ) -> SourceResult<Frame> {
let resolve = |axes: Axes<Rel<Length>>| { let resolve = |axes: Axes<Rel<Length>>| {
axes.resolve(styles) axes.resolve(styles).zip_map(region.size, Rel::relative_to).to_point()
.zip_map(regions.base(), Rel::relative_to)
.to_point()
}; };
let vertices = self.vertices(); let vertices = elem.vertices();
let points: Vec<Point> = vertices.iter().map(|c| resolve(c.vertex())).collect(); let points: Vec<Point> = vertices.iter().map(|c| resolve(c.vertex())).collect();
let mut size = Size::zero(); let mut size = Size::zero();
@ -96,8 +101,10 @@ impl LayoutSingle for Packed<PathElem> {
let mut path = Path::new(); let mut path = Path::new();
path.move_to(points[0]); path.move_to(points[0]);
let mut add_cubic = let mut add_cubic = |from_point: Point,
|from_point: Point, to_point: Point, from: PathVertex, to: PathVertex| { to_point: Point,
from: PathVertex,
to: PathVertex| {
let from_control_point = resolve(from.control_point_from()) + from_point; let from_control_point = resolve(from.control_point_from()) + from_point;
let to_control_point = resolve(to.control_point_to()) + to_point; let to_control_point = resolve(to.control_point_to()) + to_point;
path.cubic_to(from_control_point, to_control_point, to_point); path.cubic_to(from_control_point, to_control_point, to_point);
@ -107,10 +114,8 @@ impl LayoutSingle for Packed<PathElem> {
from_control_point.x.to_raw(), from_control_point.x.to_raw(),
from_control_point.y.to_raw(), from_control_point.y.to_raw(),
); );
let p2 = kurbo::Point::new( let p2 =
to_control_point.x.to_raw(), kurbo::Point::new(to_control_point.x.to_raw(), to_control_point.y.to_raw());
to_control_point.y.to_raw(),
);
let p3 = kurbo::Point::new(to_point.x.to_raw(), to_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(); let extrema = CubicBez::new(p0, p1, p2, p3).bounding_box();
size.x.set_max(Abs::raw(extrema.x1)); size.x.set_max(Abs::raw(extrema.x1));
@ -126,7 +131,7 @@ impl LayoutSingle for Packed<PathElem> {
add_cubic(from_point, to_point, from, to); add_cubic(from_point, to_point, from, to);
} }
if self.closed(styles) { if elem.closed(styles) {
let from = *vertices.last().unwrap(); // We checked that we have at least one element. let from = *vertices.last().unwrap(); // We checked that we have at least one element.
let to = vertices[0]; let to = vertices[0];
let from_point = *points.last().unwrap(); let from_point = *points.last().unwrap();
@ -137,8 +142,8 @@ impl LayoutSingle for Packed<PathElem> {
} }
// Prepare fill and stroke. // Prepare fill and stroke.
let fill = self.fill(styles); let fill = elem.fill(styles);
let stroke = match self.stroke(styles) { let stroke = match elem.stroke(styles) {
Smart::Auto if fill.is_none() => Some(FixedStroke::default()), Smart::Auto if fill.is_none() => Some(FixedStroke::default()),
Smart::Auto => None, Smart::Auto => None,
Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default), Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default),
@ -146,9 +151,8 @@ impl LayoutSingle for Packed<PathElem> {
let mut frame = Frame::soft(size); let mut frame = Frame::soft(size);
let shape = Shape { geometry: Geometry::Path(path), stroke, fill }; let shape = Shape { geometry: Geometry::Path(path), stroke, fill };
frame.push(Point::zero(), FrameItem::Shape(shape, self.span())); frame.push(Point::zero(), FrameItem::Shape(shape, elem.span()));
Ok(frame) Ok(frame)
}
} }
/// A component used for path creation. /// A component used for path creation.

View File

@ -6,7 +6,7 @@ use ecow::{eco_format, EcoString};
use crate::diag::{bail, SourceResult}; use crate::diag::{bail, SourceResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{func, repr, scope, ty, Content, Smart, StyleChain}; 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::syntax::{Span, Spanned};
use crate::utils::{LazyHash, Numeric}; use crate::utils::{LazyHash, Numeric};
use crate::visualize::RelativeTo; use crate::visualize::RelativeTo;

View File

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

View File

@ -2,10 +2,10 @@ use std::f64::consts::SQRT_2;
use crate::diag::SourceResult; use crate::diag::SourceResult;
use crate::engine::Engine; 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::{ use crate::layout::{
Abs, Axes, Corner, Corners, Frame, FrameItem, LayoutMultiple, LayoutSingle, Length, Abs, Axes, BlockElem, Corner, Corners, Frame, FrameItem, Length, Point, Ratio,
Point, Ratio, Regions, Rel, Sides, Size, Region, Regions, Rel, Sides, Size,
}; };
use crate::syntax::Span; use crate::syntax::Span;
use crate::utils::Get; use crate::utils::Get;
@ -24,7 +24,7 @@ use crate::visualize::{FixedStroke, Paint, Path, Stroke};
/// to fit the content. /// to fit the content.
/// ] /// ]
/// ``` /// ```
#[elem(title = "Rectangle", LayoutSingle)] #[elem(title = "Rectangle", Show)]
pub struct RectElem { pub struct RectElem {
/// The rectangle's width, relative to its parent container. /// The rectangle's width, relative to its parent container.
pub width: Smart<Rel<Length>>, 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 /// When this is omitted, the rectangle takes on a default size of at most
/// `{45pt}` by `{30pt}`. /// `{45pt}` by `{30pt}`.
#[positional] #[positional]
#[borrowed]
pub body: Option<Content>, pub body: Option<Content>,
} }
impl LayoutSingle for Packed<RectElem> { impl Show for Packed<RectElem> {
#[typst_macros::time(name = "rect", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
fn layout( Ok(BlockElem::single_layouter(self.clone(), |elem, engine, styles, region| {
&self, layout_shape(
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Frame> {
layout(
engine, engine,
styles, styles,
regions, region,
ShapeKind::Rect, ShapeKind::Rect,
&self.body(styles), elem.body(styles),
Axes::new(self.width(styles), self.height(styles)), elem.fill(styles),
self.fill(styles), elem.stroke(styles),
self.stroke(styles), elem.inset(styles),
self.inset(styles), elem.outset(styles),
self.outset(styles), elem.radius(styles),
self.radius(styles), elem.span(),
self.span(),
) )
})
.with_width(self.width(styles))
.with_height(self.height(styles))
.pack())
} }
} }
@ -169,7 +168,7 @@ impl LayoutSingle for Packed<RectElem> {
/// sized to fit. /// sized to fit.
/// ] /// ]
/// ``` /// ```
#[elem(LayoutSingle)] #[elem(Show)]
pub struct SquareElem { pub struct SquareElem {
/// The square's side length. This is mutually exclusive with `width` and /// The square's side length. This is mutually exclusive with `width` and
/// `height`. /// `height`.
@ -234,31 +233,30 @@ pub struct SquareElem {
/// When this is omitted, the square takes on a default size of at most /// When this is omitted, the square takes on a default size of at most
/// `{30pt}`. /// `{30pt}`.
#[positional] #[positional]
#[borrowed]
pub body: Option<Content>, pub body: Option<Content>,
} }
impl LayoutSingle for Packed<SquareElem> { impl Show for Packed<SquareElem> {
#[typst_macros::time(name = "square", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
fn layout( Ok(BlockElem::single_layouter(self.clone(), |elem, engine, styles, regions| {
&self, layout_shape(
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Frame> {
layout(
engine, engine,
styles, styles,
regions, regions,
ShapeKind::Square, ShapeKind::Square,
&self.body(styles), elem.body(styles),
Axes::new(self.width(styles), self.height(styles)), elem.fill(styles),
self.fill(styles), elem.stroke(styles),
self.stroke(styles), elem.inset(styles),
self.inset(styles), elem.outset(styles),
self.outset(styles), elem.radius(styles),
self.radius(styles), elem.span(),
self.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. /// to fit the content.
/// ] /// ]
/// ``` /// ```
#[elem(LayoutSingle)] #[elem(Show)]
pub struct EllipseElem { pub struct EllipseElem {
/// The ellipse's width, relative to its parent container. /// The ellipse's width, relative to its parent container.
pub width: Smart<Rel<Length>>, 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 /// When this is omitted, the ellipse takes on a default size of at most
/// `{45pt}` by `{30pt}`. /// `{45pt}` by `{30pt}`.
#[positional] #[positional]
#[borrowed]
pub body: Option<Content>, pub body: Option<Content>,
} }
impl LayoutSingle for Packed<EllipseElem> { impl Show for Packed<EllipseElem> {
#[typst_macros::time(name = "ellipse", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
fn layout( Ok(BlockElem::single_layouter(self.clone(), |elem, engine, styles, regions| {
&self, layout_shape(
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Frame> {
layout(
engine, engine,
styles, styles,
regions, regions,
ShapeKind::Ellipse, ShapeKind::Ellipse,
&self.body(styles), elem.body(styles),
Axes::new(self.width(styles), self.height(styles)), elem.fill(styles),
self.fill(styles), elem.stroke(styles).map(|s| Sides::splat(Some(s))),
self.stroke(styles).map(|s| Sides::splat(Some(s))), elem.inset(styles),
self.inset(styles), elem.outset(styles),
self.outset(styles),
Corners::splat(None), Corners::splat(None),
self.span(), 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. /// sized to fit.
/// ] /// ]
/// ``` /// ```
#[elem(LayoutSingle)] #[elem(Show)]
pub struct CircleElem { pub struct CircleElem {
/// The circle's radius. This is mutually exclusive with `width` and /// The circle's radius. This is mutually exclusive with `width` and
/// `height`. /// `height`.
@ -415,43 +412,42 @@ pub struct CircleElem {
/// The content to place into the circle. The circle expands to fit this /// The content to place into the circle. The circle expands to fit this
/// content, keeping the 1-1 aspect ratio. /// content, keeping the 1-1 aspect ratio.
#[positional] #[positional]
#[borrowed]
pub body: Option<Content>, pub body: Option<Content>,
} }
impl LayoutSingle for Packed<CircleElem> { impl Show for Packed<CircleElem> {
#[typst_macros::time(name = "circle", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
fn layout( Ok(BlockElem::single_layouter(self.clone(), |elem, engine, styles, regions| {
&self, layout_shape(
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Frame> {
layout(
engine, engine,
styles, styles,
regions, regions,
ShapeKind::Circle, ShapeKind::Circle,
&self.body(styles), elem.body(styles),
Axes::new(self.width(styles), self.height(styles)), elem.fill(styles),
self.fill(styles), elem.stroke(styles).map(|s| Sides::splat(Some(s))),
self.stroke(styles).map(|s| Sides::splat(Some(s))), elem.inset(styles),
self.inset(styles), elem.outset(styles),
self.outset(styles),
Corners::splat(None), Corners::splat(None),
self.span(), elem.span(),
) )
})
.with_width(self.width(styles))
.with_height(self.height(styles))
.pack())
} }
} }
/// Layout a shape. /// Layout a shape.
#[typst_macros::time(span = span)]
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn layout( fn layout_shape(
engine: &mut Engine, engine: &mut Engine,
styles: StyleChain, styles: StyleChain,
regions: Regions, region: Region,
kind: ShapeKind, kind: ShapeKind,
body: &Option<Content>, body: &Option<Content>,
sizing: Axes<Smart<Rel<Length>>>,
fill: Option<Paint>, fill: Option<Paint>,
stroke: Smart<Sides<Option<Option<Stroke<Abs>>>>>, stroke: Smart<Sides<Option<Option<Stroke<Abs>>>>>,
inset: Sides<Option<Rel<Abs>>>, inset: Sides<Option<Rel<Abs>>>,
@ -459,47 +455,41 @@ fn layout(
radius: Corners<Option<Rel<Abs>>>, radius: Corners<Option<Rel<Abs>>>,
span: Span, span: Span,
) -> SourceResult<Frame> { ) -> 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 frame;
let mut inset = inset.unwrap_or_default();
if let Some(child) = body { if let Some(child) = body {
let region = resolved.unwrap_or(regions.base()); let mut inset = inset.unwrap_or_default();
if kind.is_round() { 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. // Layout the child.
let child = child.clone().padded(inset.map(|side| side.map(Length::from))); frame = child.layout(engine, styles, pod.into_regions())?.into_frame();
let expand = sizing.as_ref().map(Smart::is_custom);
let pod = Regions::one(region, expand);
frame = child.layout(engine, styles, pod)?.into_frame();
// Enforce correct size. // If the child is a square or circle, relayout with full expansion into
*frame.size_mut() = expand.select(region, frame.size()); // square region to make sure the result is really quadratic.
// Relayout with full expansion into square region to make sure
// the result is really a square or circle.
if kind.is_quadratic() { if kind.is_quadratic() {
frame.set_size(Size::splat(frame.size().max_by_side())); let length = frame.size().max_by_side().min(pod.size.min_by_side());
let length = frame.size().max_by_side().min(region.min_by_side()); let quad_pod = Regions::one(Size::splat(length), Axes::splat(true));
let pod = Regions::one(Size::splat(length), Axes::splat(true)); frame = child.layout(engine, styles, quad_pod)?.into_frame();
frame = child.layout(engine, styles, pod)?.into_frame();
} }
// Enforce correct size again. // Apply the inset.
*frame.size_mut() = expand.select(region, frame.size()); if has_inset {
if kind.is_quadratic() { crate::layout::grow(&mut frame, &inset);
frame.set_size(Size::splat(frame.size().max_by_side()));
} }
} else { } else {
// The default size that a shape takes on if it has no child and // The default size that a shape takes on if it has no child and
// enough space. // enough space.
let default = Size::new(Abs::pt(45.0), Abs::pt(30.0)); 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() { if kind.is_quadratic() {
size = Size::splat(size.min_by_side()); size = Size::splat(size.min_by_side());
} }
@ -526,9 +516,9 @@ fn layout(
} else { } else {
frame.fill_and_stroke( frame.fill_and_stroke(
fill, fill,
stroke, &stroke,
outset.unwrap_or_default(), &outset.unwrap_or_default(),
radius.unwrap_or_default(), &radius.unwrap_or_default(),
span, span,
); );
} }
@ -633,7 +623,7 @@ pub(crate) fn ellipse(
/// Creates a new rectangle as a path. /// Creates a new rectangle as a path.
pub(crate) fn clip_rect( pub(crate) fn clip_rect(
size: Size, size: Size,
radius: Corners<Rel<Abs>>, radius: &Corners<Rel<Abs>>,
stroke: &Sides<Option<FixedStroke>>, stroke: &Sides<Option<FixedStroke>>,
) -> Path { ) -> Path {
let stroke_widths = stroke let stroke_widths = stroke
@ -644,8 +634,7 @@ pub(crate) fn clip_rect(
+ stroke_widths.iter().cloned().min().unwrap_or(Abs::zero()); + 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 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(); let mut path = Path::new();
if corners.top_left.arc_inner() { if corners.top_left.arc_inner() {
@ -674,12 +663,12 @@ pub(crate) fn clip_rect(
/// - use fill for sides for best looks /// - use fill for sides for best looks
pub(crate) fn styled_rect( pub(crate) fn styled_rect(
size: Size, size: Size,
radius: Corners<Rel<Abs>>, radius: &Corners<Rel<Abs>>,
fill: Option<Paint>, fill: Option<Paint>,
stroke: Sides<Option<FixedStroke>>, stroke: &Sides<Option<FixedStroke>>,
) -> Vec<Shape> { ) -> Vec<Shape> {
if stroke.is_uniform() && radius.iter().cloned().all(Rel::is_zero) { 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 { } else {
segmented_rect(size, radius, fill, stroke) segmented_rect(size, radius, fill, stroke)
} }
@ -696,9 +685,9 @@ fn simple_rect(
fn corners_control_points( fn corners_control_points(
size: Size, size: Size,
radius: Corners<Abs>, radius: &Corners<Abs>,
strokes: &Sides<Option<FixedStroke>>, strokes: &Sides<Option<FixedStroke>>,
stroke_widths: Sides<Abs>, stroke_widths: &Sides<Abs>,
) -> Corners<ControlPoints> { ) -> Corners<ControlPoints> {
Corners { Corners {
top_left: Corner::TopLeft, top_left: Corner::TopLeft,
@ -726,9 +715,9 @@ fn corners_control_points(
/// Use stroke and fill for the rectangle /// Use stroke and fill for the rectangle
fn segmented_rect( fn segmented_rect(
size: Size, size: Size,
radius: Corners<Rel<Abs>>, radius: &Corners<Rel<Abs>>,
fill: Option<Paint>, fill: Option<Paint>,
strokes: Sides<Option<FixedStroke>>, strokes: &Sides<Option<FixedStroke>>,
) -> Vec<Shape> { ) -> Vec<Shape> {
let mut res = vec![]; let mut res = vec![];
let stroke_widths = strokes let stroke_widths = strokes
@ -739,8 +728,7 @@ fn segmented_rect(
+ stroke_widths.iter().cloned().min().unwrap_or(Abs::zero()); + 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 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 // insert stroked sides below filled sides
let mut stroke_insert = 0; let mut stroke_insert = 0;
@ -786,10 +774,7 @@ fn segmented_rect(
let start = last; let start = last;
let end = current; let end = current;
last = current; last = current;
let stroke = match strokes.get_ref(start.side_cw()) { let Some(stroke) = strokes.get_ref(start.side_cw()) else { continue };
None => continue,
Some(stroke) => stroke.clone(),
};
let (shape, ontop) = segment(start, end, &corners, stroke); let (shape, ontop) = segment(start, end, &corners, stroke);
if ontop { if ontop {
res.push(shape); res.push(shape);
@ -798,7 +783,7 @@ fn segmented_rect(
stroke_insert += 1; stroke_insert += 1;
} }
} }
} else if let Some(stroke) = strokes.top { } else if let Some(stroke) = &strokes.top {
// single segment // single segment
let (shape, _) = segment(Corner::TopLeft, Corner::TopLeft, &corners, stroke); let (shape, _) = segment(Corner::TopLeft, Corner::TopLeft, &corners, stroke);
res.push(shape); res.push(shape);
@ -848,7 +833,7 @@ fn segment(
start: Corner, start: Corner,
end: Corner, end: Corner,
corners: &Corners<ControlPoints>, corners: &Corners<ControlPoints>,
stroke: FixedStroke, stroke: &FixedStroke,
) -> (Shape, bool) { ) -> (Shape, bool) {
fn fill_corner(corner: &ControlPoints) -> bool { fn fill_corner(corner: &ControlPoints) -> bool {
corner.stroke_before != corner.stroke_after corner.stroke_before != corner.stroke_after
@ -883,12 +868,12 @@ fn segment(
.unwrap_or(true); .unwrap_or(true);
let use_fill = solid && fill_corners(start, end, corners); let use_fill = solid && fill_corners(start, end, corners);
let shape = if use_fill { let shape = if use_fill {
fill_segment(start, end, corners, stroke) fill_segment(start, end, corners, stroke)
} else { } else {
stroke_segment(start, end, corners, stroke) stroke_segment(start, end, corners, stroke.clone())
}; };
(shape, use_fill) (shape, use_fill)
} }
@ -899,7 +884,7 @@ fn stroke_segment(
corners: &Corners<ControlPoints>, corners: &Corners<ControlPoints>,
stroke: FixedStroke, stroke: FixedStroke,
) -> Shape { ) -> Shape {
// create start corner // Create start corner.
let mut path = Path::new(); let mut path = Path::new();
path_segment(start, end, corners, &mut path); path_segment(start, end, corners, &mut path);
@ -915,7 +900,7 @@ fn fill_segment(
start: Corner, start: Corner,
end: Corner, end: Corner,
corners: &Corners<ControlPoints>, corners: &Corners<ControlPoints>,
stroke: FixedStroke, stroke: &FixedStroke,
) -> Shape { ) -> Shape {
let mut path = Path::new(); let mut path = Path::new();
@ -1004,7 +989,7 @@ fn fill_segment(
Shape { Shape {
geometry: Geometry::Path(path), geometry: Geometry::Path(path),
stroke: None, 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::Deserialize;
use serde_yaml as yaml; use serde_yaml as yaml;
use typst::diag::{bail, StrResult}; use typst::diag::{bail, StrResult};
use typst::foundations::AutoValue;
use typst::foundations::Bytes;
use typst::foundations::NoneValue;
use typst::foundations::{ use typst::foundations::{
CastInfo, Category, Func, Module, ParamInfo, Repr, Scope, Smart, Type, Value, AutoValue, Bytes, CastInfo, Category, Func, Module, NoneValue, ParamInfo, Repr,
FOUNDATIONS, Scope, Smart, Type, Value, FOUNDATIONS,
}; };
use typst::introspection::INTROSPECTION; use typst::introspection::INTROSPECTION;
use typst::layout::{Abs, Margin, PageElem, LAYOUT}; use typst::layout::{Abs, Margin, PageElem, LAYOUT};
use typst::loading::DATA_LOADING; use typst::loading::DATA_LOADING;
use typst::math::MATH; use typst::math::MATH;
use typst::model::Document; use typst::model::{Document, MODEL};
use typst::model::MODEL;
use typst::symbols::SYMBOLS; use typst::symbols::SYMBOLS;
use typst::text::{Font, FontBook, TEXT}; use typst::text::{Font, FontBook, TEXT};
use typst::utils::LazyHash; 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(), 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 --- --- box-clip-rect ---
// Test box clipping with a rectangle // Test box clipping with a rectangle
Hello #box(width: 1em, height: 1em, clip: false)[#rect(width: 3em, height: 3em, fill: red)] 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. // In-flow item with size zero in the first region.
#set page(height: 5cm, margin: 1cm) #set page(height: 5cm, margin: 1cm)
In-flow, zero-sized item. 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) #set block(spacing: 0pt)
#line(length: 0pt) #line(length: 0pt)
#rect(height: 2cm, fill: gray) #rect(height: 2cm, fill: gray)

View File

@ -45,11 +45,10 @@
fill: gradient.linear(red, purple, space: color.hsl) fill: gradient.linear(red, purple, space: color.hsl)
) )
--- gradient-linear-relative-parent --- --- gradient-linear-relative-parent ---
// The image should look as if there is a single gradient that is being used for // The image should look as if there is a single gradient that is being used for
// both the page and the rectangles. // 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) #let my-rect = rect(width: 50%, height: 50%, fill: grad)
#set page( #set page(
height: 50pt, height: 50pt,
@ -64,7 +63,7 @@
--- gradient-linear-relative-self --- --- gradient-linear-relative-self ---
// The image should look as if there are multiple gradients, one for each // The image should look as if there are multiple gradients, one for each
// rectangle. // 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) #let my-rect = rect(width: 50%, height: 50%, fill: grad)
#set page( #set page(
height: 50pt, height: 50pt,
@ -76,6 +75,29 @@
#place(top + right, my-rect) #place(top + right, my-rect)
#place(bottom + center, rotate(45deg, 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 --- --- gradient-linear-repeat-and-mirror-1 ---
// Test repeated gradients. // Test repeated gradients.
#rect( #rect(

View File

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