Show block-level elements as blocks (#4310)
@ -2,10 +2,8 @@ use std::num::{NonZeroI64, NonZeroIsize, NonZeroU64, NonZeroUsize, ParseIntError
|
||||
|
||||
use ecow::{eco_format, EcoString};
|
||||
|
||||
use crate::{
|
||||
diag::StrResult,
|
||||
foundations::{cast, func, repr, scope, ty, Repr, Str, Value},
|
||||
};
|
||||
use crate::diag::StrResult;
|
||||
use crate::foundations::{cast, func, repr, scope, ty, Repr, Str, Value};
|
||||
|
||||
/// A whole number.
|
||||
///
|
||||
|
@ -25,9 +25,9 @@ pub use self::state::*;
|
||||
|
||||
use crate::diag::{bail, SourceResult};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::NativeElement;
|
||||
use crate::foundations::{
|
||||
category, elem, Args, Category, Construct, Content, Packed, Scope, Unlabellable,
|
||||
category, elem, Args, Category, Construct, Content, NativeElement, Packed, Scope,
|
||||
Unlabellable,
|
||||
};
|
||||
use crate::realize::{Behave, Behaviour};
|
||||
|
||||
|
@ -7,6 +7,9 @@ use ecow::EcoString;
|
||||
use crate::foundations::{cast, repr, Fold, Repr, Value};
|
||||
use crate::utils::{Numeric, Scalar};
|
||||
|
||||
/// The epsilon for approximate comparisons.
|
||||
const EPS: f64 = 1e-6;
|
||||
|
||||
/// An absolute length.
|
||||
#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
pub struct Abs(Scalar);
|
||||
@ -54,7 +57,7 @@ impl Abs {
|
||||
|
||||
/// Get the value of this absolute length in raw units.
|
||||
pub const fn to_raw(self) -> f64 {
|
||||
(self.0).get()
|
||||
self.0.get()
|
||||
}
|
||||
|
||||
/// Get the value of this absolute length in a unit.
|
||||
@ -110,12 +113,17 @@ impl Abs {
|
||||
/// Whether the other absolute length fits into this one (i.e. is smaller).
|
||||
/// Allows for a bit of slack.
|
||||
pub fn fits(self, other: Self) -> bool {
|
||||
self.0 + 1e-6 >= other.0
|
||||
self.0 + EPS >= other.0
|
||||
}
|
||||
|
||||
/// Compares two absolute lengths for whether they are approximately equal.
|
||||
pub fn approx_eq(self, other: Self) -> bool {
|
||||
self == other || (self - other).to_raw().abs() < 1e-6
|
||||
self == other || (self - other).to_raw().abs() < EPS
|
||||
}
|
||||
|
||||
/// Whether the size is close to zero or negative.
|
||||
pub fn approx_empty(self) -> bool {
|
||||
self.to_raw() <= EPS
|
||||
}
|
||||
|
||||
/// Returns a number that represent the sign of this length
|
||||
|
@ -49,10 +49,7 @@ pub struct AlignElem {
|
||||
impl Show for Packed<AlignElem> {
|
||||
#[typst_macros::time(name = "align", span = self.span())]
|
||||
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
||||
Ok(self
|
||||
.body()
|
||||
.clone()
|
||||
.styled(AlignElem::set_alignment(self.alignment(styles))))
|
||||
Ok(self.body().clone().aligned(self.alignment(styles)))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,7 @@ use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Deref, Not};
|
||||
|
||||
use crate::diag::bail;
|
||||
use crate::foundations::{array, cast, Array, Resolve, Smart, StyleChain};
|
||||
use crate::layout::{Abs, Dir, Length, Ratio, Rel};
|
||||
use crate::layout::{Abs, Dir, Length, Ratio, Rel, Size};
|
||||
use crate::utils::Get;
|
||||
|
||||
/// A container with a horizontal and vertical component.
|
||||
@ -120,6 +120,16 @@ impl<T: Ord> Axes<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl Axes<Rel<Abs>> {
|
||||
/// Evaluate the axes relative to the given `size`.
|
||||
pub fn relative_to(&self, size: Size) -> Size {
|
||||
Size {
|
||||
x: self.x.relative_to(size.x),
|
||||
y: self.y.relative_to(size.y),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Get<Axis> for Axes<T> {
|
||||
type Component = T;
|
||||
|
||||
|
@ -2,10 +2,9 @@ use std::num::NonZeroUsize;
|
||||
|
||||
use crate::diag::SourceResult;
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{elem, Content, Packed, StyleChain};
|
||||
use crate::foundations::{elem, Content, NativeElement, Packed, Show, StyleChain};
|
||||
use crate::layout::{
|
||||
Abs, Axes, Dir, Fragment, Frame, LayoutMultiple, Length, Point, Ratio, Regions, Rel,
|
||||
Size,
|
||||
Abs, Axes, BlockElem, Dir, Fragment, Frame, Length, Point, Ratio, Regions, Rel, Size,
|
||||
};
|
||||
use crate::realize::{Behave, Behaviour};
|
||||
use crate::text::TextElem;
|
||||
@ -42,7 +41,7 @@ use crate::utils::Numeric;
|
||||
/// increasingly been used to solve a
|
||||
/// variety of problems.
|
||||
/// ```
|
||||
#[elem(LayoutMultiple)]
|
||||
#[elem(Show)]
|
||||
pub struct ColumnsElem {
|
||||
/// The number of columns.
|
||||
#[positional]
|
||||
@ -59,82 +58,86 @@ pub struct ColumnsElem {
|
||||
pub body: Content,
|
||||
}
|
||||
|
||||
impl LayoutMultiple for Packed<ColumnsElem> {
|
||||
#[typst_macros::time(name = "columns", span = self.span())]
|
||||
fn layout(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment> {
|
||||
let body = self.body();
|
||||
impl Show for Packed<ColumnsElem> {
|
||||
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
|
||||
Ok(BlockElem::multi_layouter(self.clone(), layout_columns)
|
||||
.with_rootable(true)
|
||||
.pack())
|
||||
}
|
||||
}
|
||||
|
||||
// Separating the infinite space into infinite columns does not make
|
||||
// much sense.
|
||||
if !regions.size.x.is_finite() {
|
||||
return body.layout(engine, styles, regions);
|
||||
}
|
||||
/// Layout the columns.
|
||||
#[typst_macros::time(span = elem.span())]
|
||||
fn layout_columns(
|
||||
elem: &Packed<ColumnsElem>,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment> {
|
||||
let body = elem.body();
|
||||
|
||||
// Determine the width of the gutter and each column.
|
||||
let columns = self.count(styles).get();
|
||||
let gutter = self.gutter(styles).relative_to(regions.base().x);
|
||||
let width = (regions.size.x - gutter * (columns - 1) as f64) / columns as f64;
|
||||
// Separating the infinite space into infinite columns does not make
|
||||
// much sense.
|
||||
if !regions.size.x.is_finite() {
|
||||
return body.layout(engine, styles, regions);
|
||||
}
|
||||
|
||||
let backlog: Vec<_> = std::iter::once(®ions.size.y)
|
||||
.chain(regions.backlog)
|
||||
.flat_map(|&height| std::iter::repeat(height).take(columns))
|
||||
.skip(1)
|
||||
.collect();
|
||||
// Determine the width of the gutter and each column.
|
||||
let columns = elem.count(styles).get();
|
||||
let gutter = elem.gutter(styles).relative_to(regions.base().x);
|
||||
let width = (regions.size.x - gutter * (columns - 1) as f64) / columns as f64;
|
||||
|
||||
// Create the pod regions.
|
||||
let pod = Regions {
|
||||
size: Size::new(width, regions.size.y),
|
||||
full: regions.full,
|
||||
backlog: &backlog,
|
||||
last: regions.last,
|
||||
expand: Axes::new(true, regions.expand.y),
|
||||
root: regions.root,
|
||||
};
|
||||
let backlog: Vec<_> = std::iter::once(®ions.size.y)
|
||||
.chain(regions.backlog)
|
||||
.flat_map(|&height| std::iter::repeat(height).take(columns))
|
||||
.skip(1)
|
||||
.collect();
|
||||
|
||||
// Layout the children.
|
||||
let mut frames = body.layout(engine, styles, pod)?.into_iter();
|
||||
let mut finished = vec![];
|
||||
// Create the pod regions.
|
||||
let pod = Regions {
|
||||
size: Size::new(width, regions.size.y),
|
||||
full: regions.full,
|
||||
backlog: &backlog,
|
||||
last: regions.last,
|
||||
expand: Axes::new(true, regions.expand.y),
|
||||
root: regions.root,
|
||||
};
|
||||
|
||||
let dir = TextElem::dir_in(styles);
|
||||
let total_regions = (frames.len() as f32 / columns as f32).ceil() as usize;
|
||||
// Layout the children.
|
||||
let mut frames = body.layout(engine, styles, pod)?.into_iter();
|
||||
let mut finished = vec![];
|
||||
|
||||
// Stitch together the columns for each region.
|
||||
for region in regions.iter().take(total_regions) {
|
||||
// The height should be the parent height if we should expand.
|
||||
// Otherwise its the maximum column height for the frame. In that
|
||||
// case, the frame is first created with zero height and then
|
||||
// resized.
|
||||
let height = if regions.expand.y { region.y } else { Abs::zero() };
|
||||
let mut output = Frame::hard(Size::new(regions.size.x, height));
|
||||
let mut cursor = Abs::zero();
|
||||
let dir = TextElem::dir_in(styles);
|
||||
let total_regions = (frames.len() as f32 / columns as f32).ceil() as usize;
|
||||
|
||||
for _ in 0..columns {
|
||||
let Some(frame) = frames.next() else { break };
|
||||
if !regions.expand.y {
|
||||
output.size_mut().y.set_max(frame.height());
|
||||
}
|
||||
// Stitch together the columns for each region.
|
||||
for region in regions.iter().take(total_regions) {
|
||||
// The height should be the parent height if we should expand.
|
||||
// Otherwise its the maximum column height for the frame. In that
|
||||
// case, the frame is first created with zero height and then
|
||||
// resized.
|
||||
let height = if regions.expand.y { region.y } else { Abs::zero() };
|
||||
let mut output = Frame::hard(Size::new(regions.size.x, height));
|
||||
let mut cursor = Abs::zero();
|
||||
|
||||
let width = frame.width();
|
||||
let x = if dir == Dir::LTR {
|
||||
cursor
|
||||
} else {
|
||||
regions.size.x - cursor - width
|
||||
};
|
||||
|
||||
output.push_frame(Point::with_x(x), frame);
|
||||
cursor += width + gutter;
|
||||
for _ in 0..columns {
|
||||
let Some(frame) = frames.next() else { break };
|
||||
if !regions.expand.y {
|
||||
output.size_mut().y.set_max(frame.height());
|
||||
}
|
||||
|
||||
finished.push(output);
|
||||
let width = frame.width();
|
||||
let x =
|
||||
if dir == Dir::LTR { cursor } else { regions.size.x - cursor - width };
|
||||
|
||||
output.push_frame(Point::with_x(x), frame);
|
||||
cursor += width + gutter;
|
||||
}
|
||||
|
||||
Ok(Fragment::frames(finished))
|
||||
finished.push(output);
|
||||
}
|
||||
|
||||
Ok(Fragment::frames(finished))
|
||||
}
|
||||
|
||||
/// Forces a column break.
|
||||
|
@ -1,11 +1,15 @@
|
||||
use crate::diag::SourceResult;
|
||||
use once_cell::unsync::Lazy;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::diag::{bail, SourceResult};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{
|
||||
cast, elem, AutoValue, Content, Packed, Resolve, Smart, StyleChain, Value,
|
||||
cast, elem, Args, AutoValue, Construct, Content, NativeElement, Packed, Resolve,
|
||||
Smart, StyleChain, Value,
|
||||
};
|
||||
use crate::layout::{
|
||||
Abs, Axes, Corners, Em, Fr, Fragment, Frame, FrameKind, LayoutMultiple, Length,
|
||||
Ratio, Regions, Rel, Sides, Size, Spacing, VElem,
|
||||
Abs, Axes, Corners, Em, Fr, Fragment, Frame, FrameKind, Length, Region, Regions, Rel,
|
||||
Sides, Size, Spacing, VElem,
|
||||
};
|
||||
use crate::utils::Numeric;
|
||||
use crate::visualize::{clip_rect, Paint, Stroke};
|
||||
@ -106,47 +110,53 @@ pub struct BoxElem {
|
||||
|
||||
/// The contents of the box.
|
||||
#[positional]
|
||||
#[borrowed]
|
||||
pub body: Option<Content>,
|
||||
}
|
||||
|
||||
impl Packed<BoxElem> {
|
||||
/// Layout this box as part of a paragraph.
|
||||
#[typst_macros::time(name = "box", span = self.span())]
|
||||
pub fn layout(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
region: Size,
|
||||
) -> SourceResult<Frame> {
|
||||
let width = match self.width(styles) {
|
||||
Sizing::Auto => Smart::Auto,
|
||||
Sizing::Rel(rel) => Smart::Custom(rel),
|
||||
Sizing::Fr(_) => Smart::Custom(Ratio::one().into()),
|
||||
// Fetch sizing properties.
|
||||
let width = self.width(styles);
|
||||
let height = self.height(styles);
|
||||
let inset = self.inset(styles).unwrap_or_default();
|
||||
|
||||
// Build the pod region.
|
||||
let pod = Self::pod(&width, &height, &inset, styles, region);
|
||||
|
||||
// Layout the body.
|
||||
let mut frame = match self.body(styles) {
|
||||
// If we have no body, just create an empty frame. If necessary,
|
||||
// its size will be adjusted below.
|
||||
None => Frame::hard(Size::zero()),
|
||||
|
||||
// If we have a child, layout it into the body. Boxes are boundaries
|
||||
// for gradient relativeness, so we set the `FrameKind` to `Hard`.
|
||||
Some(body) => body
|
||||
.layout(engine, styles, pod.into_regions())?
|
||||
.into_frame()
|
||||
.with_kind(FrameKind::Hard),
|
||||
};
|
||||
|
||||
// Resolve the sizing to a concrete size.
|
||||
let sizing = Axes::new(width, self.height(styles));
|
||||
let expand = sizing.as_ref().map(Smart::is_custom);
|
||||
let size = sizing
|
||||
.resolve(styles)
|
||||
.zip_map(regions.base(), |s, b| s.map(|v| v.relative_to(b)))
|
||||
.unwrap_or(regions.base());
|
||||
// Enforce a correct frame size on the expanded axes. Do this before
|
||||
// applying the inset, since the pod shrunk.
|
||||
frame.set_size(pod.expand.select(pod.size, frame.size()));
|
||||
|
||||
// Apply inset.
|
||||
let mut body = self.body(styles).unwrap_or_default();
|
||||
let inset = self.inset(styles).unwrap_or_default();
|
||||
if inset.iter().any(|v| !v.is_zero()) {
|
||||
body = body.padded(inset.map(|side| side.map(Length::from)));
|
||||
// Apply the inset.
|
||||
if !inset.is_zero() {
|
||||
crate::layout::grow(&mut frame, &inset);
|
||||
}
|
||||
|
||||
// Select the appropriate base and expansion for the child depending
|
||||
// on whether it is automatically or relatively sized.
|
||||
let pod = Regions::one(size, expand);
|
||||
let mut frame = body.layout(engine, styles, pod)?.into_frame();
|
||||
|
||||
// Enforce correct size.
|
||||
*frame.size_mut() = expand.select(size, frame.size());
|
||||
|
||||
// Apply baseline shift.
|
||||
// Apply baseline shift. Do this after setting the size and applying the
|
||||
// inset, so that a relative shift is resolved relative to the final
|
||||
// height.
|
||||
let shift = self.baseline(styles).relative_to(frame.height());
|
||||
if !shift.is_zero() {
|
||||
frame.set_baseline(frame.baseline() - shift);
|
||||
@ -159,27 +169,115 @@ impl Packed<BoxElem> {
|
||||
.unwrap_or_default()
|
||||
.map(|s| s.map(Stroke::unwrap_or_default));
|
||||
|
||||
// Clip the contents
|
||||
// Only fetch these if necessary (for clipping or filling/stroking).
|
||||
let outset = Lazy::new(|| self.outset(styles).unwrap_or_default());
|
||||
let radius = Lazy::new(|| self.radius(styles).unwrap_or_default());
|
||||
|
||||
// Clip the contents, if requested.
|
||||
if self.clip(styles) {
|
||||
let outset =
|
||||
self.outset(styles).unwrap_or_default().relative_to(frame.size());
|
||||
let size = frame.size() + outset.sum_by_axis();
|
||||
let radius = self.radius(styles).unwrap_or_default();
|
||||
frame.clip(clip_rect(size, radius, &stroke));
|
||||
let size = frame.size() + outset.relative_to(frame.size()).sum_by_axis();
|
||||
frame.clip(clip_rect(size, &radius, &stroke));
|
||||
}
|
||||
|
||||
// Add fill and/or stroke.
|
||||
if fill.is_some() || stroke.iter().any(Option::is_some) {
|
||||
let outset = self.outset(styles).unwrap_or_default();
|
||||
let radius = self.radius(styles).unwrap_or_default();
|
||||
frame.fill_and_stroke(fill, stroke, outset, radius, self.span());
|
||||
frame.fill_and_stroke(fill, &stroke, &outset, &radius, self.span());
|
||||
}
|
||||
|
||||
// Apply metadata.
|
||||
frame.set_kind(FrameKind::Hard);
|
||||
|
||||
Ok(frame)
|
||||
}
|
||||
|
||||
/// Builds the pod region for box layout.
|
||||
fn pod(
|
||||
width: &Sizing,
|
||||
height: &Smart<Rel>,
|
||||
inset: &Sides<Rel<Abs>>,
|
||||
styles: StyleChain,
|
||||
region: Size,
|
||||
) -> Region {
|
||||
// Resolve the size.
|
||||
let mut size = Size::new(
|
||||
match width {
|
||||
// For auto, the whole region is available.
|
||||
Sizing::Auto => region.x,
|
||||
// Resolve the relative sizing.
|
||||
Sizing::Rel(rel) => rel.resolve(styles).relative_to(region.x),
|
||||
// Fr is handled outside and already factored into the `region`,
|
||||
// so we can treat it equivalently to 100%.
|
||||
Sizing::Fr(_) => region.x,
|
||||
},
|
||||
match height {
|
||||
// See above. Note that fr is not supported on this axis.
|
||||
Smart::Auto => region.y,
|
||||
Smart::Custom(rel) => rel.resolve(styles).relative_to(region.y),
|
||||
},
|
||||
);
|
||||
|
||||
// Take the inset, if any, into account.
|
||||
if !inset.is_zero() {
|
||||
size = crate::layout::shrink(size, inset);
|
||||
}
|
||||
|
||||
// If the child is not auto-sized, the size is forced and we should
|
||||
// enable expansion.
|
||||
let expand = Axes::new(*width != Sizing::Auto, *height != Smart::Auto);
|
||||
|
||||
Region::new(size, expand)
|
||||
}
|
||||
}
|
||||
|
||||
/// An inline-level container that can produce arbitrary items that can break
|
||||
/// across lines.
|
||||
#[elem(Construct)]
|
||||
pub struct InlineElem {
|
||||
/// A callback that is invoked with the regions to produce arbitrary
|
||||
/// inline items.
|
||||
#[required]
|
||||
#[internal]
|
||||
body: callbacks::InlineCallback,
|
||||
}
|
||||
|
||||
impl Construct for InlineElem {
|
||||
fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> {
|
||||
bail!(args.span, "cannot be constructed manually");
|
||||
}
|
||||
}
|
||||
|
||||
impl InlineElem {
|
||||
/// Create an inline-level item with a custom layouter.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn layouter<T: NativeElement>(
|
||||
captured: Packed<T>,
|
||||
callback: fn(
|
||||
content: &Packed<T>,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
region: Size,
|
||||
) -> SourceResult<Vec<InlineItem>>,
|
||||
) -> Self {
|
||||
Self::new(callbacks::InlineCallback::new(captured, callback))
|
||||
}
|
||||
}
|
||||
|
||||
impl Packed<InlineElem> {
|
||||
/// Layout the element.
|
||||
pub fn layout(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
region: Size,
|
||||
) -> SourceResult<Vec<InlineItem>> {
|
||||
self.body().call(engine, styles, region)
|
||||
}
|
||||
}
|
||||
|
||||
/// Layouted items suitable for placing in a paragraph.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum InlineItem {
|
||||
/// Absolute spacing between other items, and whether it is weak.
|
||||
Space(Abs, bool),
|
||||
/// Layouted inline-level content.
|
||||
Frame(Frame),
|
||||
}
|
||||
|
||||
/// A block-level container.
|
||||
@ -211,7 +309,7 @@ impl Packed<BoxElem> {
|
||||
/// = Blocky
|
||||
/// More text.
|
||||
/// ```
|
||||
#[elem(LayoutMultiple)]
|
||||
#[elem]
|
||||
pub struct BlockElem {
|
||||
/// The block's width.
|
||||
///
|
||||
@ -332,93 +430,155 @@ pub struct BlockElem {
|
||||
#[default(false)]
|
||||
pub clip: bool,
|
||||
|
||||
/// The contents of the block.
|
||||
#[positional]
|
||||
pub body: Option<Content>,
|
||||
|
||||
/// Whether this block must stick to the following one.
|
||||
///
|
||||
/// Use this to prevent page breaks between e.g. a heading and its body.
|
||||
#[internal]
|
||||
#[default(false)]
|
||||
#[ghost]
|
||||
#[parse(None)]
|
||||
pub sticky: bool,
|
||||
|
||||
/// Whether this block can host footnotes.
|
||||
#[internal]
|
||||
#[default(false)]
|
||||
#[parse(None)]
|
||||
pub rootable: bool,
|
||||
|
||||
/// The contents of the block.
|
||||
#[positional]
|
||||
#[borrowed]
|
||||
pub body: Option<BlockChild>,
|
||||
}
|
||||
|
||||
impl LayoutMultiple for Packed<BlockElem> {
|
||||
impl BlockElem {
|
||||
/// Create a block with a custom single-region layouter.
|
||||
///
|
||||
/// Such a block must have `breakable: false` (which is set by this
|
||||
/// constructor).
|
||||
pub fn single_layouter<T: NativeElement>(
|
||||
captured: Packed<T>,
|
||||
f: fn(
|
||||
content: &Packed<T>,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
region: Region,
|
||||
) -> SourceResult<Frame>,
|
||||
) -> Self {
|
||||
Self::new()
|
||||
.with_breakable(false)
|
||||
.with_body(Some(BlockChild::SingleLayouter(
|
||||
callbacks::BlockSingleCallback::new(captured, f),
|
||||
)))
|
||||
}
|
||||
|
||||
/// Create a block with a custom multi-region layouter.
|
||||
pub fn multi_layouter<T: NativeElement>(
|
||||
captured: Packed<T>,
|
||||
f: fn(
|
||||
content: &Packed<T>,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment>,
|
||||
) -> Self {
|
||||
Self::new().with_body(Some(BlockChild::MultiLayouter(
|
||||
callbacks::BlockMultiCallback::new(captured, f),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
impl Packed<BlockElem> {
|
||||
/// Layout this block as part of a flow.
|
||||
#[typst_macros::time(name = "block", span = self.span())]
|
||||
fn layout(
|
||||
pub fn layout(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment> {
|
||||
// Apply inset.
|
||||
let mut body = self.body(styles).unwrap_or_default();
|
||||
// Fetch sizing properties.
|
||||
let width = self.width(styles);
|
||||
let height = self.height(styles);
|
||||
let inset = self.inset(styles).unwrap_or_default();
|
||||
if inset.iter().any(|v| !v.is_zero()) {
|
||||
body = body.clone().padded(inset.map(|side| side.map(Length::from)));
|
||||
}
|
||||
let breakable = self.breakable(styles);
|
||||
|
||||
// Resolve the sizing to a concrete size.
|
||||
let sizing = Axes::new(self.width(styles), self.height(styles));
|
||||
let mut expand = sizing.as_ref().map(Smart::is_custom);
|
||||
let mut size = sizing
|
||||
.resolve(styles)
|
||||
.zip_map(regions.base(), |s, b| s.map(|v| v.relative_to(b)))
|
||||
.unwrap_or(regions.base());
|
||||
// Allocate a small vector for backlogs.
|
||||
let mut buf = SmallVec::<[Abs; 2]>::new();
|
||||
|
||||
// Layout the child.
|
||||
let mut frames = if self.breakable(styles) {
|
||||
// Measure to ensure frames for all regions have the same width.
|
||||
if sizing.x == Smart::Auto {
|
||||
let pod = Regions::one(size, Axes::splat(false));
|
||||
let frame = body.measure(engine, styles, pod)?.into_frame();
|
||||
size.x = frame.width();
|
||||
expand.x = true;
|
||||
}
|
||||
// Build the pod regions.
|
||||
let pod =
|
||||
Self::pod(&width, &height, &inset, breakable, styles, regions, &mut buf);
|
||||
|
||||
let mut pod = regions;
|
||||
pod.size.x = size.x;
|
||||
pod.expand = expand;
|
||||
|
||||
if expand.y {
|
||||
pod.full = size.y;
|
||||
}
|
||||
|
||||
// Generate backlog for fixed height.
|
||||
let mut heights = vec![];
|
||||
if sizing.y.is_custom() {
|
||||
let mut remaining = size.y;
|
||||
for region in regions.iter() {
|
||||
let limited = region.y.min(remaining);
|
||||
heights.push(limited);
|
||||
remaining -= limited;
|
||||
if Abs::zero().fits(remaining) {
|
||||
break;
|
||||
// Layout the body.
|
||||
let body = self.body(styles);
|
||||
let mut fragment = match body {
|
||||
// If we have no body, just create one frame plus one per backlog
|
||||
// region. We create them zero-sized; if necessary, their size will
|
||||
// be adjusted below.
|
||||
None => {
|
||||
let mut frames = vec![];
|
||||
frames.push(Frame::hard(Size::zero()));
|
||||
if pod.expand.y {
|
||||
let mut iter = pod;
|
||||
while !iter.backlog.is_empty() {
|
||||
frames.push(Frame::hard(Size::zero()));
|
||||
iter.next();
|
||||
}
|
||||
}
|
||||
Fragment::frames(frames)
|
||||
}
|
||||
|
||||
if let Some(last) = heights.last_mut() {
|
||||
*last += remaining;
|
||||
// If we have content as our body, just layout it.
|
||||
Some(BlockChild::Content(body)) => {
|
||||
let mut fragment = body.measure(engine, styles, pod)?;
|
||||
|
||||
// If the body is automatically sized and produced more than one
|
||||
// fragment, ensure that the width was consistent across all
|
||||
// regions. If it wasn't, we need to relayout with expansion.
|
||||
if !pod.expand.x
|
||||
&& fragment
|
||||
.as_slice()
|
||||
.windows(2)
|
||||
.any(|w| !w[0].width().approx_eq(w[1].width()))
|
||||
{
|
||||
let max_width = fragment
|
||||
.iter()
|
||||
.map(|frame| frame.width())
|
||||
.max()
|
||||
.unwrap_or_default();
|
||||
let pod = Regions {
|
||||
size: Size::new(max_width, pod.size.y),
|
||||
expand: Axes::new(true, pod.expand.y),
|
||||
..pod
|
||||
};
|
||||
fragment = body.layout(engine, styles, pod)?;
|
||||
} else {
|
||||
// Apply the side effect to turn the `measure` into a
|
||||
// `layout`.
|
||||
engine.locator.visit_frames(&fragment);
|
||||
}
|
||||
|
||||
pod.size.y = heights[0];
|
||||
pod.backlog = &heights[1..];
|
||||
pod.last = None;
|
||||
fragment
|
||||
}
|
||||
|
||||
let mut frames = body.layout(engine, styles, pod)?.into_frames();
|
||||
for (frame, &height) in frames.iter_mut().zip(&heights) {
|
||||
*frame.size_mut() =
|
||||
expand.select(Size::new(size.x, height), frame.size());
|
||||
// If we have a child that wants to layout with just access to the
|
||||
// base region, give it that.
|
||||
Some(BlockChild::SingleLayouter(callback)) => {
|
||||
let pod = Region::new(pod.base(), pod.expand);
|
||||
callback.call(engine, styles, pod).map(Fragment::frame)?
|
||||
}
|
||||
|
||||
// If we have a child that wants to layout with full region access,
|
||||
// we layout it.
|
||||
//
|
||||
// For auto-sized multi-layouters, we propagate the outer expansion
|
||||
// so that they can decide for themselves. We also ensure again to
|
||||
// only expand if the size is finite.
|
||||
Some(BlockChild::MultiLayouter(callback)) => {
|
||||
let expand = (pod.expand | regions.expand) & pod.size.map(Abs::is_finite);
|
||||
let pod = Regions { expand, ..pod };
|
||||
callback.call(engine, styles, pod)?
|
||||
}
|
||||
frames
|
||||
} else {
|
||||
let pod = Regions::one(size, expand);
|
||||
let mut frames = body.layout(engine, styles, pod)?.into_frames();
|
||||
*frames[0].size_mut() = expand.select(size, frames[0].size());
|
||||
frames
|
||||
};
|
||||
|
||||
// Prepare fill and stroke.
|
||||
@ -428,60 +588,219 @@ impl LayoutMultiple for Packed<BlockElem> {
|
||||
.unwrap_or_default()
|
||||
.map(|s| s.map(Stroke::unwrap_or_default));
|
||||
|
||||
// Clip the contents
|
||||
if self.clip(styles) {
|
||||
for frame in frames.iter_mut() {
|
||||
let outset =
|
||||
self.outset(styles).unwrap_or_default().relative_to(frame.size());
|
||||
let size = frame.size() + outset.sum_by_axis();
|
||||
let radius = self.radius(styles).unwrap_or_default();
|
||||
frame.clip(clip_rect(size, radius, &stroke));
|
||||
}
|
||||
// Only fetch these if necessary (for clipping or filling/stroking).
|
||||
let outset = Lazy::new(|| self.outset(styles).unwrap_or_default());
|
||||
let radius = Lazy::new(|| self.radius(styles).unwrap_or_default());
|
||||
|
||||
// Fetch/compute these outside of the loop.
|
||||
let clip = self.clip(styles);
|
||||
let has_fill_or_stroke = fill.is_some() || stroke.iter().any(Option::is_some);
|
||||
let has_inset = !inset.is_zero();
|
||||
let is_explicit = matches!(body, None | Some(BlockChild::Content(_)));
|
||||
|
||||
// Skip filling/stroking the first frame if it is empty and a non-empty
|
||||
// one follows.
|
||||
let mut skip_first = false;
|
||||
if let [first, rest @ ..] = fragment.as_slice() {
|
||||
skip_first = has_fill_or_stroke
|
||||
&& first.is_empty()
|
||||
&& rest.iter().any(|frame| !frame.is_empty());
|
||||
}
|
||||
|
||||
// Add fill and/or stroke.
|
||||
if fill.is_some() || stroke.iter().any(Option::is_some) {
|
||||
let mut skip = false;
|
||||
if let [first, rest @ ..] = frames.as_slice() {
|
||||
skip = first.is_empty() && rest.iter().any(|frame| !frame.is_empty());
|
||||
// Post-process to apply insets, clipping, fills, and strokes.
|
||||
for (i, (frame, region)) in fragment.iter_mut().zip(pod.iter()).enumerate() {
|
||||
// Explicit blocks are boundaries for gradient relativeness.
|
||||
if is_explicit {
|
||||
frame.set_kind(FrameKind::Hard);
|
||||
}
|
||||
|
||||
let outset = self.outset(styles).unwrap_or_default();
|
||||
let radius = self.radius(styles).unwrap_or_default();
|
||||
for frame in frames.iter_mut().skip(skip as usize) {
|
||||
// Enforce a correct frame size on the expanded axes. Do this before
|
||||
// applying the inset, since the pod shrunk.
|
||||
frame.set_size(pod.expand.select(region, frame.size()));
|
||||
|
||||
// Apply the inset.
|
||||
if has_inset {
|
||||
crate::layout::grow(frame, &inset);
|
||||
}
|
||||
|
||||
// Clip the contents, if requested.
|
||||
if clip {
|
||||
let size = frame.size() + outset.relative_to(frame.size()).sum_by_axis();
|
||||
frame.clip(clip_rect(size, &radius, &stroke));
|
||||
}
|
||||
|
||||
// Add fill and/or stroke.
|
||||
if has_fill_or_stroke && (i > 0 || !skip_first) {
|
||||
frame.fill_and_stroke(
|
||||
fill.clone(),
|
||||
stroke.clone(),
|
||||
outset,
|
||||
radius,
|
||||
&stroke,
|
||||
&outset,
|
||||
&radius,
|
||||
self.span(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply metadata.
|
||||
for frame in &mut frames {
|
||||
frame.set_kind(FrameKind::Hard);
|
||||
Ok(fragment)
|
||||
}
|
||||
|
||||
/// Builds the pod regions for block layout.
|
||||
///
|
||||
/// If `breakable` is `false`, this will only ever return a single region.
|
||||
fn pod<'a>(
|
||||
width: &Smart<Rel>,
|
||||
height: &Smart<Rel>,
|
||||
inset: &Sides<Rel<Abs>>,
|
||||
breakable: bool,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
buf: &'a mut SmallVec<[Abs; 2]>,
|
||||
) -> Regions<'a> {
|
||||
let base = regions.base();
|
||||
|
||||
// The vertical region sizes we're about to build.
|
||||
let first;
|
||||
let full;
|
||||
let backlog: &mut [Abs];
|
||||
let last;
|
||||
|
||||
// If the block has a fixed height, things are very different, so we
|
||||
// handle that case completely separately.
|
||||
match height {
|
||||
Smart::Auto => {
|
||||
if breakable {
|
||||
// If the block automatically sized and breakable, we can
|
||||
// just inherit the regions.
|
||||
first = regions.size.y;
|
||||
buf.extend_from_slice(regions.backlog);
|
||||
backlog = buf;
|
||||
last = regions.last;
|
||||
} else {
|
||||
// If the block is automatically sized, but not breakable,
|
||||
// we provide the full base height. It doesn't really make
|
||||
// sense to provide just the remaining height to an
|
||||
// unbreakable block.
|
||||
first = regions.full;
|
||||
backlog = &mut [];
|
||||
last = None;
|
||||
}
|
||||
|
||||
// Since we're automatically sized, we inherit the base size.
|
||||
full = regions.full;
|
||||
}
|
||||
|
||||
Smart::Custom(rel) => {
|
||||
// Resolve the sizing to a concrete size.
|
||||
let resolved = rel.resolve(styles).relative_to(base.y);
|
||||
|
||||
if breakable {
|
||||
// If the block is fixed-height and breakable, distribute
|
||||
// the fixed height across a start region and a backlog.
|
||||
(first, backlog) = distribute(resolved, regions, buf);
|
||||
} else {
|
||||
// If the block is fixed-height, but not breakable, the
|
||||
// fixed height is all in the first region, and we have no
|
||||
// backlog.
|
||||
first = resolved;
|
||||
backlog = &mut [];
|
||||
}
|
||||
|
||||
// Since we're manually sized, the resolved size is also the
|
||||
// base height.
|
||||
full = resolved;
|
||||
|
||||
// If the height is manually sized, we don't want a final
|
||||
// repeatable region.
|
||||
last = None;
|
||||
}
|
||||
};
|
||||
|
||||
// Resolve the horizontal sizing to a concrete width and combine
|
||||
// `width` and `first` into `size`.
|
||||
let mut size = Size::new(
|
||||
match width {
|
||||
Smart::Auto => regions.size.x,
|
||||
Smart::Custom(rel) => rel.resolve(styles).relative_to(base.x),
|
||||
},
|
||||
first,
|
||||
);
|
||||
|
||||
// Take the inset, if any, into account, applying it to the
|
||||
// individual region components.
|
||||
let (mut full, mut last) = (full, last);
|
||||
if !inset.is_zero() {
|
||||
crate::layout::shrink_multiple(
|
||||
&mut size, &mut full, backlog, &mut last, inset,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Fragment::frames(frames))
|
||||
// If the child is manually sized along an axis (i.e. not `auto`), then
|
||||
// it should expand along that axis. We also ensure that we only expand
|
||||
// if the size is finite because it just doesn't make sense to expand
|
||||
// into infinite regions.
|
||||
let expand = Axes::new(*width != Smart::Auto, *height != Smart::Auto)
|
||||
& size.map(Abs::is_finite);
|
||||
|
||||
Regions {
|
||||
size,
|
||||
full,
|
||||
backlog,
|
||||
last,
|
||||
expand,
|
||||
// This will only ever be set by the flow if the block is
|
||||
// `rootable`. It is important that we propagate this, so that
|
||||
// columns can hold footnotes.
|
||||
root: regions.root,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines how to size a grid cell along an axis.
|
||||
/// The contents of a block.
|
||||
#[derive(Debug, Clone, PartialEq, Hash)]
|
||||
pub enum BlockChild {
|
||||
/// The block contains normal content.
|
||||
Content(Content),
|
||||
/// The block contains a layout callback that needs access to just one
|
||||
/// base region.
|
||||
SingleLayouter(callbacks::BlockSingleCallback),
|
||||
/// The block contains a layout callback that needs access to the exact
|
||||
/// regions.
|
||||
MultiLayouter(callbacks::BlockMultiCallback),
|
||||
}
|
||||
|
||||
impl Default for BlockChild {
|
||||
fn default() -> Self {
|
||||
Self::Content(Content::default())
|
||||
}
|
||||
}
|
||||
|
||||
cast! {
|
||||
BlockChild,
|
||||
self => match self {
|
||||
Self::Content(content) => content.into_value(),
|
||||
_ => Value::Auto,
|
||||
},
|
||||
v: Content => Self::Content(v),
|
||||
}
|
||||
|
||||
/// Defines how to size something along an axis.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum Sizing {
|
||||
/// A track that fits its cell's contents.
|
||||
/// A track that fits its item's contents.
|
||||
Auto,
|
||||
/// A track size specified in absolute terms and relative to the parent's
|
||||
/// size.
|
||||
Rel(Rel<Length>),
|
||||
/// A track size specified as a fraction of the remaining free space in the
|
||||
/// A size specified in absolute terms and relative to the parent's size.
|
||||
Rel(Rel),
|
||||
/// A size specified as a fraction of the remaining free space in the
|
||||
/// parent.
|
||||
Fr(Fr),
|
||||
}
|
||||
|
||||
impl Sizing {
|
||||
/// Whether this is an automatic sizing.
|
||||
pub fn is_auto(self) -> bool {
|
||||
matches!(self, Self::Auto)
|
||||
}
|
||||
|
||||
/// Whether this is fractional sizing.
|
||||
pub fn is_fractional(self) -> bool {
|
||||
matches!(self, Self::Fr(_))
|
||||
@ -494,6 +813,15 @@ impl Default for Sizing {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Smart<Rel>> for Sizing {
|
||||
fn from(smart: Smart<Rel>) -> Self {
|
||||
match smart {
|
||||
Smart::Auto => Self::Auto,
|
||||
Smart::Custom(rel) => Self::Rel(rel),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Into<Spacing>> From<T> for Sizing {
|
||||
fn from(spacing: T) -> Self {
|
||||
match spacing.into() {
|
||||
@ -514,3 +842,109 @@ cast! {
|
||||
v: Rel<Length> => Self::Rel(v),
|
||||
v: Fr => Self::Fr(v),
|
||||
}
|
||||
|
||||
/// Distribute a fixed height spread over existing regions into a new first
|
||||
/// height and a new backlog.
|
||||
fn distribute<'a>(
|
||||
height: Abs,
|
||||
regions: Regions,
|
||||
buf: &'a mut SmallVec<[Abs; 2]>,
|
||||
) -> (Abs, &'a mut [Abs]) {
|
||||
// Build new region heights from old regions.
|
||||
let mut remaining = height;
|
||||
for region in regions.iter() {
|
||||
let limited = region.y.min(remaining);
|
||||
buf.push(limited);
|
||||
remaining -= limited;
|
||||
if remaining.approx_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If there is still something remaining, apply it to the
|
||||
// last region (it will overflow, but there's nothing else
|
||||
// we can do).
|
||||
if !remaining.approx_empty() {
|
||||
if let Some(last) = buf.last_mut() {
|
||||
*last += remaining;
|
||||
}
|
||||
}
|
||||
|
||||
// Distribute the heights to the first region and the
|
||||
// backlog. There is no last region, since the height is
|
||||
// fixed.
|
||||
(buf[0], &mut buf[1..])
|
||||
}
|
||||
|
||||
/// Manual closure implementations for layout callbacks.
|
||||
///
|
||||
/// Normal closures are not `Hash`, so we can't use them.
|
||||
mod callbacks {
|
||||
use super::*;
|
||||
|
||||
macro_rules! callback {
|
||||
($name:ident = ($($param:ident: $param_ty:ty),* $(,)?) -> $ret:ty) => {
|
||||
#[derive(Debug, Clone, PartialEq, Hash)]
|
||||
pub struct $name {
|
||||
captured: Content,
|
||||
f: fn(&Content, $($param_ty),*) -> $ret,
|
||||
}
|
||||
|
||||
impl $name {
|
||||
pub fn new<T: NativeElement>(
|
||||
captured: Packed<T>,
|
||||
f: fn(&Packed<T>, $($param_ty),*) -> $ret,
|
||||
) -> Self {
|
||||
Self {
|
||||
// Type-erased the content.
|
||||
captured: captured.pack(),
|
||||
// Safety: The only difference between the two function
|
||||
// pointer types is the type of the first parameter,
|
||||
// which changes from `&Packed<T>` to `&Content`. This
|
||||
// is safe because:
|
||||
// - `Packed<T>` is a transparent wrapper around
|
||||
// `Content`, so for any `T` it has the same memory
|
||||
// representation as `Content`.
|
||||
// - While `Packed<T>` imposes the additional constraint
|
||||
// that the content is of type `T`, this constraint is
|
||||
// upheld: It is initially the case because we store a
|
||||
// `Packed<T>` above. It keeps being the case over the
|
||||
// lifetime of the closure because `capture` is a
|
||||
// private field and `Content`'s `Clone` impl is
|
||||
// guaranteed to retain the type (if it didn't,
|
||||
// literally everything would break).
|
||||
f: unsafe { std::mem::transmute(f) },
|
||||
}
|
||||
}
|
||||
|
||||
pub fn call(&self, $($param: $param_ty),*) -> $ret {
|
||||
(self.f)(&self.captured, $($param),*)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
callback! {
|
||||
InlineCallback = (
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
region: Size,
|
||||
) -> SourceResult<Vec<InlineItem>>
|
||||
}
|
||||
|
||||
callback! {
|
||||
BlockSingleCallback = (
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
region: Region,
|
||||
) -> SourceResult<Frame>
|
||||
}
|
||||
|
||||
callback! {
|
||||
BlockMultiCallback = (
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment>
|
||||
}
|
||||
}
|
||||
|
@ -13,9 +13,8 @@ use crate::foundations::{
|
||||
};
|
||||
use crate::introspection::TagElem;
|
||||
use crate::layout::{
|
||||
Abs, AlignElem, Axes, BlockElem, ColbreakElem, ColumnsElem, FixedAlignment,
|
||||
FlushElem, Fr, Fragment, Frame, FrameItem, LayoutMultiple, LayoutSingle, PlaceElem,
|
||||
Point, Regions, Rel, Size, Spacing, VElem,
|
||||
Abs, AlignElem, Axes, BlockElem, ColbreakElem, FixedAlignment, FlushElem, Fr,
|
||||
Fragment, Frame, FrameItem, PlaceElem, Point, Regions, Rel, Size, Spacing, VElem,
|
||||
};
|
||||
use crate::model::{FootnoteElem, FootnoteEntry, ParElem};
|
||||
use crate::utils::Numeric;
|
||||
@ -24,16 +23,16 @@ use crate::utils::Numeric;
|
||||
///
|
||||
/// This element is responsible for layouting both the top-level content flow
|
||||
/// and the contents of boxes.
|
||||
#[elem(Debug, LayoutMultiple)]
|
||||
#[elem(Debug)]
|
||||
pub struct FlowElem {
|
||||
/// The children that will be arranged into a flow.
|
||||
#[variadic]
|
||||
pub children: Vec<Content>,
|
||||
}
|
||||
|
||||
impl LayoutMultiple for Packed<FlowElem> {
|
||||
impl Packed<FlowElem> {
|
||||
#[typst_macros::time(name = "flow", span = self.span())]
|
||||
fn layout(
|
||||
pub fn layout(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
@ -59,12 +58,13 @@ impl LayoutMultiple for Packed<FlowElem> {
|
||||
alone = child
|
||||
.to_packed::<StyledElem>()
|
||||
.map_or(child, |styled| &styled.child)
|
||||
.can::<dyn LayoutMultiple>();
|
||||
.is::<BlockElem>();
|
||||
}
|
||||
|
||||
let outer = styles;
|
||||
|
||||
let mut layouter = FlowLayouter::new(regions, styles, alone);
|
||||
for mut child in self.children().iter() {
|
||||
let outer = styles;
|
||||
let mut styles = styles;
|
||||
if let Some(styled) = child.to_packed::<StyledElem>() {
|
||||
child = &styled.child;
|
||||
@ -77,6 +77,10 @@ impl LayoutMultiple for Packed<FlowElem> {
|
||||
layouter.flush(engine)?;
|
||||
} else if let Some(elem) = child.to_packed::<VElem>() {
|
||||
layouter.layout_spacing(engine, elem, styles)?;
|
||||
} else if let Some(elem) = child.to_packed::<ParElem>() {
|
||||
layouter.layout_par(engine, elem, styles)?;
|
||||
} else if let Some(elem) = child.to_packed::<BlockElem>() {
|
||||
layouter.layout_block(engine, elem, styles)?;
|
||||
} else if let Some(placed) = child.to_packed::<PlaceElem>() {
|
||||
layouter.layout_placed(engine, placed, styles)?;
|
||||
} else if child.is::<ColbreakElem>() {
|
||||
@ -84,12 +88,6 @@ impl LayoutMultiple for Packed<FlowElem> {
|
||||
{
|
||||
layouter.finish_region(engine, true)?;
|
||||
}
|
||||
} else if let Some(elem) = child.to_packed::<ParElem>() {
|
||||
layouter.layout_par(engine, elem, styles)?;
|
||||
} else if let Some(layoutable) = child.with::<dyn LayoutSingle>() {
|
||||
layouter.layout_single(engine, layoutable, styles)?;
|
||||
} else if let Some(layoutable) = child.with::<dyn LayoutMultiple>() {
|
||||
layouter.layout_multiple(engine, child, layoutable, styles)?;
|
||||
} else {
|
||||
bail!(child.span(), "unexpected flow child");
|
||||
}
|
||||
@ -199,6 +197,7 @@ impl<'a> FlowLayouter<'a> {
|
||||
/// Create a new flow layouter.
|
||||
fn new(mut regions: Regions<'a>, styles: StyleChain<'a>, alone: bool) -> Self {
|
||||
let expand = regions.expand;
|
||||
let root = std::mem::replace(&mut regions.root, false);
|
||||
|
||||
// Disable vertical expansion when there are multiple or not directly
|
||||
// layoutable children.
|
||||
@ -206,9 +205,6 @@ impl<'a> FlowLayouter<'a> {
|
||||
regions.expand.y = false;
|
||||
}
|
||||
|
||||
// Disable root.
|
||||
let root = std::mem::replace(&mut regions.root, false);
|
||||
|
||||
Self {
|
||||
root,
|
||||
regions,
|
||||
@ -253,27 +249,6 @@ impl<'a> FlowLayouter<'a> {
|
||||
)
|
||||
}
|
||||
|
||||
/// Layout a placed element.
|
||||
fn layout_placed(
|
||||
&mut self,
|
||||
engine: &mut Engine,
|
||||
placed: &Packed<PlaceElem>,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
let float = placed.float(styles);
|
||||
let clearance = placed.clearance(styles);
|
||||
let alignment = placed.alignment(styles);
|
||||
let delta = Axes::new(placed.dx(styles), placed.dy(styles)).resolve(styles);
|
||||
let x_align = alignment.map_or(FixedAlignment::Center, |align| {
|
||||
align.x().unwrap_or_default().resolve(styles)
|
||||
});
|
||||
let y_align = alignment.map(|align| align.y().map(|y| y.resolve(styles)));
|
||||
let mut frame = placed.layout(engine, styles, self.regions.base())?.into_frame();
|
||||
frame.post_process(styles);
|
||||
let item = FlowItem::Placed { frame, x_align, y_align, delta, float, clearance };
|
||||
self.layout_item(engine, item)
|
||||
}
|
||||
|
||||
/// Layout a paragraph.
|
||||
fn layout_par(
|
||||
&mut self,
|
||||
@ -337,63 +312,33 @@ impl<'a> FlowLayouter<'a> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Layout into a single region.
|
||||
fn layout_single(
|
||||
&mut self,
|
||||
engine: &mut Engine,
|
||||
layoutable: &dyn LayoutSingle,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
let align = AlignElem::alignment_in(styles).resolve(styles);
|
||||
let sticky = BlockElem::sticky_in(styles);
|
||||
let pod = Regions::one(self.regions.base(), Axes::splat(false));
|
||||
let mut frame = layoutable.layout(engine, styles, pod)?;
|
||||
self.drain_tag(&mut frame);
|
||||
frame.post_process(styles);
|
||||
self.layout_item(
|
||||
engine,
|
||||
FlowItem::Frame { frame, align, sticky, movable: true },
|
||||
)?;
|
||||
self.last_was_par = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Layout into multiple regions.
|
||||
fn layout_multiple(
|
||||
fn layout_block(
|
||||
&mut self,
|
||||
engine: &mut Engine,
|
||||
child: &Content,
|
||||
layoutable: &dyn LayoutMultiple,
|
||||
styles: StyleChain,
|
||||
block: &'a Packed<BlockElem>,
|
||||
styles: StyleChain<'a>,
|
||||
) -> SourceResult<()> {
|
||||
// Temporarily delegerate rootness to the columns.
|
||||
// Temporarily delegate rootness to the columns.
|
||||
let is_root = self.root;
|
||||
if is_root && child.is::<ColumnsElem>() {
|
||||
if is_root && block.rootable(styles) {
|
||||
self.root = false;
|
||||
self.regions.root = true;
|
||||
}
|
||||
|
||||
let mut notes = Vec::new();
|
||||
|
||||
if self.regions.is_full() {
|
||||
// Skip directly if region is already full.
|
||||
self.finish_region(engine, false)?;
|
||||
}
|
||||
|
||||
// How to align the block.
|
||||
let align = if let Some(align) = child.to_packed::<AlignElem>() {
|
||||
align.alignment(styles)
|
||||
} else if let Some(styled) = child.to_packed::<StyledElem>() {
|
||||
AlignElem::alignment_in(styles.chain(&styled.styles))
|
||||
} else {
|
||||
AlignElem::alignment_in(styles)
|
||||
}
|
||||
.resolve(styles);
|
||||
|
||||
// Layout the block itself.
|
||||
let sticky = BlockElem::sticky_in(styles);
|
||||
let fragment = layoutable.layout(engine, styles, self.regions)?;
|
||||
let sticky = block.sticky(styles);
|
||||
let fragment = block.layout(engine, styles, self.regions)?;
|
||||
|
||||
// How to align the block.
|
||||
let align = AlignElem::alignment_in(styles).resolve(styles);
|
||||
|
||||
let mut notes = Vec::new();
|
||||
for (i, mut frame) in fragment.into_iter().enumerate() {
|
||||
// Find footnotes in the frame.
|
||||
if self.root {
|
||||
@ -421,6 +366,27 @@ impl<'a> FlowLayouter<'a> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Layout a placed element.
|
||||
fn layout_placed(
|
||||
&mut self,
|
||||
engine: &mut Engine,
|
||||
placed: &Packed<PlaceElem>,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
let float = placed.float(styles);
|
||||
let clearance = placed.clearance(styles);
|
||||
let alignment = placed.alignment(styles);
|
||||
let delta = Axes::new(placed.dx(styles), placed.dy(styles)).resolve(styles);
|
||||
let x_align = alignment.map_or(FixedAlignment::Center, |align| {
|
||||
align.x().unwrap_or_default().resolve(styles)
|
||||
});
|
||||
let y_align = alignment.map(|align| align.y().map(|y| y.resolve(styles)));
|
||||
let mut frame = placed.layout(engine, styles, self.regions.base())?.into_frame();
|
||||
frame.post_process(styles);
|
||||
let item = FlowItem::Placed { frame, x_align, y_align, delta, float, clearance };
|
||||
self.layout_item(engine, item)
|
||||
}
|
||||
|
||||
/// Attach currently pending metadata to the frame.
|
||||
fn drain_tag(&mut self, frame: &mut Frame) {
|
||||
if !self.pending_tags.is_empty() && !frame.is_empty() {
|
||||
@ -444,13 +410,13 @@ impl<'a> FlowLayouter<'a> {
|
||||
&& !self
|
||||
.items
|
||||
.iter()
|
||||
.any(|item| matches!(item, FlowItem::Frame { .. }))
|
||||
.any(|item| matches!(item, FlowItem::Frame { .. },))
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
self.regions.size.y -= v
|
||||
}
|
||||
FlowItem::Fractional(_) => {}
|
||||
FlowItem::Fractional(..) => {}
|
||||
FlowItem::Frame { ref frame, movable, .. } => {
|
||||
let height = frame.height();
|
||||
while !self.regions.size.y.fits(height) && !self.regions.in_last() {
|
||||
@ -615,7 +581,8 @@ impl<'a> FlowLayouter<'a> {
|
||||
}
|
||||
FlowItem::Fractional(v) => {
|
||||
let remaining = self.initial.y - used.y;
|
||||
offset += v.share(fr, remaining);
|
||||
let length = v.share(fr, remaining);
|
||||
offset += length;
|
||||
}
|
||||
FlowItem::Frame { frame, align, .. } => {
|
||||
ruler = ruler.max(align.y);
|
||||
|
@ -41,6 +41,11 @@ impl Fragment {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Extract a slice with the contained frames.
|
||||
pub fn as_slice(&self) -> &[Frame] {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Iterate over the contained frames.
|
||||
pub fn iter(&self) -> std::slice::Iter<Frame> {
|
||||
self.0.iter()
|
||||
|
@ -30,6 +30,8 @@ pub struct Frame {
|
||||
/// The items composing this layout.
|
||||
items: Arc<LazyHash<Vec<(Point, FrameItem)>>>,
|
||||
/// The hardness of this frame.
|
||||
///
|
||||
/// Determines whether it is a boundary for gradient drawing.
|
||||
kind: FrameKind,
|
||||
}
|
||||
|
||||
@ -70,6 +72,12 @@ impl Frame {
|
||||
self.kind = kind;
|
||||
}
|
||||
|
||||
/// Sets the frame's hardness builder-style.
|
||||
pub fn with_kind(mut self, kind: FrameKind) -> Self {
|
||||
self.kind = kind;
|
||||
self
|
||||
}
|
||||
|
||||
/// Whether the frame is hard or soft.
|
||||
pub fn kind(&self) -> FrameKind {
|
||||
self.kind
|
||||
@ -217,6 +225,11 @@ impl Frame {
|
||||
|
||||
/// Inline a frame at the given layer.
|
||||
fn inline(&mut self, layer: usize, pos: Point, frame: Frame) {
|
||||
// Skip work if there's nothing to do.
|
||||
if frame.items.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to just reuse the items.
|
||||
if pos.is_zero() && self.items.is_empty() {
|
||||
self.items = frame.items;
|
||||
@ -354,9 +367,9 @@ impl Frame {
|
||||
pub fn fill_and_stroke(
|
||||
&mut self,
|
||||
fill: Option<Paint>,
|
||||
stroke: Sides<Option<FixedStroke>>,
|
||||
outset: Sides<Rel<Abs>>,
|
||||
radius: Corners<Rel<Abs>>,
|
||||
stroke: &Sides<Option<FixedStroke>>,
|
||||
outset: &Sides<Rel<Abs>>,
|
||||
radius: &Corners<Rel<Abs>>,
|
||||
span: Span,
|
||||
) {
|
||||
let outset = outset.relative_to(self.size());
|
||||
@ -479,7 +492,7 @@ pub enum FrameKind {
|
||||
Soft,
|
||||
/// A container which uses its own size.
|
||||
///
|
||||
/// This is used for page, block, box, column, grid, and stack elements.
|
||||
/// This is used for pages, blocks, and boxes.
|
||||
Hard,
|
||||
}
|
||||
|
||||
|
@ -14,10 +14,7 @@ use crate::foundations::{
|
||||
Array, CastInfo, Content, Context, Fold, FromValue, Func, IntoValue, Reflect,
|
||||
Resolve, Smart, StyleChain, Value,
|
||||
};
|
||||
use crate::layout::{
|
||||
Abs, Alignment, Axes, Fragment, LayoutMultiple, Length, LinePosition, Regions, Rel,
|
||||
Sides, Sizing,
|
||||
};
|
||||
use crate::layout::{Abs, Alignment, Axes, Length, LinePosition, Rel, Sides, Sizing};
|
||||
use crate::syntax::Span;
|
||||
use crate::utils::NonZeroExt;
|
||||
use crate::visualize::{Paint, Stroke};
|
||||
@ -204,17 +201,6 @@ impl From<Content> for Cell {
|
||||
}
|
||||
}
|
||||
|
||||
impl LayoutMultiple for Cell {
|
||||
fn layout(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment> {
|
||||
self.body.layout(engine, styles, regions)
|
||||
}
|
||||
}
|
||||
|
||||
/// A grid entry.
|
||||
#[derive(Clone)]
|
||||
pub(super) enum Entry {
|
||||
|
@ -10,8 +10,8 @@ use crate::diag::{bail, SourceResult};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{Resolve, StyleChain};
|
||||
use crate::layout::{
|
||||
Abs, Axes, Cell, CellGrid, Dir, Fr, Fragment, Frame, FrameItem, LayoutMultiple,
|
||||
Length, Point, Regions, Rel, Size, Sizing,
|
||||
Abs, Axes, Cell, CellGrid, Dir, Fr, Fragment, Frame, FrameItem, Length, Point,
|
||||
Regions, Rel, Size, Sizing,
|
||||
};
|
||||
use crate::syntax::Span;
|
||||
use crate::text::TextElem;
|
||||
@ -841,7 +841,7 @@ impl<'a> GridLayouter<'a> {
|
||||
|
||||
let size = Size::new(available, height);
|
||||
let pod = Regions::one(size, Axes::splat(false));
|
||||
let frame = cell.measure(engine, self.styles, pod)?.into_frame();
|
||||
let frame = cell.body.measure(engine, self.styles, pod)?.into_frame();
|
||||
resolved.set_max(frame.width() - already_covered_width);
|
||||
}
|
||||
|
||||
@ -1069,7 +1069,7 @@ impl<'a> GridLayouter<'a> {
|
||||
pod
|
||||
};
|
||||
|
||||
let frames = cell.measure(engine, self.styles, pod)?.into_frames();
|
||||
let frames = cell.body.measure(engine, self.styles, pod)?.into_frames();
|
||||
|
||||
// Skip the first region if one cell in it is empty. Then,
|
||||
// remeasure.
|
||||
@ -1232,7 +1232,7 @@ impl<'a> GridLayouter<'a> {
|
||||
// rows.
|
||||
pod.full = self.regions.full;
|
||||
}
|
||||
let frame = cell.layout(engine, self.styles, pod)?.into_frame();
|
||||
let frame = cell.body.layout(engine, self.styles, pod)?.into_frame();
|
||||
let mut pos = pos;
|
||||
if self.is_rtl {
|
||||
// In the grid, cell colspans expand to the right,
|
||||
@ -1286,7 +1286,7 @@ impl<'a> GridLayouter<'a> {
|
||||
pod.size.x = width;
|
||||
|
||||
// Push the layouted frames into the individual output frames.
|
||||
let fragment = cell.layout(engine, self.styles, pod)?;
|
||||
let fragment = cell.body.layout(engine, self.styles, pod)?;
|
||||
for (output, frame) in outputs.iter_mut().zip(fragment) {
|
||||
let mut pos = pos;
|
||||
if self.is_rtl {
|
||||
|
@ -19,11 +19,12 @@ use smallvec::{smallvec, SmallVec};
|
||||
use crate::diag::{bail, SourceResult, StrResult, Trace, Tracepoint};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{
|
||||
cast, elem, scope, Array, Content, Fold, Packed, Show, Smart, StyleChain, Value,
|
||||
cast, elem, scope, Array, Content, Fold, NativeElement, Packed, Show, Smart,
|
||||
StyleChain, Value,
|
||||
};
|
||||
use crate::layout::{
|
||||
Abs, AlignElem, Alignment, Axes, Dir, Fragment, LayoutMultiple, Length,
|
||||
OuterHAlignment, OuterVAlignment, Regions, Rel, Sides, Sizing,
|
||||
Abs, Alignment, Axes, BlockElem, Dir, Fragment, Length, OuterHAlignment,
|
||||
OuterVAlignment, Regions, Rel, Sides, Sizing,
|
||||
};
|
||||
use crate::model::{TableCell, TableFooter, TableHLine, TableHeader, TableVLine};
|
||||
use crate::syntax::Span;
|
||||
@ -148,7 +149,7 @@ use crate::visualize::{Paint, Stroke};
|
||||
///
|
||||
/// Furthermore, strokes of a repeated grid header or footer will take
|
||||
/// precedence over regular cell strokes.
|
||||
#[elem(scope, LayoutMultiple)]
|
||||
#[elem(scope, Show)]
|
||||
pub struct GridElem {
|
||||
/// The column sizes.
|
||||
///
|
||||
@ -335,64 +336,67 @@ impl GridElem {
|
||||
type GridFooter;
|
||||
}
|
||||
|
||||
impl LayoutMultiple for Packed<GridElem> {
|
||||
#[typst_macros::time(name = "grid", span = self.span())]
|
||||
fn layout(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment> {
|
||||
let inset = self.inset(styles);
|
||||
let align = self.align(styles);
|
||||
let columns = self.columns(styles);
|
||||
let rows = self.rows(styles);
|
||||
let column_gutter = self.column_gutter(styles);
|
||||
let row_gutter = self.row_gutter(styles);
|
||||
let fill = self.fill(styles);
|
||||
let stroke = self.stroke(styles);
|
||||
|
||||
let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice());
|
||||
let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice());
|
||||
// Use trace to link back to the grid when a specific cell errors
|
||||
let tracepoint = || Tracepoint::Call(Some(eco_format!("grid")));
|
||||
let resolve_item = |item: &GridItem| item.to_resolvable(styles);
|
||||
let children = self.children().iter().map(|child| match child {
|
||||
GridChild::Header(header) => ResolvableGridChild::Header {
|
||||
repeat: header.repeat(styles),
|
||||
span: header.span(),
|
||||
items: header.children().iter().map(resolve_item),
|
||||
},
|
||||
GridChild::Footer(footer) => ResolvableGridChild::Footer {
|
||||
repeat: footer.repeat(styles),
|
||||
span: footer.span(),
|
||||
items: footer.children().iter().map(resolve_item),
|
||||
},
|
||||
GridChild::Item(item) => {
|
||||
ResolvableGridChild::Item(item.to_resolvable(styles))
|
||||
}
|
||||
});
|
||||
let grid = CellGrid::resolve(
|
||||
tracks,
|
||||
gutter,
|
||||
children,
|
||||
fill,
|
||||
align,
|
||||
&inset,
|
||||
&stroke,
|
||||
engine,
|
||||
styles,
|
||||
self.span(),
|
||||
)
|
||||
.trace(engine.world, tracepoint, self.span())?;
|
||||
|
||||
let layouter = GridLayouter::new(&grid, regions, styles, self.span());
|
||||
|
||||
// Measure the columns and layout the grid row-by-row.
|
||||
layouter.layout(engine)
|
||||
impl Show for Packed<GridElem> {
|
||||
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
|
||||
Ok(BlockElem::multi_layouter(self.clone(), layout_grid).pack())
|
||||
}
|
||||
}
|
||||
|
||||
/// Layout the grid.
|
||||
#[typst_macros::time(span = elem.span())]
|
||||
fn layout_grid(
|
||||
elem: &Packed<GridElem>,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment> {
|
||||
let inset = elem.inset(styles);
|
||||
let align = elem.align(styles);
|
||||
let columns = elem.columns(styles);
|
||||
let rows = elem.rows(styles);
|
||||
let column_gutter = elem.column_gutter(styles);
|
||||
let row_gutter = elem.row_gutter(styles);
|
||||
let fill = elem.fill(styles);
|
||||
let stroke = elem.stroke(styles);
|
||||
|
||||
let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice());
|
||||
let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice());
|
||||
// Use trace to link back to the grid when a specific cell errors
|
||||
let tracepoint = || Tracepoint::Call(Some(eco_format!("grid")));
|
||||
let resolve_item = |item: &GridItem| item.to_resolvable(styles);
|
||||
let children = elem.children().iter().map(|child| match child {
|
||||
GridChild::Header(header) => ResolvableGridChild::Header {
|
||||
repeat: header.repeat(styles),
|
||||
span: header.span(),
|
||||
items: header.children().iter().map(resolve_item),
|
||||
},
|
||||
GridChild::Footer(footer) => ResolvableGridChild::Footer {
|
||||
repeat: footer.repeat(styles),
|
||||
span: footer.span(),
|
||||
items: footer.children().iter().map(resolve_item),
|
||||
},
|
||||
GridChild::Item(item) => ResolvableGridChild::Item(item.to_resolvable(styles)),
|
||||
});
|
||||
let grid = CellGrid::resolve(
|
||||
tracks,
|
||||
gutter,
|
||||
children,
|
||||
fill,
|
||||
align,
|
||||
&inset,
|
||||
&stroke,
|
||||
engine,
|
||||
styles,
|
||||
elem.span(),
|
||||
)
|
||||
.trace(engine.world, tracepoint, elem.span())?;
|
||||
|
||||
let layouter = GridLayouter::new(&grid, regions, styles, elem.span());
|
||||
|
||||
// Measure the columns and layout the grid row-by-row.
|
||||
layouter.layout(engine)
|
||||
}
|
||||
|
||||
/// Track sizing definitions.
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct TrackSizings(pub SmallVec<[Sizing; 4]>);
|
||||
@ -956,7 +960,7 @@ pub fn show_grid_cell(
|
||||
}
|
||||
|
||||
if let Smart::Custom(alignment) = align {
|
||||
body = body.styled(AlignElem::set_alignment(alignment));
|
||||
body = body.aligned(alignment);
|
||||
}
|
||||
|
||||
Ok(body)
|
||||
|
@ -3,9 +3,7 @@ use super::repeated::Repeatable;
|
||||
use crate::diag::SourceResult;
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::Resolve;
|
||||
use crate::layout::{
|
||||
Abs, Axes, Cell, Frame, GridLayouter, LayoutMultiple, Point, Regions, Size, Sizing,
|
||||
};
|
||||
use crate::layout::{Abs, Axes, Cell, Frame, GridLayouter, Point, Regions, Size, Sizing};
|
||||
use crate::utils::MaybeReverseIter;
|
||||
|
||||
/// All information needed to layout a single rowspan.
|
||||
@ -138,7 +136,7 @@ impl<'a> GridLayouter<'a> {
|
||||
}
|
||||
|
||||
// Push the layouted frames directly into the finished frames.
|
||||
let fragment = cell.layout(engine, self.styles, pod)?;
|
||||
let fragment = cell.body.layout(engine, self.styles, pod)?;
|
||||
let (current_region, current_rrows) = current_region_data.unzip();
|
||||
for ((i, finished), frame) in self
|
||||
.finished
|
||||
|
@ -16,10 +16,9 @@ use crate::eval::Tracer;
|
||||
use crate::foundations::{Content, Packed, Resolve, Smart, StyleChain, StyledElem};
|
||||
use crate::introspection::{Introspector, Locator, TagElem};
|
||||
use crate::layout::{
|
||||
Abs, AlignElem, Axes, BoxElem, Dir, Em, FixedAlignment, Fr, Fragment, Frame,
|
||||
FrameItem, HElem, Point, Regions, Size, Sizing, Spacing,
|
||||
Abs, AlignElem, BoxElem, Dir, Em, FixedAlignment, Fr, Fragment, Frame, FrameItem,
|
||||
HElem, InlineElem, InlineItem, Point, Size, Sizing, Spacing,
|
||||
};
|
||||
use crate::math::{EquationElem, MathParItem};
|
||||
use crate::model::{Linebreaks, ParElem};
|
||||
use crate::syntax::Span;
|
||||
use crate::text::{
|
||||
@ -220,7 +219,7 @@ impl Segment<'_> {
|
||||
enum Item<'a> {
|
||||
/// A shaped text run with consistent style and direction.
|
||||
Text(ShapedText<'a>),
|
||||
/// Absolute spacing between other items.
|
||||
/// Absolute spacing between other items, and whether it is weak.
|
||||
Absolute(Abs, bool),
|
||||
/// Fractional spacing between other items.
|
||||
Fractional(Fr, Option<(&'a Packed<BoxElem>, StyleChain<'a>)>),
|
||||
@ -544,17 +543,15 @@ fn collect<'a>(
|
||||
} else {
|
||||
collector.push_text(if double { "\"" } else { "'" }, styles);
|
||||
}
|
||||
} else if let Some(elem) = child.to_packed::<EquationElem>() {
|
||||
} else if let Some(elem) = child.to_packed::<InlineElem>() {
|
||||
collector.push_item(Item::Skip(LTR_ISOLATE));
|
||||
|
||||
let pod = Regions::one(region, Axes::splat(false));
|
||||
for item in elem.layout_inline(engine, styles, pod)? {
|
||||
for item in elem.layout(engine, styles, region)? {
|
||||
match item {
|
||||
MathParItem::Space(space) => {
|
||||
// Spaces generated by math layout are weak.
|
||||
collector.push_item(Item::Absolute(space, true));
|
||||
InlineItem::Space(space, weak) => {
|
||||
collector.push_item(Item::Absolute(space, weak));
|
||||
}
|
||||
MathParItem::Frame(frame) => {
|
||||
InlineItem::Frame(frame) => {
|
||||
collector.push_item(Item::Frame(frame, styles));
|
||||
}
|
||||
}
|
||||
@ -565,8 +562,7 @@ fn collect<'a>(
|
||||
if let Sizing::Fr(v) = elem.width(styles) {
|
||||
collector.push_item(Item::Fractional(v, Some((elem, styles))));
|
||||
} else {
|
||||
let pod = Regions::one(region, Axes::splat(false));
|
||||
let frame = elem.layout(engine, styles, pod)?;
|
||||
let frame = elem.layout(engine, styles, region)?;
|
||||
collector.push_item(Item::Frame(frame, styles));
|
||||
}
|
||||
} else if let Some(elem) = child.to_packed::<TagElem>() {
|
||||
@ -1440,8 +1436,7 @@ fn commit(
|
||||
let amount = v.share(fr, remaining);
|
||||
if let Some((elem, styles)) = elem {
|
||||
let region = Size::new(amount, full);
|
||||
let pod = Regions::one(region, Axes::new(true, false));
|
||||
let mut frame = elem.layout(engine, *styles, pod)?;
|
||||
let mut frame = elem.layout(engine, *styles, region)?;
|
||||
frame.post_process(*styles);
|
||||
frame.translate(Point::with_y(TextElem::baseline_in(*styles)));
|
||||
push(&mut offset, frame);
|
||||
|
@ -3,10 +3,10 @@ use comemo::Track;
|
||||
use crate::diag::SourceResult;
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{
|
||||
dict, elem, func, Content, Context, Func, NativeElement, Packed, StyleChain,
|
||||
dict, elem, func, Content, Context, Func, NativeElement, Packed, Show, StyleChain,
|
||||
};
|
||||
use crate::introspection::Locatable;
|
||||
use crate::layout::{Fragment, LayoutMultiple, Regions, Size};
|
||||
use crate::layout::{BlockElem, Size};
|
||||
use crate::syntax::Span;
|
||||
|
||||
/// Provides access to the current outer container's (or page's, if none)
|
||||
@ -67,30 +67,27 @@ pub fn layout(
|
||||
}
|
||||
|
||||
/// Executes a `layout` call.
|
||||
#[elem(Locatable, LayoutMultiple)]
|
||||
#[elem(Locatable, Show)]
|
||||
struct LayoutElem {
|
||||
/// The function to call with the outer container's (or page's) size.
|
||||
#[required]
|
||||
func: Func,
|
||||
}
|
||||
|
||||
impl LayoutMultiple for Packed<LayoutElem> {
|
||||
#[typst_macros::time(name = "layout", span = self.span())]
|
||||
fn layout(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment> {
|
||||
// Gets the current region's base size, which will be the size of the
|
||||
// outer container, or of the page if there is no such container.
|
||||
let Size { x, y } = regions.base();
|
||||
let loc = self.location().unwrap();
|
||||
let context = Context::new(Some(loc), Some(styles));
|
||||
let result = self
|
||||
.func()
|
||||
.call(engine, context.track(), [dict! { "width" => x, "height" => y }])?
|
||||
.display();
|
||||
result.layout(engine, styles, regions)
|
||||
impl Show for Packed<LayoutElem> {
|
||||
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
|
||||
Ok(BlockElem::multi_layouter(self.clone(), |elem, engine, styles, regions| {
|
||||
// Gets the current region's base size, which will be the size of the
|
||||
// outer container, or of the page if there is no such container.
|
||||
let Size { x, y } = regions.base();
|
||||
let loc = elem.location().unwrap();
|
||||
let context = Context::new(Some(loc), Some(styles));
|
||||
let result = elem
|
||||
.func()
|
||||
.call(engine, context.track(), [dict! { "width" => x, "height" => y }])?
|
||||
.display();
|
||||
result.layout(engine, styles, regions)
|
||||
})
|
||||
.pack())
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ use crate::engine::Engine;
|
||||
use crate::foundations::{
|
||||
dict, func, Content, Context, Dict, Resolve, Smart, StyleChain, Styles,
|
||||
};
|
||||
use crate::layout::{Abs, Axes, LayoutMultiple, Length, Regions, Size};
|
||||
use crate::layout::{Abs, Axes, Length, Regions, Size};
|
||||
use crate::syntax::Span;
|
||||
|
||||
/// Measures the layouted size of content.
|
||||
|
@ -58,7 +58,7 @@ pub use self::page::*;
|
||||
pub use self::place::*;
|
||||
pub use self::point::*;
|
||||
pub use self::ratio::*;
|
||||
pub use self::regions::Regions;
|
||||
pub use self::regions::*;
|
||||
pub use self::rel::*;
|
||||
pub use self::repeat::*;
|
||||
pub use self::sides::*;
|
||||
@ -119,72 +119,15 @@ pub fn define(global: &mut Scope) {
|
||||
global.define_func::<layout>();
|
||||
}
|
||||
|
||||
/// Root-level layout.
|
||||
///
|
||||
/// This produces a complete document and is implemented for
|
||||
/// [`DocumentElem`][crate::model::DocumentElem]. Any [`Content`]
|
||||
/// can also be laid out at root level, in which case it is
|
||||
/// wrapped inside a document element.
|
||||
pub trait LayoutRoot {
|
||||
/// Layout into a document with one frame per page.
|
||||
fn layout_root(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<Document>;
|
||||
}
|
||||
|
||||
/// Layout into multiple [regions][Regions].
|
||||
///
|
||||
/// This is more appropriate for elements that, for example, can be
|
||||
/// laid out across multiple pages or columns.
|
||||
pub trait LayoutMultiple {
|
||||
/// Layout into one frame per region.
|
||||
fn layout(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment>;
|
||||
|
||||
/// Layout without side effects.
|
||||
impl Content {
|
||||
/// Layout the content into a document.
|
||||
///
|
||||
/// This element must be layouted again in the same order for the results to
|
||||
/// be valid.
|
||||
fn measure(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment> {
|
||||
let mut locator = Locator::chained(engine.locator.track());
|
||||
let mut engine = Engine {
|
||||
world: engine.world,
|
||||
route: engine.route.clone(),
|
||||
introspector: engine.introspector,
|
||||
locator: &mut locator,
|
||||
tracer: TrackedMut::reborrow_mut(&mut engine.tracer),
|
||||
};
|
||||
self.layout(&mut engine, styles, regions)
|
||||
}
|
||||
}
|
||||
|
||||
/// Layout into a single [region][Regions].
|
||||
///
|
||||
/// This is more appropriate for elements that don't make sense to
|
||||
/// layout across multiple pages or columns, such as shapes.
|
||||
pub trait LayoutSingle {
|
||||
/// Layout into one frame per region.
|
||||
fn layout(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Frame>;
|
||||
}
|
||||
|
||||
impl LayoutRoot for Content {
|
||||
fn layout_root(
|
||||
/// This first realizes the content into a
|
||||
/// [`DocumentElem`][crate::model::DocumentElem], which is then laid out. In
|
||||
/// contrast to [`layout`](Self::layout()), this does not take regions since
|
||||
/// the regions are defined by the page configuration in the content and
|
||||
/// style chain.
|
||||
pub fn layout_document(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
@ -209,7 +152,7 @@ impl LayoutRoot for Content {
|
||||
};
|
||||
let arenas = Arenas::default();
|
||||
let (document, styles) = realize_doc(&mut engine, &arenas, content, styles)?;
|
||||
document.layout_root(&mut engine, styles)
|
||||
document.layout(&mut engine, styles)
|
||||
}
|
||||
|
||||
cached(
|
||||
@ -222,10 +165,25 @@ impl LayoutRoot for Content {
|
||||
styles,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl LayoutMultiple for Content {
|
||||
fn layout(
|
||||
/// Layout the content into the given regions.
|
||||
pub fn layout(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment> {
|
||||
let fragment = self.measure(engine, styles, regions)?;
|
||||
engine.locator.visit_frames(&fragment);
|
||||
Ok(fragment)
|
||||
}
|
||||
|
||||
/// Layout without side effects.
|
||||
///
|
||||
/// For the results to be valid, the element must either be layouted again
|
||||
/// or the measurement must be confirmed through a call to
|
||||
/// `engine.locator.visit_frames(&fragment)`.
|
||||
pub fn measure(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
@ -271,7 +229,7 @@ impl LayoutMultiple for Content {
|
||||
flow.layout(&mut engine, styles, regions)
|
||||
}
|
||||
|
||||
let fragment = cached(
|
||||
cached(
|
||||
self,
|
||||
engine.world,
|
||||
engine.introspector,
|
||||
@ -280,9 +238,6 @@ impl LayoutMultiple for Content {
|
||||
TrackedMut::reborrow_mut(&mut engine.tracer),
|
||||
styles,
|
||||
regions,
|
||||
)?;
|
||||
|
||||
engine.locator.visit_frames(&fragment);
|
||||
Ok(fragment)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
use crate::diag::SourceResult;
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{elem, Content, Packed, Resolve, StyleChain};
|
||||
use crate::foundations::{
|
||||
elem, Content, NativeElement, Packed, Resolve, Show, StyleChain,
|
||||
};
|
||||
use crate::layout::{
|
||||
Abs, Fragment, LayoutMultiple, Length, Point, Regions, Rel, Sides, Size,
|
||||
Abs, BlockElem, Fragment, Frame, Length, Point, Regions, Rel, Sides, Size,
|
||||
};
|
||||
|
||||
/// Adds spacing around content.
|
||||
@ -18,7 +20,7 @@ use crate::layout::{
|
||||
/// _Typing speeds can be
|
||||
/// measured in words per minute._
|
||||
/// ```
|
||||
#[elem(title = "Padding", LayoutMultiple)]
|
||||
#[elem(title = "Padding", Show)]
|
||||
pub struct PadElem {
|
||||
/// The padding at the left side.
|
||||
#[parse(
|
||||
@ -60,49 +62,64 @@ pub struct PadElem {
|
||||
pub body: Content,
|
||||
}
|
||||
|
||||
impl LayoutMultiple for Packed<PadElem> {
|
||||
#[typst_macros::time(name = "pad", span = self.span())]
|
||||
fn layout(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment> {
|
||||
let sides = Sides::new(
|
||||
self.left(styles),
|
||||
self.top(styles),
|
||||
self.right(styles),
|
||||
self.bottom(styles),
|
||||
);
|
||||
|
||||
// Layout child into padded regions.
|
||||
let mut backlog = vec![];
|
||||
let padding = sides.resolve(styles);
|
||||
let pod = regions.map(&mut backlog, |size| shrink(size, padding));
|
||||
let mut fragment = self.body().layout(engine, styles, pod)?;
|
||||
|
||||
for frame in &mut fragment {
|
||||
// Apply the padding inversely such that the grown size padded
|
||||
// yields the frame's size.
|
||||
let padded = grow(frame.size(), padding);
|
||||
let padding = padding.relative_to(padded);
|
||||
let offset = Point::new(padding.left, padding.top);
|
||||
|
||||
// Grow the frame and translate everything in the frame inwards.
|
||||
frame.set_size(padded);
|
||||
frame.translate(offset);
|
||||
}
|
||||
|
||||
Ok(fragment)
|
||||
impl Show for Packed<PadElem> {
|
||||
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
|
||||
Ok(BlockElem::multi_layouter(self.clone(), layout_pad).pack())
|
||||
}
|
||||
}
|
||||
|
||||
/// Shrink a size by padding relative to the size itself.
|
||||
fn shrink(size: Size, padding: Sides<Rel<Abs>>) -> Size {
|
||||
size - padding.relative_to(size).sum_by_axis()
|
||||
/// Layout the padded content.
|
||||
#[typst_macros::time(span = elem.span())]
|
||||
fn layout_pad(
|
||||
elem: &Packed<PadElem>,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment> {
|
||||
let padding = Sides::new(
|
||||
elem.left(styles).resolve(styles),
|
||||
elem.top(styles).resolve(styles),
|
||||
elem.right(styles).resolve(styles),
|
||||
elem.bottom(styles).resolve(styles),
|
||||
);
|
||||
|
||||
let mut backlog = vec![];
|
||||
let pod = regions.map(&mut backlog, |size| shrink(size, &padding));
|
||||
|
||||
// Layout child into padded regions.
|
||||
let mut fragment = elem.body().layout(engine, styles, pod)?;
|
||||
|
||||
for frame in &mut fragment {
|
||||
grow(frame, &padding);
|
||||
}
|
||||
|
||||
Ok(fragment)
|
||||
}
|
||||
|
||||
/// Grow a size by padding relative to the grown size.
|
||||
/// Shrink a region size by an inset relative to the size itself.
|
||||
pub(crate) fn shrink(size: Size, inset: &Sides<Rel<Abs>>) -> Size {
|
||||
size - inset.sum_by_axis().relative_to(size)
|
||||
}
|
||||
|
||||
/// Shrink the components of possibly multiple `Regions` by an inset relative to
|
||||
/// the regions themselves.
|
||||
pub(crate) fn shrink_multiple(
|
||||
size: &mut Size,
|
||||
full: &mut Abs,
|
||||
backlog: &mut [Abs],
|
||||
last: &mut Option<Abs>,
|
||||
inset: &Sides<Rel<Abs>>,
|
||||
) {
|
||||
let summed = inset.sum_by_axis();
|
||||
*size -= summed.relative_to(*size);
|
||||
*full -= summed.y.relative_to(*full);
|
||||
for item in backlog {
|
||||
*item -= summed.y.relative_to(*item);
|
||||
}
|
||||
*last = last.map(|v| v - summed.y.relative_to(v));
|
||||
}
|
||||
|
||||
/// Grow a frame's size by an inset relative to the grown size.
|
||||
/// This is the inverse operation to `shrink()`.
|
||||
///
|
||||
/// For the horizontal axis the derivation looks as follows.
|
||||
@ -110,8 +127,8 @@ fn shrink(size: Size, padding: Sides<Rel<Abs>>) -> Size {
|
||||
///
|
||||
/// Let w be the grown target width,
|
||||
/// s be the given width,
|
||||
/// l be the left padding,
|
||||
/// r be the right padding,
|
||||
/// l be the left inset,
|
||||
/// r be the right inset,
|
||||
/// p = l + r.
|
||||
///
|
||||
/// We want that: w - l.resolve(w) - r.resolve(w) = s
|
||||
@ -121,6 +138,17 @@ fn shrink(size: Size, padding: Sides<Rel<Abs>>) -> Size {
|
||||
/// <=> w - p.rel * w - p.abs = s
|
||||
/// <=> (1 - p.rel) * w = s + p.abs
|
||||
/// <=> w = (s + p.abs) / (1 - p.rel)
|
||||
fn grow(size: Size, padding: Sides<Rel<Abs>>) -> Size {
|
||||
size.zip_map(padding.sum_by_axis(), |s, p| (s + p.abs) / (1.0 - p.rel.get()))
|
||||
pub(crate) fn grow(frame: &mut Frame, inset: &Sides<Rel<Abs>>) {
|
||||
// Apply the padding inversely such that the grown size padded
|
||||
// yields the frame's size.
|
||||
let padded = frame
|
||||
.size()
|
||||
.zip_map(inset.sum_by_axis(), |s, p| (s + p.abs) / (1.0 - p.rel.get()));
|
||||
|
||||
let inset = inset.relative_to(padded);
|
||||
let offset = Point::new(inset.left, inset.top);
|
||||
|
||||
// Grow the frame and translate everything in the frame inwards.
|
||||
frame.set_size(padded);
|
||||
frame.translate(offset);
|
||||
}
|
||||
|
@ -14,8 +14,8 @@ use crate::foundations::{
|
||||
};
|
||||
use crate::introspection::{Counter, CounterDisplayElem, CounterKey, ManualPageCounter};
|
||||
use crate::layout::{
|
||||
Abs, AlignElem, Alignment, Axes, ColumnsElem, Dir, Frame, HAlignment, LayoutMultiple,
|
||||
Length, OuterVAlignment, Point, Ratio, Regions, Rel, Sides, Size, SpecificAlignment,
|
||||
Abs, AlignElem, Alignment, Axes, ColumnsElem, Dir, Frame, HAlignment, Length,
|
||||
OuterVAlignment, Point, Ratio, Regions, Rel, Sides, Size, SpecificAlignment,
|
||||
VAlignment,
|
||||
};
|
||||
|
||||
|
@ -2,7 +2,7 @@ use crate::diag::{bail, At, Hint, SourceResult};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{elem, scope, Content, Packed, Smart, StyleChain, Unlabellable};
|
||||
use crate::layout::{
|
||||
Alignment, Axes, Em, Fragment, LayoutMultiple, Length, Regions, Rel, Size, VAlignment,
|
||||
Alignment, Axes, Em, Fragment, Length, Regions, Rel, Size, VAlignment,
|
||||
};
|
||||
use crate::realize::{Behave, Behaviour};
|
||||
|
||||
|
@ -2,6 +2,28 @@ use std::fmt::{self, Debug, Formatter};
|
||||
|
||||
use crate::layout::{Abs, Axes, Size};
|
||||
|
||||
/// A single region to layout into.
|
||||
#[derive(Debug, Copy, Clone, Hash)]
|
||||
pub struct Region {
|
||||
/// The size of the region.
|
||||
pub size: Size,
|
||||
/// Whether elements should expand to fill the regions instead of shrinking
|
||||
/// to fit the content.
|
||||
pub expand: Axes<bool>,
|
||||
}
|
||||
|
||||
impl Region {
|
||||
/// Create a new region.
|
||||
pub fn new(size: Size, expand: Axes<bool>) -> Self {
|
||||
Self { size, expand }
|
||||
}
|
||||
|
||||
/// Turns this into a region sequence.
|
||||
pub fn into_regions(self) -> Regions<'static> {
|
||||
Regions::one(self.size, self.expand)
|
||||
}
|
||||
}
|
||||
|
||||
/// A sequence of regions to layout into.
|
||||
///
|
||||
/// A *region* is a contiguous rectangular space in which elements
|
||||
@ -80,7 +102,7 @@ impl Regions<'_> {
|
||||
backlog,
|
||||
last: self.last.map(|y| f(Size::new(x, y)).y),
|
||||
expand: self.expand,
|
||||
root: false,
|
||||
root: self.root,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,10 @@
|
||||
use crate::diag::{bail, SourceResult};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{elem, Content, Packed, Resolve, StyleChain};
|
||||
use crate::foundations::{
|
||||
elem, Content, NativeElement, Packed, Resolve, Show, StyleChain,
|
||||
};
|
||||
use crate::layout::{
|
||||
Abs, AlignElem, Axes, Fragment, Frame, LayoutMultiple, Point, Regions, Size,
|
||||
Abs, AlignElem, Axes, BlockElem, Fragment, Frame, Point, Regions, Size,
|
||||
};
|
||||
use crate::utils::Numeric;
|
||||
|
||||
@ -27,54 +29,59 @@ use crate::utils::Numeric;
|
||||
/// Berlin, the 22nd of December, 2022
|
||||
/// ]
|
||||
/// ```
|
||||
#[elem(LayoutMultiple)]
|
||||
#[elem(Show)]
|
||||
pub struct RepeatElem {
|
||||
/// The content to repeat.
|
||||
#[required]
|
||||
pub body: Content,
|
||||
}
|
||||
|
||||
impl LayoutMultiple for Packed<RepeatElem> {
|
||||
#[typst_macros::time(name = "repeat", span = self.span())]
|
||||
fn layout(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment> {
|
||||
let pod = Regions::one(regions.size, Axes::new(false, false));
|
||||
let piece = self.body().layout(engine, styles, pod)?.into_frame();
|
||||
let align = AlignElem::alignment_in(styles).resolve(styles);
|
||||
|
||||
let fill = regions.size.x;
|
||||
let width = piece.width();
|
||||
let count = (fill / width).floor();
|
||||
let remaining = fill % width;
|
||||
let apart = remaining / (count - 1.0);
|
||||
|
||||
let size = Size::new(regions.size.x, piece.height());
|
||||
|
||||
if !size.is_finite() {
|
||||
bail!(self.span(), "repeat with no size restrictions");
|
||||
}
|
||||
|
||||
let mut frame = Frame::soft(size);
|
||||
if piece.has_baseline() {
|
||||
frame.set_baseline(piece.baseline());
|
||||
}
|
||||
|
||||
let mut offset = Abs::zero();
|
||||
if count == 1.0 {
|
||||
offset += align.x.position(remaining);
|
||||
}
|
||||
|
||||
if width > Abs::zero() {
|
||||
for _ in 0..(count as usize).min(1000) {
|
||||
frame.push_frame(Point::with_x(offset), piece.clone());
|
||||
offset += piece.width() + apart;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Fragment::frame(frame))
|
||||
impl Show for Packed<RepeatElem> {
|
||||
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
|
||||
Ok(BlockElem::multi_layouter(self.clone(), layout_repeat).pack())
|
||||
}
|
||||
}
|
||||
|
||||
/// Layout the repeated content.
|
||||
#[typst_macros::time(span = elem.span())]
|
||||
fn layout_repeat(
|
||||
elem: &Packed<RepeatElem>,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment> {
|
||||
let pod = Regions::one(regions.size, Axes::new(false, false));
|
||||
let piece = elem.body().layout(engine, styles, pod)?.into_frame();
|
||||
let align = AlignElem::alignment_in(styles).resolve(styles);
|
||||
|
||||
let fill = regions.size.x;
|
||||
let width = piece.width();
|
||||
let count = (fill / width).floor();
|
||||
let remaining = fill % width;
|
||||
let apart = remaining / (count - 1.0);
|
||||
|
||||
let size = Size::new(regions.size.x, piece.height());
|
||||
|
||||
if !size.is_finite() {
|
||||
bail!(elem.span(), "repeat with no size restrictions");
|
||||
}
|
||||
|
||||
let mut frame = Frame::soft(size);
|
||||
if piece.has_baseline() {
|
||||
frame.set_baseline(piece.baseline());
|
||||
}
|
||||
|
||||
let mut offset = Abs::zero();
|
||||
if count == 1.0 {
|
||||
offset += align.x.position(remaining);
|
||||
}
|
||||
|
||||
if width > Abs::zero() {
|
||||
for _ in 0..(count as usize).min(1000) {
|
||||
frame.push_frame(Point::with_x(offset), piece.clone());
|
||||
offset += piece.width() + apart;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Fragment::frame(frame))
|
||||
}
|
||||
|
@ -107,7 +107,7 @@ impl<T> Sides<Option<T>> {
|
||||
|
||||
impl Sides<Rel<Abs>> {
|
||||
/// Evaluate the sides relative to the given `size`.
|
||||
pub fn relative_to(self, size: Size) -> Sides<Abs> {
|
||||
pub fn relative_to(&self, size: Size) -> Sides<Abs> {
|
||||
Sides {
|
||||
left: self.left.relative_to(size.x),
|
||||
top: self.top.relative_to(size.y),
|
||||
@ -115,6 +115,14 @@ impl Sides<Rel<Abs>> {
|
||||
bottom: self.bottom.relative_to(size.y),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether all sides are zero.
|
||||
pub fn is_zero(&self) -> bool {
|
||||
self.left.is_zero()
|
||||
&& self.top.is_zero()
|
||||
&& self.right.is_zero()
|
||||
&& self.bottom.is_zero()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Get<Side> for Sides<T> {
|
||||
|
@ -130,6 +130,11 @@ pub struct VElem {
|
||||
#[internal]
|
||||
#[parse(args.named("weak")?.map(|v: bool| v as usize))]
|
||||
pub weakness: usize,
|
||||
|
||||
/// Whether the element collapses if there is a parbreak in front.
|
||||
#[internal]
|
||||
#[parse(Some(false))]
|
||||
pub attach: bool,
|
||||
}
|
||||
|
||||
impl VElem {
|
||||
@ -145,7 +150,7 @@ impl VElem {
|
||||
|
||||
/// Weak spacing with list attach weakness.
|
||||
pub fn list_attach(amount: Spacing) -> Self {
|
||||
Self::new(amount).with_weakness(2)
|
||||
Self::new(amount).with_weakness(2).with_attach(true)
|
||||
}
|
||||
|
||||
/// Weak spacing with BlockElem::ABOVE/BELOW weakness.
|
||||
|
@ -3,10 +3,12 @@ use typst_syntax::Span;
|
||||
|
||||
use crate::diag::{bail, SourceResult};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{cast, elem, Content, Packed, Resolve, StyleChain, StyledElem};
|
||||
use crate::foundations::{
|
||||
cast, elem, Content, NativeElement, Packed, Resolve, Show, StyleChain, StyledElem,
|
||||
};
|
||||
use crate::layout::{
|
||||
Abs, AlignElem, Axes, Axis, Dir, FixedAlignment, Fr, Fragment, Frame, HElem,
|
||||
LayoutMultiple, Point, Regions, Size, Spacing, VElem,
|
||||
Abs, AlignElem, Axes, Axis, BlockElem, Dir, FixedAlignment, Fr, Fragment, Frame,
|
||||
HElem, Point, Regions, Size, Spacing, VElem,
|
||||
};
|
||||
use crate::utils::{Get, Numeric};
|
||||
|
||||
@ -24,7 +26,7 @@ use crate::utils::{Get, Numeric};
|
||||
/// rect(width: 90pt),
|
||||
/// )
|
||||
/// ```
|
||||
#[elem(LayoutMultiple)]
|
||||
#[elem(Show)]
|
||||
pub struct StackElem {
|
||||
/// The direction along which the items are stacked. Possible values are:
|
||||
///
|
||||
@ -52,54 +54,9 @@ pub struct StackElem {
|
||||
pub children: Vec<StackChild>,
|
||||
}
|
||||
|
||||
impl LayoutMultiple for Packed<StackElem> {
|
||||
#[typst_macros::time(name = "stack", span = self.span())]
|
||||
fn layout(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment> {
|
||||
let mut layouter =
|
||||
StackLayouter::new(self.span(), self.dir(styles), regions, styles);
|
||||
let axis = layouter.dir.axis();
|
||||
|
||||
// Spacing to insert before the next block.
|
||||
let spacing = self.spacing(styles);
|
||||
let mut deferred = None;
|
||||
|
||||
for child in self.children() {
|
||||
match child {
|
||||
StackChild::Spacing(kind) => {
|
||||
layouter.layout_spacing(*kind);
|
||||
deferred = None;
|
||||
}
|
||||
StackChild::Block(block) => {
|
||||
// Transparently handle `h`.
|
||||
if let (Axis::X, Some(h)) = (axis, block.to_packed::<HElem>()) {
|
||||
layouter.layout_spacing(*h.amount());
|
||||
deferred = None;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Transparently handle `v`.
|
||||
if let (Axis::Y, Some(v)) = (axis, block.to_packed::<VElem>()) {
|
||||
layouter.layout_spacing(*v.amount());
|
||||
deferred = None;
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(kind) = deferred {
|
||||
layouter.layout_spacing(kind);
|
||||
}
|
||||
|
||||
layouter.layout_block(engine, block, styles)?;
|
||||
deferred = spacing;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
layouter.finish()
|
||||
impl Show for Packed<StackElem> {
|
||||
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
|
||||
Ok(BlockElem::multi_layouter(self.clone(), layout_stack).pack())
|
||||
}
|
||||
}
|
||||
|
||||
@ -131,6 +88,55 @@ cast! {
|
||||
v: Content => Self::Block(v),
|
||||
}
|
||||
|
||||
/// Layout the stack.
|
||||
#[typst_macros::time(span = elem.span())]
|
||||
fn layout_stack(
|
||||
elem: &Packed<StackElem>,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment> {
|
||||
let mut layouter = StackLayouter::new(elem.span(), elem.dir(styles), regions, styles);
|
||||
let axis = layouter.dir.axis();
|
||||
|
||||
// Spacing to insert before the next block.
|
||||
let spacing = elem.spacing(styles);
|
||||
let mut deferred = None;
|
||||
|
||||
for child in elem.children() {
|
||||
match child {
|
||||
StackChild::Spacing(kind) => {
|
||||
layouter.layout_spacing(*kind);
|
||||
deferred = None;
|
||||
}
|
||||
StackChild::Block(block) => {
|
||||
// Transparently handle `h`.
|
||||
if let (Axis::X, Some(h)) = (axis, block.to_packed::<HElem>()) {
|
||||
layouter.layout_spacing(*h.amount());
|
||||
deferred = None;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Transparently handle `v`.
|
||||
if let (Axis::Y, Some(v)) = (axis, block.to_packed::<VElem>()) {
|
||||
layouter.layout_spacing(*v.amount());
|
||||
deferred = None;
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(kind) = deferred {
|
||||
layouter.layout_spacing(kind);
|
||||
}
|
||||
|
||||
layouter.layout_block(engine, block, styles)?;
|
||||
deferred = spacing;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
layouter.finish()
|
||||
}
|
||||
|
||||
/// Performs stack layout.
|
||||
struct StackLayouter<'a> {
|
||||
/// The span to raise errors at during layout.
|
||||
@ -231,7 +237,7 @@ impl<'a> StackLayouter<'a> {
|
||||
self.finish_region()?;
|
||||
}
|
||||
|
||||
// Block-axis alignment of the `AlignElement` is respected by stacks.
|
||||
// Block-axis alignment of the `AlignElem` is respected by stacks.
|
||||
let align = if let Some(align) = block.to_packed::<AlignElem>() {
|
||||
align.alignment(styles)
|
||||
} else if let Some(styled) = block.to_packed::<StyledElem>() {
|
||||
|
@ -1,9 +1,11 @@
|
||||
use crate::diag::SourceResult;
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{elem, Content, Packed, Resolve, StyleChain};
|
||||
use crate::foundations::{
|
||||
elem, Content, NativeElement, Packed, Resolve, Show, StyleChain,
|
||||
};
|
||||
use crate::layout::{
|
||||
Abs, Alignment, Angle, Axes, FixedAlignment, Frame, HAlignment, LayoutMultiple,
|
||||
LayoutSingle, Length, Point, Ratio, Regions, Rel, Size, VAlignment,
|
||||
Abs, Alignment, Angle, Axes, BlockElem, FixedAlignment, Frame, HAlignment, Length,
|
||||
Point, Ratio, Region, Regions, Rel, Size, VAlignment,
|
||||
};
|
||||
|
||||
/// Moves content without affecting layout.
|
||||
@ -24,7 +26,7 @@ use crate::layout::{
|
||||
/// )
|
||||
/// ))
|
||||
/// ```
|
||||
#[elem(LayoutSingle)]
|
||||
#[elem(Show)]
|
||||
pub struct MoveElem {
|
||||
/// The horizontal displacement of the content.
|
||||
pub dx: Rel<Length>,
|
||||
@ -37,23 +39,30 @@ pub struct MoveElem {
|
||||
pub body: Content,
|
||||
}
|
||||
|
||||
impl LayoutSingle for Packed<MoveElem> {
|
||||
#[typst_macros::time(name = "move", span = self.span())]
|
||||
fn layout(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Frame> {
|
||||
let pod = Regions::one(regions.base(), Axes::splat(false));
|
||||
let mut frame = self.body().layout(engine, styles, pod)?.into_frame();
|
||||
let delta = Axes::new(self.dx(styles), self.dy(styles)).resolve(styles);
|
||||
let delta = delta.zip_map(regions.base(), Rel::relative_to);
|
||||
frame.translate(delta.to_point());
|
||||
Ok(frame)
|
||||
impl Show for Packed<MoveElem> {
|
||||
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
|
||||
Ok(BlockElem::single_layouter(self.clone(), layout_move).pack())
|
||||
}
|
||||
}
|
||||
|
||||
/// Layout the moved content.
|
||||
#[typst_macros::time(span = elem.span())]
|
||||
fn layout_move(
|
||||
elem: &Packed<MoveElem>,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
region: Region,
|
||||
) -> SourceResult<Frame> {
|
||||
let mut frame = elem
|
||||
.body()
|
||||
.layout(engine, styles, region.into_regions())?
|
||||
.into_frame();
|
||||
let delta = Axes::new(elem.dx(styles), elem.dy(styles)).resolve(styles);
|
||||
let delta = delta.zip_map(region.size, Rel::relative_to);
|
||||
frame.translate(delta.to_point());
|
||||
Ok(frame)
|
||||
}
|
||||
|
||||
/// Rotates content without affecting layout.
|
||||
///
|
||||
/// Rotates an element by a given angle. The layout will act as if the element
|
||||
@ -68,7 +77,7 @@ impl LayoutSingle for Packed<MoveElem> {
|
||||
/// .map(i => rotate(24deg * i)[X]),
|
||||
/// )
|
||||
/// ```
|
||||
#[elem(LayoutSingle)]
|
||||
#[elem(Show)]
|
||||
pub struct RotateElem {
|
||||
/// The amount of rotation.
|
||||
///
|
||||
@ -115,38 +124,43 @@ pub struct RotateElem {
|
||||
pub body: Content,
|
||||
}
|
||||
|
||||
impl LayoutSingle for Packed<RotateElem> {
|
||||
#[typst_macros::time(name = "rotate", span = self.span())]
|
||||
fn layout(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Frame> {
|
||||
let angle = self.angle(styles);
|
||||
let align = self.origin(styles).resolve(styles);
|
||||
|
||||
// Compute the new region's approximate size.
|
||||
let size = regions
|
||||
.base()
|
||||
.to_point()
|
||||
.transform_inf(Transform::rotate(angle))
|
||||
.map(Abs::abs)
|
||||
.to_size();
|
||||
|
||||
measure_and_layout(
|
||||
engine,
|
||||
regions.base(),
|
||||
size,
|
||||
styles,
|
||||
self.body(),
|
||||
Transform::rotate(angle),
|
||||
align,
|
||||
self.reflow(styles),
|
||||
)
|
||||
impl Show for Packed<RotateElem> {
|
||||
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
|
||||
Ok(BlockElem::single_layouter(self.clone(), layout_rotate).pack())
|
||||
}
|
||||
}
|
||||
|
||||
/// Layout the rotated content.
|
||||
#[typst_macros::time(span = elem.span())]
|
||||
fn layout_rotate(
|
||||
elem: &Packed<RotateElem>,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
region: Region,
|
||||
) -> SourceResult<Frame> {
|
||||
let angle = elem.angle(styles);
|
||||
let align = elem.origin(styles).resolve(styles);
|
||||
|
||||
// Compute the new region's approximate size.
|
||||
let size = region
|
||||
.size
|
||||
.to_point()
|
||||
.transform_inf(Transform::rotate(angle))
|
||||
.map(Abs::abs)
|
||||
.to_size();
|
||||
|
||||
measure_and_layout(
|
||||
engine,
|
||||
region,
|
||||
size,
|
||||
styles,
|
||||
elem.body(),
|
||||
Transform::rotate(angle),
|
||||
align,
|
||||
elem.reflow(styles),
|
||||
)
|
||||
}
|
||||
|
||||
/// Scales content without affecting layout.
|
||||
///
|
||||
/// Lets you mirror content by specifying a negative scale on a single axis.
|
||||
@ -157,7 +171,7 @@ impl LayoutSingle for Packed<RotateElem> {
|
||||
/// #scale(x: -100%)[This is mirrored.]
|
||||
/// #scale(x: -100%, reflow: true)[This is mirrored.]
|
||||
/// ```
|
||||
#[elem(LayoutSingle)]
|
||||
#[elem(Show)]
|
||||
pub struct ScaleElem {
|
||||
/// The horizontal scaling factor.
|
||||
///
|
||||
@ -203,37 +217,39 @@ pub struct ScaleElem {
|
||||
pub body: Content,
|
||||
}
|
||||
|
||||
impl LayoutSingle for Packed<ScaleElem> {
|
||||
#[typst_macros::time(name = "scale", span = self.span())]
|
||||
fn layout(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Frame> {
|
||||
let sx = self.x(styles);
|
||||
let sy = self.y(styles);
|
||||
let align = self.origin(styles).resolve(styles);
|
||||
|
||||
// Compute the new region's approximate size.
|
||||
let size = regions
|
||||
.base()
|
||||
.zip_map(Axes::new(sx, sy), |r, s| s.of(r))
|
||||
.map(Abs::abs);
|
||||
|
||||
measure_and_layout(
|
||||
engine,
|
||||
regions.base(),
|
||||
size,
|
||||
styles,
|
||||
self.body(),
|
||||
Transform::scale(sx, sy),
|
||||
align,
|
||||
self.reflow(styles),
|
||||
)
|
||||
impl Show for Packed<ScaleElem> {
|
||||
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
|
||||
Ok(BlockElem::single_layouter(self.clone(), layout_scale).pack())
|
||||
}
|
||||
}
|
||||
|
||||
/// Layout the scaled content.
|
||||
#[typst_macros::time(span = elem.span())]
|
||||
fn layout_scale(
|
||||
elem: &Packed<ScaleElem>,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
region: Region,
|
||||
) -> SourceResult<Frame> {
|
||||
let sx = elem.x(styles);
|
||||
let sy = elem.y(styles);
|
||||
let align = elem.origin(styles).resolve(styles);
|
||||
|
||||
// Compute the new region's approximate size.
|
||||
let size = region.size.zip_map(Axes::new(sx, sy), |r, s| s.of(r)).map(Abs::abs);
|
||||
|
||||
measure_and_layout(
|
||||
engine,
|
||||
region,
|
||||
size,
|
||||
styles,
|
||||
elem.body(),
|
||||
Transform::scale(sx, sy),
|
||||
align,
|
||||
elem.reflow(styles),
|
||||
)
|
||||
}
|
||||
|
||||
/// A scale-skew-translate transformation.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Transform {
|
||||
@ -363,7 +379,7 @@ impl Default for Transform {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn measure_and_layout(
|
||||
engine: &mut Engine,
|
||||
base_size: Size,
|
||||
region: Region,
|
||||
size: Size,
|
||||
styles: StyleChain,
|
||||
body: &Content,
|
||||
@ -371,41 +387,41 @@ fn measure_and_layout(
|
||||
align: Axes<FixedAlignment>,
|
||||
reflow: bool,
|
||||
) -> SourceResult<Frame> {
|
||||
if !reflow {
|
||||
// Layout the body.
|
||||
let pod = Regions::one(base_size, Axes::splat(false));
|
||||
if reflow {
|
||||
// Measure the size of the body.
|
||||
let pod = Regions::one(size, Axes::splat(false));
|
||||
let frame = body.measure(engine, styles, pod)?.into_frame();
|
||||
|
||||
// Actually perform the layout.
|
||||
let pod = Regions::one(frame.size(), Axes::splat(true));
|
||||
let mut frame = body.layout(engine, styles, pod)?.into_frame();
|
||||
let Axes { x, y } = align.zip_map(frame.size(), FixedAlignment::position);
|
||||
|
||||
// Apply the transform.
|
||||
// Compute the transform.
|
||||
let ts = Transform::translate(x, y)
|
||||
.pre_concat(transform)
|
||||
.pre_concat(Transform::translate(-x, -y));
|
||||
|
||||
// Compute the bounding box and offset and wrap in a new frame.
|
||||
let (offset, size) = compute_bounding_box(&frame, ts);
|
||||
frame.transform(ts);
|
||||
frame.translate(offset);
|
||||
frame.set_size(size);
|
||||
Ok(frame)
|
||||
} else {
|
||||
// Layout the body.
|
||||
let mut frame = body.layout(engine, styles, region.into_regions())?.into_frame();
|
||||
let Axes { x, y } = align.zip_map(frame.size(), FixedAlignment::position);
|
||||
|
||||
return Ok(frame);
|
||||
// Compute the transform.
|
||||
let ts = Transform::translate(x, y)
|
||||
.pre_concat(transform)
|
||||
.pre_concat(Transform::translate(-x, -y));
|
||||
|
||||
// Apply the transform.
|
||||
frame.transform(ts);
|
||||
Ok(frame)
|
||||
}
|
||||
|
||||
// Measure the size of the body.
|
||||
let pod = Regions::one(size, Axes::splat(false));
|
||||
let frame = body.measure(engine, styles, pod)?.into_frame();
|
||||
|
||||
// Actually perform the layout.
|
||||
let pod = Regions::one(frame.size(), Axes::splat(true));
|
||||
let mut frame = body.layout(engine, styles, pod)?.into_frame();
|
||||
let Axes { x, y } = align.zip_map(frame.size(), FixedAlignment::position);
|
||||
|
||||
// Apply the transform.
|
||||
let ts = Transform::translate(x, y)
|
||||
.pre_concat(transform)
|
||||
.pre_concat(Transform::translate(-x, -y));
|
||||
|
||||
// Compute the bounding box and offset and wrap in a new frame.
|
||||
let (offset, size) = compute_bounding_box(&frame, ts);
|
||||
frame.transform(ts);
|
||||
frame.translate(offset);
|
||||
frame.set_size(size);
|
||||
Ok(frame)
|
||||
}
|
||||
|
||||
/// Computes the bounding box and offset of a transformed frame.
|
||||
|
@ -26,7 +26,7 @@
|
||||
//! [evaluate]: eval::eval
|
||||
//! [module]: foundations::Module
|
||||
//! [content]: foundations::Content
|
||||
//! [layouted]: layout::LayoutRoot
|
||||
//! [layouted]: foundations::Content::layout_document
|
||||
//! [document]: model::Document
|
||||
//! [frame]: layout::Frame
|
||||
|
||||
@ -70,7 +70,7 @@ use crate::foundations::{
|
||||
Array, Bytes, Content, Datetime, Dict, Module, Scope, StyleChain, Styles, Value,
|
||||
};
|
||||
use crate::introspection::{Introspector, Locator};
|
||||
use crate::layout::{Alignment, Dir, LayoutRoot};
|
||||
use crate::layout::{Alignment, Dir};
|
||||
use crate::model::Document;
|
||||
use crate::syntax::package::PackageSpec;
|
||||
use crate::syntax::{FileId, Source, Span};
|
||||
@ -139,7 +139,7 @@ fn typeset(
|
||||
};
|
||||
|
||||
// Layout!
|
||||
document = content.layout_root(&mut engine, styles)?;
|
||||
document = content.layout_document(&mut engine, styles)?;
|
||||
document.introspector.rebuild(&document.pages);
|
||||
iter += 1;
|
||||
|
||||
|
@ -12,7 +12,7 @@ use unicode_segmentation::UnicodeSegmentation;
|
||||
use crate::diag::SourceResult;
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{Content, Packed, StyleChain};
|
||||
use crate::layout::{Abs, Axes, BoxElem, Em, Frame, LayoutMultiple, Regions, Size};
|
||||
use crate::layout::{Abs, Axes, BoxElem, Em, Frame, Regions, Size};
|
||||
use crate::math::{
|
||||
scaled_font_size, styled_char, EquationElem, FrameFragment, GlyphFragment,
|
||||
LayoutMath, MathFragment, MathRun, MathSize, THICK,
|
||||
@ -65,7 +65,7 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> {
|
||||
pub fn new(
|
||||
engine: &'v mut Engine<'b>,
|
||||
styles: StyleChain<'a>,
|
||||
regions: Regions,
|
||||
base: Size,
|
||||
font: &'a Font,
|
||||
) -> Self {
|
||||
let math_table = font.ttf().tables().math.unwrap();
|
||||
@ -102,7 +102,7 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> {
|
||||
|
||||
Self {
|
||||
engine,
|
||||
regions: Regions::one(regions.base(), Axes::splat(false)),
|
||||
regions: Regions::one(base, Axes::splat(false)),
|
||||
font,
|
||||
ttf: font.ttf(),
|
||||
table: math_table,
|
||||
@ -173,7 +173,7 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> {
|
||||
) -> SourceResult<Frame> {
|
||||
let local =
|
||||
TextElem::set_size(TextSize(scaled_font_size(self, styles).into())).wrap();
|
||||
boxed.layout(self.engine, styles.chain(&local), self.regions)
|
||||
boxed.layout(self.engine, styles.chain(&local), self.regions.base())
|
||||
}
|
||||
|
||||
/// Layout the given [`Content`] into a [`Frame`].
|
||||
|
@ -5,13 +5,14 @@ use unicode_math_class::MathClass;
|
||||
use crate::diag::{bail, SourceResult};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{
|
||||
elem, Content, NativeElement, Packed, Resolve, ShowSet, Smart, StyleChain, Styles,
|
||||
Synthesize,
|
||||
elem, Content, NativeElement, Packed, Resolve, Show, ShowSet, Smart, StyleChain,
|
||||
Styles, Synthesize,
|
||||
};
|
||||
use crate::introspection::{Count, Counter, CounterUpdate, Locatable};
|
||||
use crate::layout::{
|
||||
Abs, AlignElem, Alignment, Axes, BlockElem, Em, FixedAlignment, Fragment, Frame,
|
||||
LayoutMultiple, OuterHAlignment, Point, Regions, Size, SpecificAlignment, VAlignment,
|
||||
InlineElem, InlineItem, OuterHAlignment, Point, Regions, Size, SpecificAlignment,
|
||||
VAlignment,
|
||||
};
|
||||
use crate::math::{
|
||||
scaled_font_size, LayoutMath, MathContext, MathRunFrameBuilder, MathSize, MathVariant,
|
||||
@ -48,14 +49,7 @@ use crate::World;
|
||||
/// horizontally. For more details about math syntax, see the
|
||||
/// [main math page]($category/math).
|
||||
#[elem(
|
||||
Locatable,
|
||||
Synthesize,
|
||||
ShowSet,
|
||||
LayoutMultiple,
|
||||
LayoutMath,
|
||||
Count,
|
||||
LocalName,
|
||||
Refable,
|
||||
Locatable, Synthesize, Show, ShowSet, LayoutMath, Count, LocalName, Refable,
|
||||
Outlinable
|
||||
)]
|
||||
pub struct EquationElem {
|
||||
@ -169,6 +163,16 @@ impl Synthesize for Packed<EquationElem> {
|
||||
}
|
||||
}
|
||||
|
||||
impl Show for Packed<EquationElem> {
|
||||
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
||||
if self.block(styles) {
|
||||
Ok(BlockElem::multi_layouter(self.clone(), layout_equation_block).pack())
|
||||
} else {
|
||||
Ok(InlineElem::layouter(self.clone(), layout_equation_inline).pack())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ShowSet for Packed<EquationElem> {
|
||||
fn show_set(&self, styles: StyleChain) -> Styles {
|
||||
let mut out = Styles::new();
|
||||
@ -187,178 +191,6 @@ impl ShowSet for Packed<EquationElem> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Layouted items suitable for placing in a paragraph.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum MathParItem {
|
||||
Space(Abs),
|
||||
Frame(Frame),
|
||||
}
|
||||
|
||||
impl Packed<EquationElem> {
|
||||
pub fn layout_inline(
|
||||
&self,
|
||||
engine: &mut Engine<'_>,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Vec<MathParItem>> {
|
||||
assert!(!self.block(styles));
|
||||
|
||||
let font = find_math_font(engine, styles, self.span())?;
|
||||
|
||||
let mut ctx = MathContext::new(engine, styles, regions, &font);
|
||||
let run = ctx.layout_into_run(self, styles)?;
|
||||
|
||||
let mut items = if run.row_count() == 1 {
|
||||
run.into_par_items()
|
||||
} else {
|
||||
vec![MathParItem::Frame(run.into_fragment(&ctx, styles).into_frame())]
|
||||
};
|
||||
|
||||
// An empty equation should have a height, so we still create a frame
|
||||
// (which is then resized in the loop).
|
||||
if items.is_empty() {
|
||||
items.push(MathParItem::Frame(Frame::soft(Size::zero())));
|
||||
}
|
||||
|
||||
for item in &mut items {
|
||||
let MathParItem::Frame(frame) = item else { continue };
|
||||
|
||||
let font_size = scaled_font_size(&ctx, styles);
|
||||
let slack = ParElem::leading_in(styles) * 0.7;
|
||||
let top_edge = TextElem::top_edge_in(styles).resolve(font_size, &font, None);
|
||||
let bottom_edge =
|
||||
-TextElem::bottom_edge_in(styles).resolve(font_size, &font, None);
|
||||
|
||||
let ascent = top_edge.max(frame.ascent() - slack);
|
||||
let descent = bottom_edge.max(frame.descent() - slack);
|
||||
frame.translate(Point::with_y(ascent - frame.baseline()));
|
||||
frame.size_mut().y = ascent + descent;
|
||||
}
|
||||
|
||||
Ok(items)
|
||||
}
|
||||
}
|
||||
|
||||
impl LayoutMultiple for Packed<EquationElem> {
|
||||
#[typst_macros::time(name = "math.equation", span = self.span())]
|
||||
fn layout(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment> {
|
||||
assert!(self.block(styles));
|
||||
|
||||
let span = self.span();
|
||||
let font = find_math_font(engine, styles, span)?;
|
||||
|
||||
let mut ctx = MathContext::new(engine, styles, regions, &font);
|
||||
let full_equation_builder = ctx
|
||||
.layout_into_run(self, styles)?
|
||||
.multiline_frame_builder(&ctx, styles);
|
||||
let width = full_equation_builder.size.x;
|
||||
|
||||
let equation_builders = if BlockElem::breakable_in(styles) {
|
||||
let mut rows = full_equation_builder.frames.into_iter().peekable();
|
||||
let mut equation_builders = vec![];
|
||||
let mut last_first_pos = Point::zero();
|
||||
|
||||
for region in regions.iter() {
|
||||
// Keep track of the position of the first row in this region,
|
||||
// so that the offset can be reverted later.
|
||||
let Some(&(_, first_pos)) = rows.peek() else { break };
|
||||
last_first_pos = first_pos;
|
||||
|
||||
let mut frames = vec![];
|
||||
let mut height = Abs::zero();
|
||||
while let Some((sub, pos)) = rows.peek() {
|
||||
let mut pos = *pos;
|
||||
pos.y -= first_pos.y;
|
||||
|
||||
// Finish this region if the line doesn't fit. Only do it if
|
||||
// we placed at least one line _or_ we still have non-last
|
||||
// regions. Crucially, we don't want to infinitely create
|
||||
// new regions which are too small.
|
||||
if !region.y.fits(sub.height() + pos.y)
|
||||
&& (!frames.is_empty() || !regions.in_last())
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
let (sub, _) = rows.next().unwrap();
|
||||
height = height.max(pos.y + sub.height());
|
||||
frames.push((sub, pos));
|
||||
}
|
||||
|
||||
equation_builders
|
||||
.push(MathRunFrameBuilder { frames, size: Size::new(width, height) });
|
||||
}
|
||||
|
||||
// Append remaining rows to the equation builder of the last region.
|
||||
if let Some(equation_builder) = equation_builders.last_mut() {
|
||||
equation_builder.frames.extend(rows.map(|(frame, mut pos)| {
|
||||
pos.y -= last_first_pos.y;
|
||||
(frame, pos)
|
||||
}));
|
||||
|
||||
let height = equation_builder
|
||||
.frames
|
||||
.iter()
|
||||
.map(|(frame, pos)| frame.height() + pos.y)
|
||||
.max()
|
||||
.unwrap_or(equation_builder.size.y);
|
||||
|
||||
equation_builder.size.y = height;
|
||||
}
|
||||
|
||||
equation_builders
|
||||
} else {
|
||||
vec![full_equation_builder]
|
||||
};
|
||||
|
||||
let Some(numbering) = (**self).numbering(styles) else {
|
||||
let frames = equation_builders
|
||||
.into_iter()
|
||||
.map(MathRunFrameBuilder::build)
|
||||
.collect();
|
||||
return Ok(Fragment::frames(frames));
|
||||
};
|
||||
|
||||
let pod = Regions::one(regions.base(), Axes::splat(false));
|
||||
let number = Counter::of(EquationElem::elem())
|
||||
.display_at_loc(engine, self.location().unwrap(), styles, numbering)?
|
||||
.spanned(span)
|
||||
.layout(engine, styles, pod)?
|
||||
.into_frame();
|
||||
|
||||
static NUMBER_GUTTER: Em = Em::new(0.5);
|
||||
let full_number_width = number.width() + NUMBER_GUTTER.resolve(styles);
|
||||
|
||||
let number_align = match self.number_align(styles) {
|
||||
SpecificAlignment::H(h) => SpecificAlignment::Both(h, VAlignment::Horizon),
|
||||
SpecificAlignment::V(v) => SpecificAlignment::Both(OuterHAlignment::End, v),
|
||||
SpecificAlignment::Both(h, v) => SpecificAlignment::Both(h, v),
|
||||
};
|
||||
|
||||
// Add equation numbers to each equation region.
|
||||
let frames = equation_builders
|
||||
.into_iter()
|
||||
.map(|builder| {
|
||||
add_equation_number(
|
||||
builder,
|
||||
number.clone(),
|
||||
number_align.resolve(styles),
|
||||
AlignElem::alignment_in(styles).resolve(styles).x,
|
||||
regions.size.x,
|
||||
full_number_width,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Fragment::frames(frames))
|
||||
}
|
||||
}
|
||||
|
||||
impl Count for Packed<EquationElem> {
|
||||
fn update(&self) -> Option<CounterUpdate> {
|
||||
(self.block(StyleChain::default()) && self.numbering().is_some())
|
||||
@ -429,6 +261,170 @@ impl LayoutMath for Packed<EquationElem> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Layout an inline equation (in a paragraph).
|
||||
#[typst_macros::time(span = elem.span())]
|
||||
fn layout_equation_inline(
|
||||
elem: &Packed<EquationElem>,
|
||||
engine: &mut Engine<'_>,
|
||||
styles: StyleChain,
|
||||
region: Size,
|
||||
) -> SourceResult<Vec<InlineItem>> {
|
||||
assert!(!elem.block(styles));
|
||||
|
||||
let font = find_math_font(engine, styles, elem.span())?;
|
||||
|
||||
let mut ctx = MathContext::new(engine, styles, region, &font);
|
||||
let run = ctx.layout_into_run(elem, styles)?;
|
||||
|
||||
let mut items = if run.row_count() == 1 {
|
||||
run.into_par_items()
|
||||
} else {
|
||||
vec![InlineItem::Frame(run.into_fragment(&ctx, styles).into_frame())]
|
||||
};
|
||||
|
||||
// An empty equation should have a height, so we still create a frame
|
||||
// (which is then resized in the loop).
|
||||
if items.is_empty() {
|
||||
items.push(InlineItem::Frame(Frame::soft(Size::zero())));
|
||||
}
|
||||
|
||||
for item in &mut items {
|
||||
let InlineItem::Frame(frame) = item else { continue };
|
||||
|
||||
let font_size = scaled_font_size(&ctx, styles);
|
||||
let slack = ParElem::leading_in(styles) * 0.7;
|
||||
let top_edge = TextElem::top_edge_in(styles).resolve(font_size, &font, None);
|
||||
let bottom_edge =
|
||||
-TextElem::bottom_edge_in(styles).resolve(font_size, &font, None);
|
||||
|
||||
let ascent = top_edge.max(frame.ascent() - slack);
|
||||
let descent = bottom_edge.max(frame.descent() - slack);
|
||||
frame.translate(Point::with_y(ascent - frame.baseline()));
|
||||
frame.size_mut().y = ascent + descent;
|
||||
}
|
||||
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
/// Layout a block-level equation (in a flow).
|
||||
#[typst_macros::time(span = elem.span())]
|
||||
fn layout_equation_block(
|
||||
elem: &Packed<EquationElem>,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment> {
|
||||
assert!(elem.block(styles));
|
||||
|
||||
let span = elem.span();
|
||||
let font = find_math_font(engine, styles, span)?;
|
||||
|
||||
let mut ctx = MathContext::new(engine, styles, regions.base(), &font);
|
||||
let full_equation_builder = ctx
|
||||
.layout_into_run(elem, styles)?
|
||||
.multiline_frame_builder(&ctx, styles);
|
||||
let width = full_equation_builder.size.x;
|
||||
|
||||
let equation_builders = if BlockElem::breakable_in(styles) {
|
||||
let mut rows = full_equation_builder.frames.into_iter().peekable();
|
||||
let mut equation_builders = vec![];
|
||||
let mut last_first_pos = Point::zero();
|
||||
|
||||
for region in regions.iter() {
|
||||
// Keep track of the position of the first row in this region,
|
||||
// so that the offset can be reverted later.
|
||||
let Some(&(_, first_pos)) = rows.peek() else { break };
|
||||
last_first_pos = first_pos;
|
||||
|
||||
let mut frames = vec![];
|
||||
let mut height = Abs::zero();
|
||||
while let Some((sub, pos)) = rows.peek() {
|
||||
let mut pos = *pos;
|
||||
pos.y -= first_pos.y;
|
||||
|
||||
// Finish this region if the line doesn't fit. Only do it if
|
||||
// we placed at least one line _or_ we still have non-last
|
||||
// regions. Crucially, we don't want to infinitely create
|
||||
// new regions which are too small.
|
||||
if !region.y.fits(sub.height() + pos.y)
|
||||
&& (!frames.is_empty() || !regions.in_last())
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
let (sub, _) = rows.next().unwrap();
|
||||
height = height.max(pos.y + sub.height());
|
||||
frames.push((sub, pos));
|
||||
}
|
||||
|
||||
equation_builders
|
||||
.push(MathRunFrameBuilder { frames, size: Size::new(width, height) });
|
||||
}
|
||||
|
||||
// Append remaining rows to the equation builder of the last region.
|
||||
if let Some(equation_builder) = equation_builders.last_mut() {
|
||||
equation_builder.frames.extend(rows.map(|(frame, mut pos)| {
|
||||
pos.y -= last_first_pos.y;
|
||||
(frame, pos)
|
||||
}));
|
||||
|
||||
let height = equation_builder
|
||||
.frames
|
||||
.iter()
|
||||
.map(|(frame, pos)| frame.height() + pos.y)
|
||||
.max()
|
||||
.unwrap_or(equation_builder.size.y);
|
||||
|
||||
equation_builder.size.y = height;
|
||||
}
|
||||
|
||||
equation_builders
|
||||
} else {
|
||||
vec![full_equation_builder]
|
||||
};
|
||||
|
||||
let Some(numbering) = (**elem).numbering(styles) else {
|
||||
let frames = equation_builders
|
||||
.into_iter()
|
||||
.map(MathRunFrameBuilder::build)
|
||||
.collect();
|
||||
return Ok(Fragment::frames(frames));
|
||||
};
|
||||
|
||||
let pod = Regions::one(regions.base(), Axes::splat(false));
|
||||
let number = Counter::of(EquationElem::elem())
|
||||
.display_at_loc(engine, elem.location().unwrap(), styles, numbering)?
|
||||
.spanned(span)
|
||||
.layout(engine, styles, pod)?
|
||||
.into_frame();
|
||||
|
||||
static NUMBER_GUTTER: Em = Em::new(0.5);
|
||||
let full_number_width = number.width() + NUMBER_GUTTER.resolve(styles);
|
||||
|
||||
let number_align = match elem.number_align(styles) {
|
||||
SpecificAlignment::H(h) => SpecificAlignment::Both(h, VAlignment::Horizon),
|
||||
SpecificAlignment::V(v) => SpecificAlignment::Both(OuterHAlignment::End, v),
|
||||
SpecificAlignment::Both(h, v) => SpecificAlignment::Both(h, v),
|
||||
};
|
||||
|
||||
// Add equation numbers to each equation region.
|
||||
let frames = equation_builders
|
||||
.into_iter()
|
||||
.map(|builder| {
|
||||
add_equation_number(
|
||||
builder,
|
||||
number.clone(),
|
||||
number_align.resolve(styles),
|
||||
AlignElem::alignment_in(styles).resolve(styles).x,
|
||||
regions.size.x,
|
||||
full_number_width,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Fragment::frames(frames))
|
||||
}
|
||||
|
||||
fn find_math_font(
|
||||
engine: &mut Engine<'_>,
|
||||
styles: StyleChain,
|
||||
|
@ -3,10 +3,10 @@ use std::iter::once;
|
||||
use unicode_math_class::MathClass;
|
||||
|
||||
use crate::foundations::{Resolve, StyleChain};
|
||||
use crate::layout::{Abs, AlignElem, Em, Frame, Point, Size};
|
||||
use crate::layout::{Abs, AlignElem, Em, Frame, InlineItem, Point, Size};
|
||||
use crate::math::{
|
||||
alignments, scaled_font_size, spacing, EquationElem, FrameFragment, MathContext,
|
||||
MathFragment, MathParItem, MathSize,
|
||||
MathFragment, MathSize,
|
||||
};
|
||||
use crate::model::ParElem;
|
||||
|
||||
@ -251,7 +251,7 @@ impl MathRun {
|
||||
frame
|
||||
}
|
||||
|
||||
pub fn into_par_items(self) -> Vec<MathParItem> {
|
||||
pub fn into_par_items(self) -> Vec<InlineItem> {
|
||||
let mut items = vec![];
|
||||
|
||||
let mut x = Abs::zero();
|
||||
@ -279,7 +279,7 @@ impl MathRun {
|
||||
match fragment {
|
||||
MathFragment::Space(width)
|
||||
| MathFragment::Spacing(SpacingFragment { width, .. }) => {
|
||||
items.push(MathParItem::Space(width));
|
||||
items.push(InlineItem::Space(width, true));
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
@ -305,7 +305,7 @@ impl MathRun {
|
||||
std::mem::replace(&mut frame, Frame::soft(Size::zero()));
|
||||
|
||||
finalize_frame(&mut frame_prev, x, ascent, descent);
|
||||
items.push(MathParItem::Frame(frame_prev));
|
||||
items.push(InlineItem::Frame(frame_prev));
|
||||
empty = true;
|
||||
|
||||
x = Abs::zero();
|
||||
@ -315,7 +315,7 @@ impl MathRun {
|
||||
space_is_visible = true;
|
||||
if let Some(f_next) = iter.peek() {
|
||||
if !is_space(f_next) {
|
||||
items.push(MathParItem::Space(Abs::zero()));
|
||||
items.push(InlineItem::Space(Abs::zero(), true));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -327,7 +327,7 @@ impl MathRun {
|
||||
// contribute width (if it had hidden content).
|
||||
if !empty {
|
||||
finalize_frame(&mut frame, x, ascent, descent);
|
||||
items.push(MathParItem::Frame(frame));
|
||||
items.push(InlineItem::Frame(frame));
|
||||
}
|
||||
|
||||
items
|
||||
|
@ -29,8 +29,8 @@ use crate::foundations::{
|
||||
};
|
||||
use crate::introspection::{Introspector, Locatable, Location};
|
||||
use crate::layout::{
|
||||
BlockElem, Em, GridCell, GridChild, GridElem, GridItem, HElem, PadElem, Sizing,
|
||||
TrackSizings, VElem,
|
||||
BlockChild, BlockElem, Em, GridCell, GridChild, GridElem, GridItem, HElem, PadElem,
|
||||
Sizing, TrackSizings, VElem,
|
||||
};
|
||||
use crate::model::{
|
||||
CitationForm, CiteGroup, Destination, FootnoteElem, HeadingElem, LinkElem, ParElem,
|
||||
@ -926,8 +926,10 @@ impl ElemRenderer<'_> {
|
||||
|
||||
match elem.display {
|
||||
Some(Display::Block) => {
|
||||
content =
|
||||
BlockElem::new().with_body(Some(content)).pack().spanned(self.span);
|
||||
content = BlockElem::new()
|
||||
.with_body(Some(BlockChild::Content(content)))
|
||||
.pack()
|
||||
.spanned(self.span);
|
||||
}
|
||||
Some(Display::Indent) => {
|
||||
content = PadElem::new(content).pack().spanned(self.span);
|
||||
|
@ -7,7 +7,7 @@ use crate::foundations::{
|
||||
StyledElem, Value,
|
||||
};
|
||||
use crate::introspection::{Introspector, ManualPageCounter};
|
||||
use crate::layout::{LayoutRoot, Page, PageElem};
|
||||
use crate::layout::{Page, PageElem};
|
||||
|
||||
/// The root element of a document and its metadata.
|
||||
///
|
||||
@ -25,7 +25,7 @@ use crate::layout::{LayoutRoot, Page, PageElem};
|
||||
///
|
||||
/// Note that metadata set with this function is not rendered within the
|
||||
/// document. Instead, it is embedded in the compiled PDF file.
|
||||
#[elem(Construct, LayoutRoot)]
|
||||
#[elem(Construct)]
|
||||
pub struct DocumentElem {
|
||||
/// The document's title. This is often rendered as the title of the
|
||||
/// PDF viewer window.
|
||||
@ -69,9 +69,10 @@ impl Construct for DocumentElem {
|
||||
}
|
||||
}
|
||||
|
||||
impl LayoutRoot for Packed<DocumentElem> {
|
||||
impl Packed<DocumentElem> {
|
||||
/// Layout this document.
|
||||
#[typst_macros::time(name = "document", span = self.span())]
|
||||
fn layout_root(
|
||||
pub fn layout(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
|
@ -6,11 +6,12 @@ use smallvec::{smallvec, SmallVec};
|
||||
use crate::diag::{bail, SourceResult};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{
|
||||
cast, elem, scope, Array, Content, Context, Packed, Smart, StyleChain,
|
||||
cast, elem, scope, Array, Content, Context, NativeElement, Packed, Show, Smart,
|
||||
StyleChain,
|
||||
};
|
||||
use crate::layout::{
|
||||
Alignment, Axes, BlockElem, Cell, CellGrid, Em, Fragment, GridLayouter, HAlignment,
|
||||
LayoutMultiple, Length, Regions, Sizing, Spacing, VAlignment,
|
||||
Length, Regions, Sizing, Spacing, VAlignment, VElem,
|
||||
};
|
||||
use crate::model::{Numbering, NumberingPattern, ParElem};
|
||||
use crate::text::TextElem;
|
||||
@ -71,7 +72,7 @@ use crate::text::TextElem;
|
||||
/// Enumeration items can contain multiple paragraphs and other block-level
|
||||
/// content. All content that is indented more than an item's marker becomes
|
||||
/// part of that item.
|
||||
#[elem(scope, title = "Numbered List", LayoutMultiple)]
|
||||
#[elem(scope, title = "Numbered List", Show)]
|
||||
pub struct EnumElem {
|
||||
/// If this is `{false}`, the items are spaced apart with
|
||||
/// [enum spacing]($enum.spacing). If it is `{true}`, they use normal
|
||||
@ -212,85 +213,97 @@ impl EnumElem {
|
||||
type EnumItem;
|
||||
}
|
||||
|
||||
impl LayoutMultiple for Packed<EnumElem> {
|
||||
#[typst_macros::time(name = "enum", span = self.span())]
|
||||
fn layout(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment> {
|
||||
let numbering = self.numbering(styles);
|
||||
let indent = self.indent(styles);
|
||||
let body_indent = self.body_indent(styles);
|
||||
let gutter = if self.tight(styles) {
|
||||
ParElem::leading_in(styles).into()
|
||||
} else {
|
||||
self.spacing(styles)
|
||||
.unwrap_or_else(|| *BlockElem::below_in(styles).amount())
|
||||
};
|
||||
impl Show for Packed<EnumElem> {
|
||||
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
||||
let mut realized = BlockElem::multi_layouter(self.clone(), layout_enum).pack();
|
||||
|
||||
let mut cells = vec![];
|
||||
let mut number = self.start(styles);
|
||||
let mut parents = EnumElem::parents_in(styles);
|
||||
|
||||
let full = self.full(styles);
|
||||
|
||||
// Horizontally align based on the given respective parameter.
|
||||
// Vertically align to the top to avoid inheriting `horizon` or `bottom`
|
||||
// alignment from the context and having the number be displaced in
|
||||
// relation to the item it refers to.
|
||||
let number_align = self.number_align(styles);
|
||||
|
||||
for item in self.children() {
|
||||
number = item.number(styles).unwrap_or(number);
|
||||
|
||||
let context = Context::new(None, Some(styles));
|
||||
let resolved = if full {
|
||||
parents.push(number);
|
||||
let content =
|
||||
numbering.apply(engine, context.track(), &parents)?.display();
|
||||
parents.pop();
|
||||
content
|
||||
} else {
|
||||
match numbering {
|
||||
Numbering::Pattern(pattern) => {
|
||||
TextElem::packed(pattern.apply_kth(parents.len(), number))
|
||||
}
|
||||
other => other.apply(engine, context.track(), &[number])?.display(),
|
||||
}
|
||||
};
|
||||
|
||||
// Disable overhang as a workaround to end-aligned dots glitching
|
||||
// and decreasing spacing between numbers and items.
|
||||
let resolved =
|
||||
resolved.aligned(number_align).styled(TextElem::set_overhang(false));
|
||||
|
||||
cells.push(Cell::from(Content::empty()));
|
||||
cells.push(Cell::from(resolved));
|
||||
cells.push(Cell::from(Content::empty()));
|
||||
cells.push(Cell::from(
|
||||
item.body().clone().styled(EnumElem::set_parents(smallvec![number])),
|
||||
));
|
||||
number = number.saturating_add(1);
|
||||
if self.tight(styles) {
|
||||
let leading = ParElem::leading_in(styles);
|
||||
let spacing = VElem::list_attach(leading.into()).pack();
|
||||
realized = spacing + realized;
|
||||
}
|
||||
|
||||
let grid = CellGrid::new(
|
||||
Axes::with_x(&[
|
||||
Sizing::Rel(indent.into()),
|
||||
Sizing::Auto,
|
||||
Sizing::Rel(body_indent.into()),
|
||||
Sizing::Auto,
|
||||
]),
|
||||
Axes::with_y(&[gutter.into()]),
|
||||
cells,
|
||||
);
|
||||
let layouter = GridLayouter::new(&grid, regions, styles, self.span());
|
||||
|
||||
layouter.layout(engine)
|
||||
Ok(realized)
|
||||
}
|
||||
}
|
||||
|
||||
/// Layout the enumeration.
|
||||
#[typst_macros::time(span = elem.span())]
|
||||
fn layout_enum(
|
||||
elem: &Packed<EnumElem>,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment> {
|
||||
let numbering = elem.numbering(styles);
|
||||
let indent = elem.indent(styles);
|
||||
let body_indent = elem.body_indent(styles);
|
||||
let gutter = if elem.tight(styles) {
|
||||
ParElem::leading_in(styles).into()
|
||||
} else {
|
||||
elem.spacing(styles)
|
||||
.unwrap_or_else(|| *BlockElem::below_in(styles).amount())
|
||||
};
|
||||
|
||||
let mut cells = vec![];
|
||||
let mut number = elem.start(styles);
|
||||
let mut parents = EnumElem::parents_in(styles);
|
||||
|
||||
let full = elem.full(styles);
|
||||
|
||||
// Horizontally align based on the given respective parameter.
|
||||
// Vertically align to the top to avoid inheriting `horizon` or `bottom`
|
||||
// alignment from the context and having the number be displaced in
|
||||
// relation to the item it refers to.
|
||||
let number_align = elem.number_align(styles);
|
||||
|
||||
for item in elem.children() {
|
||||
number = item.number(styles).unwrap_or(number);
|
||||
|
||||
let context = Context::new(None, Some(styles));
|
||||
let resolved = if full {
|
||||
parents.push(number);
|
||||
let content = numbering.apply(engine, context.track(), &parents)?.display();
|
||||
parents.pop();
|
||||
content
|
||||
} else {
|
||||
match numbering {
|
||||
Numbering::Pattern(pattern) => {
|
||||
TextElem::packed(pattern.apply_kth(parents.len(), number))
|
||||
}
|
||||
other => other.apply(engine, context.track(), &[number])?.display(),
|
||||
}
|
||||
};
|
||||
|
||||
// Disable overhang as a workaround to end-aligned dots glitching
|
||||
// and decreasing spacing between numbers and items.
|
||||
let resolved =
|
||||
resolved.aligned(number_align).styled(TextElem::set_overhang(false));
|
||||
|
||||
cells.push(Cell::from(Content::empty()));
|
||||
cells.push(Cell::from(resolved));
|
||||
cells.push(Cell::from(Content::empty()));
|
||||
cells.push(Cell::from(
|
||||
item.body().clone().styled(EnumElem::set_parents(smallvec![number])),
|
||||
));
|
||||
number = number.saturating_add(1);
|
||||
}
|
||||
|
||||
let grid = CellGrid::new(
|
||||
Axes::with_x(&[
|
||||
Sizing::Rel(indent.into()),
|
||||
Sizing::Auto,
|
||||
Sizing::Rel(body_indent.into()),
|
||||
Sizing::Auto,
|
||||
]),
|
||||
Axes::with_y(&[gutter.into()]),
|
||||
cells,
|
||||
);
|
||||
let layouter = GridLayouter::new(&grid, regions, styles, elem.span());
|
||||
|
||||
layouter.layout(engine)
|
||||
}
|
||||
|
||||
/// An enumeration item.
|
||||
#[elem(name = "item", title = "Numbered List Item")]
|
||||
pub struct EnumItem {
|
||||
|
@ -14,8 +14,8 @@ use crate::introspection::{
|
||||
Count, Counter, CounterKey, CounterUpdate, Locatable, Location,
|
||||
};
|
||||
use crate::layout::{
|
||||
AlignElem, Alignment, BlockElem, Em, HAlignment, Length, OuterVAlignment, PlaceElem,
|
||||
VAlignment, VElem,
|
||||
AlignElem, Alignment, BlockChild, BlockElem, Em, HAlignment, Length, OuterVAlignment,
|
||||
PlaceElem, VAlignment, VElem,
|
||||
};
|
||||
use crate::model::{Numbering, NumberingPattern, Outlinable, Refable, Supplement};
|
||||
use crate::text::{Lang, Region, TextElem};
|
||||
@ -317,7 +317,10 @@ impl Show for Packed<FigureElem> {
|
||||
}
|
||||
|
||||
// Wrap the contents in a block.
|
||||
realized = BlockElem::new().with_body(Some(realized)).pack().spanned(self.span());
|
||||
realized = BlockElem::new()
|
||||
.with_body(Some(BlockChild::Content(realized)))
|
||||
.pack()
|
||||
.spanned(self.span());
|
||||
|
||||
// Wrap in a float.
|
||||
if let Some(align) = self.placement(styles) {
|
||||
|
@ -8,7 +8,7 @@ use crate::foundations::{
|
||||
};
|
||||
use crate::introspection::{Count, Counter, CounterUpdate, Locatable};
|
||||
use crate::layout::{
|
||||
Abs, Axes, BlockElem, Em, HElem, LayoutMultiple, Length, Regions, VElem,
|
||||
Abs, Axes, BlockChild, BlockElem, Em, HElem, Length, Regions, VElem,
|
||||
};
|
||||
use crate::model::{Numbering, Outlinable, ParElem, Refable, Supplement};
|
||||
use crate::text::{FontWeight, LocalName, SpaceElem, TextElem, TextSize};
|
||||
@ -248,7 +248,10 @@ impl Show for Packed<HeadingElem> {
|
||||
realized = realized.styled(ParElem::set_hanging_indent(indent.into()));
|
||||
}
|
||||
|
||||
Ok(BlockElem::new().with_body(Some(realized)).pack().spanned(span))
|
||||
Ok(BlockElem::new()
|
||||
.with_body(Some(BlockChild::Content(realized)))
|
||||
.pack()
|
||||
.spanned(span))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,12 +3,12 @@ use comemo::Track;
|
||||
use crate::diag::{bail, SourceResult};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{
|
||||
cast, elem, scope, Array, Content, Context, Depth, Func, Packed, Smart, StyleChain,
|
||||
Value,
|
||||
cast, elem, scope, Array, Content, Context, Depth, Func, NativeElement, Packed, Show,
|
||||
Smart, StyleChain, Value,
|
||||
};
|
||||
use crate::layout::{
|
||||
Axes, BlockElem, Cell, CellGrid, Em, Fragment, GridLayouter, HAlignment,
|
||||
LayoutMultiple, Length, Regions, Sizing, Spacing, VAlignment,
|
||||
Axes, BlockElem, Cell, CellGrid, Em, Fragment, GridLayouter, HAlignment, Length,
|
||||
Regions, Sizing, Spacing, VAlignment, VElem,
|
||||
};
|
||||
use crate::model::ParElem;
|
||||
use crate::text::TextElem;
|
||||
@ -44,7 +44,7 @@ use crate::text::TextElem;
|
||||
/// followed by a space to create a list item. A list item can contain multiple
|
||||
/// paragraphs and other block-level content. All content that is indented
|
||||
/// more than an item's marker becomes part of that item.
|
||||
#[elem(scope, title = "Bullet List", LayoutMultiple)]
|
||||
#[elem(scope, title = "Bullet List", Show)]
|
||||
pub struct ListElem {
|
||||
/// If this is `{false}`, the items are spaced apart with
|
||||
/// [list spacing]($list.spacing). If it is `{true}`, they use normal
|
||||
@ -137,56 +137,67 @@ impl ListElem {
|
||||
type ListItem;
|
||||
}
|
||||
|
||||
impl LayoutMultiple for Packed<ListElem> {
|
||||
#[typst_macros::time(name = "list", span = self.span())]
|
||||
fn layout(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment> {
|
||||
let indent = self.indent(styles);
|
||||
let body_indent = self.body_indent(styles);
|
||||
let gutter = if self.tight(styles) {
|
||||
ParElem::leading_in(styles).into()
|
||||
} else {
|
||||
self.spacing(styles)
|
||||
.unwrap_or_else(|| *BlockElem::below_in(styles).amount())
|
||||
};
|
||||
impl Show for Packed<ListElem> {
|
||||
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
||||
let mut realized = BlockElem::multi_layouter(self.clone(), layout_list).pack();
|
||||
|
||||
let Depth(depth) = ListElem::depth_in(styles);
|
||||
let marker = self
|
||||
.marker(styles)
|
||||
.resolve(engine, styles, depth)?
|
||||
// avoid '#set align' interference with the list
|
||||
.aligned(HAlignment::Start + VAlignment::Top);
|
||||
|
||||
let mut cells = vec![];
|
||||
for item in self.children() {
|
||||
cells.push(Cell::from(Content::empty()));
|
||||
cells.push(Cell::from(marker.clone()));
|
||||
cells.push(Cell::from(Content::empty()));
|
||||
cells.push(Cell::from(
|
||||
item.body().clone().styled(ListElem::set_depth(Depth(1))),
|
||||
));
|
||||
if self.tight(styles) {
|
||||
let leading = ParElem::leading_in(styles);
|
||||
let spacing = VElem::list_attach(leading.into()).pack();
|
||||
realized = spacing + realized;
|
||||
}
|
||||
|
||||
let grid = CellGrid::new(
|
||||
Axes::with_x(&[
|
||||
Sizing::Rel(indent.into()),
|
||||
Sizing::Auto,
|
||||
Sizing::Rel(body_indent.into()),
|
||||
Sizing::Auto,
|
||||
]),
|
||||
Axes::with_y(&[gutter.into()]),
|
||||
cells,
|
||||
);
|
||||
let layouter = GridLayouter::new(&grid, regions, styles, self.span());
|
||||
|
||||
layouter.layout(engine)
|
||||
Ok(realized)
|
||||
}
|
||||
}
|
||||
|
||||
/// Layout the list.
|
||||
#[typst_macros::time(span = elem.span())]
|
||||
fn layout_list(
|
||||
elem: &Packed<ListElem>,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment> {
|
||||
let indent = elem.indent(styles);
|
||||
let body_indent = elem.body_indent(styles);
|
||||
let gutter = if elem.tight(styles) {
|
||||
ParElem::leading_in(styles).into()
|
||||
} else {
|
||||
elem.spacing(styles)
|
||||
.unwrap_or_else(|| *BlockElem::below_in(styles).amount())
|
||||
};
|
||||
|
||||
let Depth(depth) = ListElem::depth_in(styles);
|
||||
let marker = elem
|
||||
.marker(styles)
|
||||
.resolve(engine, styles, depth)?
|
||||
// avoid '#set align' interference with the list
|
||||
.aligned(HAlignment::Start + VAlignment::Top);
|
||||
|
||||
let mut cells = vec![];
|
||||
for item in elem.children() {
|
||||
cells.push(Cell::from(Content::empty()));
|
||||
cells.push(Cell::from(marker.clone()));
|
||||
cells.push(Cell::from(Content::empty()));
|
||||
cells.push(Cell::from(item.body().clone().styled(ListElem::set_depth(Depth(1)))));
|
||||
}
|
||||
|
||||
let grid = CellGrid::new(
|
||||
Axes::with_x(&[
|
||||
Sizing::Rel(indent.into()),
|
||||
Sizing::Auto,
|
||||
Sizing::Rel(body_indent.into()),
|
||||
Sizing::Auto,
|
||||
]),
|
||||
Axes::with_y(&[gutter.into()]),
|
||||
cells,
|
||||
);
|
||||
let layouter = GridLayouter::new(&grid, regions, styles, elem.span());
|
||||
|
||||
layouter.layout(engine)
|
||||
}
|
||||
|
||||
/// A bullet list item.
|
||||
#[elem(name = "item", title = "Bullet List Item")]
|
||||
pub struct ListItem {
|
||||
|
@ -4,7 +4,9 @@ use crate::foundations::{
|
||||
cast, elem, Content, Depth, Label, NativeElement, Packed, Show, ShowSet, Smart,
|
||||
StyleChain, Styles,
|
||||
};
|
||||
use crate::layout::{Alignment, BlockElem, Em, HElem, PadElem, Spacing, VElem};
|
||||
use crate::layout::{
|
||||
Alignment, BlockChild, BlockElem, Em, HElem, PadElem, Spacing, VElem,
|
||||
};
|
||||
use crate::model::{CitationForm, CiteElem};
|
||||
use crate::text::{SmartQuoteElem, SmartQuotes, SpaceElem, TextElem};
|
||||
|
||||
@ -181,8 +183,10 @@ impl Show for Packed<QuoteElem> {
|
||||
}
|
||||
|
||||
if block {
|
||||
realized =
|
||||
BlockElem::new().with_body(Some(realized)).pack().spanned(self.span());
|
||||
realized = BlockElem::new()
|
||||
.with_body(Some(BlockChild::Content(realized)))
|
||||
.pack()
|
||||
.spanned(self.span());
|
||||
|
||||
if let Some(attribution) = self.attribution(styles).as_ref() {
|
||||
let mut seq = vec![TextElem::packed('—'), SpaceElem::new().pack()];
|
||||
|
@ -6,11 +6,11 @@ use ecow::{eco_format, EcoString};
|
||||
use crate::diag::{bail, SourceResult, StrResult, Trace, Tracepoint};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{
|
||||
cast, elem, scope, Content, Fold, Packed, Show, Smart, StyleChain,
|
||||
cast, elem, scope, Content, Fold, NativeElement, Packed, Show, Smart, StyleChain,
|
||||
};
|
||||
use crate::layout::{
|
||||
show_grid_cell, Abs, Alignment, Axes, Cell, CellGrid, Celled, Dir, Fragment,
|
||||
GridCell, GridFooter, GridHLine, GridHeader, GridLayouter, GridVLine, LayoutMultiple,
|
||||
show_grid_cell, Abs, Alignment, Axes, BlockElem, Cell, CellGrid, Celled, Dir,
|
||||
Fragment, GridCell, GridFooter, GridHLine, GridHeader, GridLayouter, GridVLine,
|
||||
Length, LinePosition, OuterHAlignment, OuterVAlignment, Regions, Rel, ResolvableCell,
|
||||
ResolvableGridChild, ResolvableGridItem, Sides, TrackSizings,
|
||||
};
|
||||
@ -120,7 +120,7 @@ use crate::visualize::{Paint, Stroke};
|
||||
/// [Robert], b, a, b,
|
||||
/// )
|
||||
/// ```
|
||||
#[elem(scope, LayoutMultiple, LocalName, Figurable)]
|
||||
#[elem(scope, Show, LocalName, Figurable)]
|
||||
pub struct TableElem {
|
||||
/// The column sizes. See the [grid documentation]($grid) for more
|
||||
/// information on track sizing.
|
||||
@ -260,62 +260,65 @@ impl TableElem {
|
||||
type TableFooter;
|
||||
}
|
||||
|
||||
impl LayoutMultiple for Packed<TableElem> {
|
||||
#[typst_macros::time(name = "table", span = self.span())]
|
||||
fn layout(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment> {
|
||||
let inset = self.inset(styles);
|
||||
let align = self.align(styles);
|
||||
let columns = self.columns(styles);
|
||||
let rows = self.rows(styles);
|
||||
let column_gutter = self.column_gutter(styles);
|
||||
let row_gutter = self.row_gutter(styles);
|
||||
let fill = self.fill(styles);
|
||||
let stroke = self.stroke(styles);
|
||||
|
||||
let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice());
|
||||
let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice());
|
||||
// Use trace to link back to the table when a specific cell errors
|
||||
let tracepoint = || Tracepoint::Call(Some(eco_format!("table")));
|
||||
let resolve_item = |item: &TableItem| item.to_resolvable(styles);
|
||||
let children = self.children().iter().map(|child| match child {
|
||||
TableChild::Header(header) => ResolvableGridChild::Header {
|
||||
repeat: header.repeat(styles),
|
||||
span: header.span(),
|
||||
items: header.children().iter().map(resolve_item),
|
||||
},
|
||||
TableChild::Footer(footer) => ResolvableGridChild::Footer {
|
||||
repeat: footer.repeat(styles),
|
||||
span: footer.span(),
|
||||
items: footer.children().iter().map(resolve_item),
|
||||
},
|
||||
TableChild::Item(item) => {
|
||||
ResolvableGridChild::Item(item.to_resolvable(styles))
|
||||
}
|
||||
});
|
||||
let grid = CellGrid::resolve(
|
||||
tracks,
|
||||
gutter,
|
||||
children,
|
||||
fill,
|
||||
align,
|
||||
&inset,
|
||||
&stroke,
|
||||
engine,
|
||||
styles,
|
||||
self.span(),
|
||||
)
|
||||
.trace(engine.world, tracepoint, self.span())?;
|
||||
|
||||
let layouter = GridLayouter::new(&grid, regions, styles, self.span());
|
||||
layouter.layout(engine)
|
||||
impl Show for Packed<TableElem> {
|
||||
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
|
||||
Ok(BlockElem::multi_layouter(self.clone(), layout_table).pack())
|
||||
}
|
||||
}
|
||||
|
||||
/// Layout the table.
|
||||
#[typst_macros::time(span = elem.span())]
|
||||
fn layout_table(
|
||||
elem: &Packed<TableElem>,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment> {
|
||||
let inset = elem.inset(styles);
|
||||
let align = elem.align(styles);
|
||||
let columns = elem.columns(styles);
|
||||
let rows = elem.rows(styles);
|
||||
let column_gutter = elem.column_gutter(styles);
|
||||
let row_gutter = elem.row_gutter(styles);
|
||||
let fill = elem.fill(styles);
|
||||
let stroke = elem.stroke(styles);
|
||||
|
||||
let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice());
|
||||
let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice());
|
||||
// Use trace to link back to the table when a specific cell errors
|
||||
let tracepoint = || Tracepoint::Call(Some(eco_format!("table")));
|
||||
let resolve_item = |item: &TableItem| item.to_resolvable(styles);
|
||||
let children = elem.children().iter().map(|child| match child {
|
||||
TableChild::Header(header) => ResolvableGridChild::Header {
|
||||
repeat: header.repeat(styles),
|
||||
span: header.span(),
|
||||
items: header.children().iter().map(resolve_item),
|
||||
},
|
||||
TableChild::Footer(footer) => ResolvableGridChild::Footer {
|
||||
repeat: footer.repeat(styles),
|
||||
span: footer.span(),
|
||||
items: footer.children().iter().map(resolve_item),
|
||||
},
|
||||
TableChild::Item(item) => ResolvableGridChild::Item(item.to_resolvable(styles)),
|
||||
});
|
||||
let grid = CellGrid::resolve(
|
||||
tracks,
|
||||
gutter,
|
||||
children,
|
||||
fill,
|
||||
align,
|
||||
&inset,
|
||||
&stroke,
|
||||
engine,
|
||||
styles,
|
||||
elem.span(),
|
||||
)
|
||||
.trace(engine.world, tracepoint, elem.span())?;
|
||||
|
||||
let layouter = GridLayouter::new(&grid, regions, styles, elem.span());
|
||||
layouter.layout(engine)
|
||||
}
|
||||
|
||||
impl LocalName for Packed<TableElem> {
|
||||
const KEY: &'static str = "table";
|
||||
}
|
||||
|
@ -1,11 +1,10 @@
|
||||
use crate::diag::{bail, SourceResult};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{
|
||||
cast, elem, scope, Array, Content, NativeElement, Packed, Smart, StyleChain,
|
||||
cast, elem, scope, Array, Content, NativeElement, Packed, Show, Smart, StyleChain,
|
||||
};
|
||||
use crate::layout::{
|
||||
BlockElem, Dir, Em, Fragment, HElem, LayoutMultiple, Length, Regions, Sides, Spacing,
|
||||
StackChild, StackElem,
|
||||
BlockElem, Dir, Em, HElem, Length, Sides, Spacing, StackChild, StackElem, VElem,
|
||||
};
|
||||
use crate::model::ParElem;
|
||||
use crate::text::TextElem;
|
||||
@ -27,7 +26,7 @@ use crate::utils::Numeric;
|
||||
/// # Syntax
|
||||
/// This function also has dedicated syntax: Starting a line with a slash,
|
||||
/// followed by a term, a colon and a description creates a term list item.
|
||||
#[elem(scope, title = "Term List", LayoutMultiple)]
|
||||
#[elem(scope, title = "Term List", Show)]
|
||||
pub struct TermsElem {
|
||||
/// If this is `{false}`, the items are spaced apart with
|
||||
/// [term list spacing]($terms.spacing). If it is `{true}`, they use normal
|
||||
@ -109,14 +108,8 @@ impl TermsElem {
|
||||
type TermItem;
|
||||
}
|
||||
|
||||
impl LayoutMultiple for Packed<TermsElem> {
|
||||
#[typst_macros::time(name = "terms", span = self.span())]
|
||||
fn layout(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment> {
|
||||
impl Show for Packed<TermsElem> {
|
||||
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
||||
let separator = self.separator(styles);
|
||||
let indent = self.indent(styles);
|
||||
let hanging_indent = self.hanging_indent(styles);
|
||||
@ -148,11 +141,18 @@ impl LayoutMultiple for Packed<TermsElem> {
|
||||
padding.right = pad.into();
|
||||
}
|
||||
|
||||
StackElem::new(children)
|
||||
let mut realized = StackElem::new(children)
|
||||
.with_spacing(Some(gutter))
|
||||
.pack()
|
||||
.padded(padding)
|
||||
.layout(engine, styles, regions)
|
||||
.padded(padding);
|
||||
|
||||
if self.tight(styles) {
|
||||
let leading = ParElem::leading_in(styles);
|
||||
let spacing = VElem::list_attach(leading.into()).pack();
|
||||
realized = spacing + realized;
|
||||
}
|
||||
|
||||
Ok(realized)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -23,8 +23,8 @@ use crate::foundations::{
|
||||
};
|
||||
use crate::introspection::TagElem;
|
||||
use crate::layout::{
|
||||
AlignElem, BlockElem, BoxElem, ColbreakElem, FlowElem, FlushElem, HElem,
|
||||
LayoutMultiple, LayoutSingle, PageElem, PagebreakElem, Parity, PlaceElem, VElem,
|
||||
AlignElem, BlockElem, BoxElem, ColbreakElem, FlowElem, FlushElem, HElem, InlineElem,
|
||||
PageElem, PagebreakElem, Parity, PlaceElem, VElem,
|
||||
};
|
||||
use crate::math::{EquationElem, LayoutMath};
|
||||
use crate::model::{
|
||||
@ -377,8 +377,14 @@ impl<'a> FlowBuilder<'a> {
|
||||
let last_was_parbreak = self.1;
|
||||
self.1 = false;
|
||||
|
||||
if content.is::<VElem>()
|
||||
|| content.is::<ColbreakElem>()
|
||||
if let Some(elem) = content.to_packed::<VElem>() {
|
||||
if !elem.attach(styles) || !last_was_parbreak {
|
||||
self.0.push(content, styles);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if content.is::<ColbreakElem>()
|
||||
|| content.is::<TagElem>()
|
||||
|| content.is::<PlaceElem>()
|
||||
|| content.is::<FlushElem>()
|
||||
@ -387,35 +393,17 @@ impl<'a> FlowBuilder<'a> {
|
||||
return true;
|
||||
}
|
||||
|
||||
if content.can::<dyn LayoutSingle>()
|
||||
|| content.can::<dyn LayoutMultiple>()
|
||||
|| content.is::<ParElem>()
|
||||
{
|
||||
let is_tight_list = if let Some(elem) = content.to_packed::<ListElem>() {
|
||||
elem.tight(styles)
|
||||
} else if let Some(elem) = content.to_packed::<EnumElem>() {
|
||||
elem.tight(styles)
|
||||
} else if let Some(elem) = content.to_packed::<TermsElem>() {
|
||||
elem.tight(styles)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if !last_was_parbreak && is_tight_list {
|
||||
let leading = ParElem::leading_in(styles);
|
||||
let spacing = VElem::list_attach(leading.into());
|
||||
self.0.push(arenas.store(spacing.pack()), styles);
|
||||
}
|
||||
|
||||
let (above, below) = if let Some(block) = content.to_packed::<BlockElem>() {
|
||||
(block.above(styles), block.below(styles))
|
||||
} else {
|
||||
(BlockElem::above_in(styles), BlockElem::below_in(styles))
|
||||
};
|
||||
|
||||
self.0.push(arenas.store(above.pack()), styles);
|
||||
if let Some(elem) = content.to_packed::<BlockElem>() {
|
||||
self.0.push(arenas.store(elem.above(styles).pack()), styles);
|
||||
self.0.push(content, styles);
|
||||
self.0.push(arenas.store(below.pack()), styles);
|
||||
self.0.push(arenas.store(elem.below(styles).pack()), styles);
|
||||
return true;
|
||||
}
|
||||
|
||||
if content.is::<ParElem>() {
|
||||
self.0.push(arenas.store(BlockElem::above_in(styles).pack()), styles);
|
||||
self.0.push(content, styles);
|
||||
self.0.push(arenas.store(BlockElem::below_in(styles).pack()), styles);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -452,9 +440,7 @@ impl<'a> ParBuilder<'a> {
|
||||
|| content.is::<HElem>()
|
||||
|| content.is::<LinebreakElem>()
|
||||
|| content.is::<SmartQuoteElem>()
|
||||
|| content
|
||||
.to_packed::<EquationElem>()
|
||||
.is_some_and(|elem| !elem.block(styles))
|
||||
|| content.is::<InlineElem>()
|
||||
|| content.is::<BoxElem>()
|
||||
{
|
||||
self.0.push(content, styles);
|
||||
|
@ -423,7 +423,7 @@ pub(crate) fn decorate(
|
||||
{
|
||||
let (top, bottom) = determine_edges(text, *top_edge, *bottom_edge);
|
||||
let size = Size::new(width + 2.0 * deco.extent, top - bottom);
|
||||
let rects = styled_rect(size, *radius, fill.clone(), stroke.clone());
|
||||
let rects = styled_rect(size, radius, fill.clone(), stroke);
|
||||
let origin = Point::new(pos.x - deco.extent, pos.y - top - shift);
|
||||
frame.prepend_multiple(
|
||||
rects
|
||||
|
@ -16,7 +16,7 @@ use crate::foundations::{
|
||||
cast, elem, scope, Args, Array, Bytes, Content, Fold, NativeElement, Packed,
|
||||
PlainText, Show, ShowSet, Smart, StyleChain, Styles, Synthesize, Value,
|
||||
};
|
||||
use crate::layout::{BlockElem, Em, HAlignment};
|
||||
use crate::layout::{BlockChild, BlockElem, Em, HAlignment};
|
||||
use crate::model::{Figurable, ParElem};
|
||||
use crate::syntax::{split_newlines, LinkedNode, Span, Spanned};
|
||||
use crate::text::{
|
||||
@ -444,8 +444,10 @@ impl Show for Packed<RawElem> {
|
||||
if self.block(styles) {
|
||||
// Align the text before inserting it into the block.
|
||||
realized = realized.aligned(self.align(styles).into());
|
||||
realized =
|
||||
BlockElem::new().with_body(Some(realized)).pack().spanned(self.span());
|
||||
realized = BlockElem::new()
|
||||
.with_body(Some(BlockChild::Content(realized)))
|
||||
.pack()
|
||||
.spanned(self.span());
|
||||
}
|
||||
|
||||
Ok(realized)
|
||||
|
@ -16,12 +16,12 @@ use ecow::EcoString;
|
||||
use crate::diag::{bail, At, SourceResult, StrResult};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{
|
||||
cast, elem, func, scope, Bytes, Cast, Content, NativeElement, Packed, Resolve, Smart,
|
||||
cast, elem, func, scope, Bytes, Cast, Content, NativeElement, Packed, Show, Smart,
|
||||
StyleChain,
|
||||
};
|
||||
use crate::layout::{
|
||||
Abs, Axes, FixedAlignment, Frame, FrameItem, LayoutSingle, Length, Point, Regions,
|
||||
Rel, Size,
|
||||
Abs, Axes, BlockElem, FixedAlignment, Frame, FrameItem, Length, Point, Region, Rel,
|
||||
Size,
|
||||
};
|
||||
use crate::loading::Readable;
|
||||
use crate::model::Figurable;
|
||||
@ -51,7 +51,7 @@ use crate::World;
|
||||
/// ```
|
||||
///
|
||||
/// [gh-svg]: https://github.com/typst/typst/issues?q=is%3Aopen+is%3Aissue+label%3Asvg
|
||||
#[elem(scope, LayoutSingle, LocalName, Figurable)]
|
||||
#[elem(scope, Show, LocalName, Figurable)]
|
||||
pub struct ImageElem {
|
||||
/// Path to an image file.
|
||||
#[required]
|
||||
@ -154,112 +154,12 @@ impl ImageElem {
|
||||
}
|
||||
}
|
||||
|
||||
impl LayoutSingle for Packed<ImageElem> {
|
||||
#[typst_macros::time(name = "image", span = self.span())]
|
||||
fn layout(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Frame> {
|
||||
// Take the format that was explicitly defined, or parse the extension,
|
||||
// or try to detect the format.
|
||||
let data = self.data();
|
||||
let format = match self.format(styles) {
|
||||
Smart::Custom(v) => v,
|
||||
Smart::Auto => {
|
||||
let ext = std::path::Path::new(self.path().as_str())
|
||||
.extension()
|
||||
.and_then(OsStr::to_str)
|
||||
.unwrap_or_default()
|
||||
.to_lowercase();
|
||||
|
||||
match ext.as_str() {
|
||||
"png" => ImageFormat::Raster(RasterFormat::Png),
|
||||
"jpg" | "jpeg" => ImageFormat::Raster(RasterFormat::Jpg),
|
||||
"gif" => ImageFormat::Raster(RasterFormat::Gif),
|
||||
"svg" | "svgz" => ImageFormat::Vector(VectorFormat::Svg),
|
||||
_ => match &data {
|
||||
Readable::Str(_) => ImageFormat::Vector(VectorFormat::Svg),
|
||||
Readable::Bytes(bytes) => match RasterFormat::detect(bytes) {
|
||||
Some(f) => ImageFormat::Raster(f),
|
||||
None => bail!(self.span(), "unknown image format"),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let image = Image::with_fonts(
|
||||
data.clone().into(),
|
||||
format,
|
||||
self.alt(styles),
|
||||
engine.world,
|
||||
&families(styles).map(|s| s.into()).collect::<Vec<_>>(),
|
||||
)
|
||||
.at(self.span())?;
|
||||
|
||||
let sizing = Axes::new(self.width(styles), self.height(styles));
|
||||
let region = sizing
|
||||
.zip_map(regions.base(), |s, r| s.map(|v| v.resolve(styles).relative_to(r)))
|
||||
.unwrap_or(regions.base());
|
||||
|
||||
let expand = sizing.as_ref().map(Smart::is_custom) | regions.expand;
|
||||
let region_ratio = region.x / region.y;
|
||||
|
||||
// Find out whether the image is wider or taller than the target size.
|
||||
let pxw = image.width();
|
||||
let pxh = image.height();
|
||||
let px_ratio = pxw / pxh;
|
||||
let wide = px_ratio > region_ratio;
|
||||
|
||||
// The space into which the image will be placed according to its fit.
|
||||
let target = if expand.x && expand.y {
|
||||
// If both width and height are forced, take them.
|
||||
region
|
||||
} else if expand.x {
|
||||
// If just width is forced, take it.
|
||||
Size::new(region.x, region.y.min(region.x / px_ratio))
|
||||
} else if expand.y {
|
||||
// If just height is forced, take it.
|
||||
Size::new(region.x.min(region.y * px_ratio), region.y)
|
||||
} else {
|
||||
// If neither is forced, take the natural image size at the image's
|
||||
// DPI bounded by the available space.
|
||||
let dpi = image.dpi().unwrap_or(Image::DEFAULT_DPI);
|
||||
let natural = Axes::new(pxw, pxh).map(|v| Abs::inches(v / dpi));
|
||||
Size::new(
|
||||
natural.x.min(region.x).min(region.y * px_ratio),
|
||||
natural.y.min(region.y).min(region.x / px_ratio),
|
||||
)
|
||||
};
|
||||
|
||||
// Compute the actual size of the fitted image.
|
||||
let fit = self.fit(styles);
|
||||
let fitted = match fit {
|
||||
ImageFit::Cover | ImageFit::Contain => {
|
||||
if wide == (fit == ImageFit::Contain) {
|
||||
Size::new(target.x, target.x / px_ratio)
|
||||
} else {
|
||||
Size::new(target.y * px_ratio, target.y)
|
||||
}
|
||||
}
|
||||
ImageFit::Stretch => target,
|
||||
};
|
||||
|
||||
// First, place the image in a frame of exactly its size and then resize
|
||||
// the frame to the target size, center aligning the image in the
|
||||
// process.
|
||||
let mut frame = Frame::soft(fitted);
|
||||
frame.push(Point::zero(), FrameItem::Image(image, fitted, self.span()));
|
||||
frame.resize(target, Axes::splat(FixedAlignment::Center));
|
||||
|
||||
// Create a clipping group if only part of the image should be visible.
|
||||
if fit == ImageFit::Cover && !target.fits(fitted) {
|
||||
frame.clip(Path::rect(frame.size()));
|
||||
}
|
||||
|
||||
Ok(frame)
|
||||
impl Show for Packed<ImageElem> {
|
||||
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
||||
Ok(BlockElem::single_layouter(self.clone(), layout_image)
|
||||
.with_width(self.width(styles))
|
||||
.with_height(self.height(styles))
|
||||
.pack())
|
||||
}
|
||||
}
|
||||
|
||||
@ -269,6 +169,117 @@ impl LocalName for Packed<ImageElem> {
|
||||
|
||||
impl Figurable for Packed<ImageElem> {}
|
||||
|
||||
/// Layout the image.
|
||||
#[typst_macros::time(span = elem.span())]
|
||||
fn layout_image(
|
||||
elem: &Packed<ImageElem>,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
region: Region,
|
||||
) -> SourceResult<Frame> {
|
||||
let span = elem.span();
|
||||
|
||||
// Take the format that was explicitly defined, or parse the extension,
|
||||
// or try to detect the format.
|
||||
let data = elem.data();
|
||||
let format = match elem.format(styles) {
|
||||
Smart::Custom(v) => v,
|
||||
Smart::Auto => determine_format(elem.path().as_str(), data).at(span)?,
|
||||
};
|
||||
|
||||
// Construct the image itself.
|
||||
let image = Image::with_fonts(
|
||||
data.clone().into(),
|
||||
format,
|
||||
elem.alt(styles),
|
||||
engine.world,
|
||||
&families(styles).map(|s| s.into()).collect::<Vec<_>>(),
|
||||
)
|
||||
.at(span)?;
|
||||
|
||||
// Determine the image's pixel aspect ratio.
|
||||
let pxw = image.width();
|
||||
let pxh = image.height();
|
||||
let px_ratio = pxw / pxh;
|
||||
|
||||
// Determine the region's aspect ratio.
|
||||
let region_ratio = region.size.x / region.size.y;
|
||||
|
||||
// Find out whether the image is wider or taller than the region.
|
||||
let wide = px_ratio > region_ratio;
|
||||
|
||||
// The space into which the image will be placed according to its fit.
|
||||
let target = if region.expand.x && region.expand.y {
|
||||
// If both width and height are forced, take them.
|
||||
region.size
|
||||
} else if region.expand.x {
|
||||
// If just width is forced, take it.
|
||||
Size::new(region.size.x, region.size.y.min(region.size.x / px_ratio))
|
||||
} else if region.expand.y {
|
||||
// If just height is forced, take it.
|
||||
Size::new(region.size.x.min(region.size.y * px_ratio), region.size.y)
|
||||
} else {
|
||||
// If neither is forced, take the natural image size at the image's
|
||||
// DPI bounded by the available space.
|
||||
let dpi = image.dpi().unwrap_or(Image::DEFAULT_DPI);
|
||||
let natural = Axes::new(pxw, pxh).map(|v| Abs::inches(v / dpi));
|
||||
Size::new(
|
||||
natural.x.min(region.size.x).min(region.size.y * px_ratio),
|
||||
natural.y.min(region.size.y).min(region.size.x / px_ratio),
|
||||
)
|
||||
};
|
||||
|
||||
// Compute the actual size of the fitted image.
|
||||
let fit = elem.fit(styles);
|
||||
let fitted = match fit {
|
||||
ImageFit::Cover | ImageFit::Contain => {
|
||||
if wide == (fit == ImageFit::Contain) {
|
||||
Size::new(target.x, target.x / px_ratio)
|
||||
} else {
|
||||
Size::new(target.y * px_ratio, target.y)
|
||||
}
|
||||
}
|
||||
ImageFit::Stretch => target,
|
||||
};
|
||||
|
||||
// First, place the image in a frame of exactly its size and then resize
|
||||
// the frame to the target size, center aligning the image in the
|
||||
// process.
|
||||
let mut frame = Frame::soft(fitted);
|
||||
frame.push(Point::zero(), FrameItem::Image(image, fitted, span));
|
||||
frame.resize(target, Axes::splat(FixedAlignment::Center));
|
||||
|
||||
// Create a clipping group if only part of the image should be visible.
|
||||
if fit == ImageFit::Cover && !target.fits(fitted) {
|
||||
frame.clip(Path::rect(frame.size()));
|
||||
}
|
||||
|
||||
Ok(frame)
|
||||
}
|
||||
|
||||
/// Determine the image format based on path and data.
|
||||
fn determine_format(path: &str, data: &Readable) -> StrResult<ImageFormat> {
|
||||
let ext = std::path::Path::new(path)
|
||||
.extension()
|
||||
.and_then(OsStr::to_str)
|
||||
.unwrap_or_default()
|
||||
.to_lowercase();
|
||||
|
||||
Ok(match ext.as_str() {
|
||||
"png" => ImageFormat::Raster(RasterFormat::Png),
|
||||
"jpg" | "jpeg" => ImageFormat::Raster(RasterFormat::Jpg),
|
||||
"gif" => ImageFormat::Raster(RasterFormat::Gif),
|
||||
"svg" | "svgz" => ImageFormat::Vector(VectorFormat::Svg),
|
||||
_ => match &data {
|
||||
Readable::Str(_) => ImageFormat::Vector(VectorFormat::Svg),
|
||||
Readable::Bytes(bytes) => match RasterFormat::detect(bytes) {
|
||||
Some(f) => ImageFormat::Raster(f),
|
||||
None => bail!("unknown image format"),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// How an image should adjust itself to a given area,
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
|
||||
pub enum ImageFit {
|
||||
|
@ -1,8 +1,8 @@
|
||||
use crate::diag::{bail, SourceResult};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{elem, Packed, StyleChain};
|
||||
use crate::foundations::{elem, Content, NativeElement, Packed, Show, StyleChain};
|
||||
use crate::layout::{
|
||||
Abs, Angle, Axes, Frame, FrameItem, LayoutSingle, Length, Regions, Rel, Size,
|
||||
Abs, Angle, Axes, BlockElem, Frame, FrameItem, Length, Region, Rel, Size,
|
||||
};
|
||||
use crate::utils::Numeric;
|
||||
use crate::visualize::{Geometry, Stroke};
|
||||
@ -20,7 +20,7 @@ use crate::visualize::{Geometry, Stroke};
|
||||
/// stroke: 2pt + maroon,
|
||||
/// )
|
||||
/// ```
|
||||
#[elem(LayoutSingle)]
|
||||
#[elem(Show)]
|
||||
pub struct LineElem {
|
||||
/// The start point of the line.
|
||||
///
|
||||
@ -58,37 +58,39 @@ pub struct LineElem {
|
||||
pub stroke: Stroke,
|
||||
}
|
||||
|
||||
impl LayoutSingle for Packed<LineElem> {
|
||||
#[typst_macros::time(name = "line", span = self.span())]
|
||||
fn layout(
|
||||
&self,
|
||||
_: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Frame> {
|
||||
let resolve =
|
||||
|axes: Axes<Rel<Abs>>| axes.zip_map(regions.base(), Rel::relative_to);
|
||||
let start = resolve(self.start(styles));
|
||||
let delta =
|
||||
self.end(styles).map(|end| resolve(end) - start).unwrap_or_else(|| {
|
||||
let length = self.length(styles);
|
||||
let angle = self.angle(styles);
|
||||
let x = angle.cos() * length;
|
||||
let y = angle.sin() * length;
|
||||
resolve(Axes::new(x, y))
|
||||
});
|
||||
|
||||
let stroke = self.stroke(styles).unwrap_or_default();
|
||||
let size = start.max(start + delta).max(Size::zero());
|
||||
let target = regions.expand.select(regions.size, size);
|
||||
|
||||
if !target.is_finite() {
|
||||
bail!(self.span(), "cannot create line with infinite length");
|
||||
}
|
||||
|
||||
let mut frame = Frame::soft(target);
|
||||
let shape = Geometry::Line(delta.to_point()).stroked(stroke);
|
||||
frame.push(start.to_point(), FrameItem::Shape(shape, self.span()));
|
||||
Ok(frame)
|
||||
impl Show for Packed<LineElem> {
|
||||
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
|
||||
Ok(BlockElem::single_layouter(self.clone(), layout_line).pack())
|
||||
}
|
||||
}
|
||||
|
||||
/// Layout the line.
|
||||
#[typst_macros::time(span = elem.span())]
|
||||
fn layout_line(
|
||||
elem: &Packed<LineElem>,
|
||||
_: &mut Engine,
|
||||
styles: StyleChain,
|
||||
region: Region,
|
||||
) -> SourceResult<Frame> {
|
||||
let resolve = |axes: Axes<Rel<Abs>>| axes.zip_map(region.size, Rel::relative_to);
|
||||
let start = resolve(elem.start(styles));
|
||||
let delta = elem.end(styles).map(|end| resolve(end) - start).unwrap_or_else(|| {
|
||||
let length = elem.length(styles);
|
||||
let angle = elem.angle(styles);
|
||||
let x = angle.cos() * length;
|
||||
let y = angle.sin() * length;
|
||||
resolve(Axes::new(x, y))
|
||||
});
|
||||
|
||||
let stroke = elem.stroke(styles).unwrap_or_default();
|
||||
let size = start.max(start + delta).max(Size::zero());
|
||||
|
||||
if !size.is_finite() {
|
||||
bail!(elem.span(), "cannot create line with infinite length");
|
||||
}
|
||||
|
||||
let mut frame = Frame::soft(size);
|
||||
let shape = Geometry::Line(delta.to_point()).stroked(stroke);
|
||||
frame.push(start.to_point(), FrameItem::Shape(shape, elem.span()));
|
||||
Ok(frame)
|
||||
}
|
||||
|
@ -3,10 +3,11 @@ use kurbo::{CubicBez, ParamCurveExtrema};
|
||||
use crate::diag::{bail, SourceResult};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{
|
||||
array, cast, elem, Array, Packed, Reflect, Resolve, Smart, StyleChain,
|
||||
array, cast, elem, Array, Content, NativeElement, Packed, Reflect, Resolve, Show,
|
||||
Smart, StyleChain,
|
||||
};
|
||||
use crate::layout::{
|
||||
Abs, Axes, Frame, FrameItem, LayoutSingle, Length, Point, Regions, Rel, Size,
|
||||
Abs, Axes, BlockElem, Frame, FrameItem, Length, Point, Region, Rel, Size,
|
||||
};
|
||||
use crate::visualize::{FixedStroke, Geometry, Paint, Shape, Stroke};
|
||||
|
||||
@ -25,7 +26,7 @@ use PathVertex::{AllControlPoints, MirroredControlPoint, Vertex};
|
||||
/// ((50%, 0pt), (40pt, 0pt)),
|
||||
/// )
|
||||
/// ```
|
||||
#[elem(LayoutSingle)]
|
||||
#[elem(Show)]
|
||||
pub struct PathElem {
|
||||
/// How to fill the path.
|
||||
///
|
||||
@ -69,88 +70,91 @@ pub struct PathElem {
|
||||
pub vertices: Vec<PathVertex>,
|
||||
}
|
||||
|
||||
impl LayoutSingle for Packed<PathElem> {
|
||||
#[typst_macros::time(name = "path", span = self.span())]
|
||||
fn layout(
|
||||
&self,
|
||||
_: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Frame> {
|
||||
let resolve = |axes: Axes<Rel<Length>>| {
|
||||
axes.resolve(styles)
|
||||
.zip_map(regions.base(), Rel::relative_to)
|
||||
.to_point()
|
||||
};
|
||||
|
||||
let vertices = self.vertices();
|
||||
let points: Vec<Point> = vertices.iter().map(|c| resolve(c.vertex())).collect();
|
||||
|
||||
let mut size = Size::zero();
|
||||
if points.is_empty() {
|
||||
return Ok(Frame::soft(size));
|
||||
}
|
||||
|
||||
// Only create a path if there are more than zero points.
|
||||
// Construct a closed path given all points.
|
||||
let mut path = Path::new();
|
||||
path.move_to(points[0]);
|
||||
|
||||
let mut add_cubic =
|
||||
|from_point: Point, to_point: Point, from: PathVertex, to: PathVertex| {
|
||||
let from_control_point = resolve(from.control_point_from()) + from_point;
|
||||
let to_control_point = resolve(to.control_point_to()) + to_point;
|
||||
path.cubic_to(from_control_point, to_control_point, to_point);
|
||||
|
||||
let p0 = kurbo::Point::new(from_point.x.to_raw(), from_point.y.to_raw());
|
||||
let p1 = kurbo::Point::new(
|
||||
from_control_point.x.to_raw(),
|
||||
from_control_point.y.to_raw(),
|
||||
);
|
||||
let p2 = kurbo::Point::new(
|
||||
to_control_point.x.to_raw(),
|
||||
to_control_point.y.to_raw(),
|
||||
);
|
||||
let p3 = kurbo::Point::new(to_point.x.to_raw(), to_point.y.to_raw());
|
||||
let extrema = CubicBez::new(p0, p1, p2, p3).bounding_box();
|
||||
size.x.set_max(Abs::raw(extrema.x1));
|
||||
size.y.set_max(Abs::raw(extrema.y1));
|
||||
};
|
||||
|
||||
for (vertex_window, point_window) in vertices.windows(2).zip(points.windows(2)) {
|
||||
let from = vertex_window[0];
|
||||
let to = vertex_window[1];
|
||||
let from_point = point_window[0];
|
||||
let to_point = point_window[1];
|
||||
|
||||
add_cubic(from_point, to_point, from, to);
|
||||
}
|
||||
|
||||
if self.closed(styles) {
|
||||
let from = *vertices.last().unwrap(); // We checked that we have at least one element.
|
||||
let to = vertices[0];
|
||||
let from_point = *points.last().unwrap();
|
||||
let to_point = points[0];
|
||||
|
||||
add_cubic(from_point, to_point, from, to);
|
||||
path.close_path();
|
||||
}
|
||||
|
||||
// Prepare fill and stroke.
|
||||
let fill = self.fill(styles);
|
||||
let stroke = match self.stroke(styles) {
|
||||
Smart::Auto if fill.is_none() => Some(FixedStroke::default()),
|
||||
Smart::Auto => None,
|
||||
Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default),
|
||||
};
|
||||
|
||||
let mut frame = Frame::soft(size);
|
||||
let shape = Shape { geometry: Geometry::Path(path), stroke, fill };
|
||||
frame.push(Point::zero(), FrameItem::Shape(shape, self.span()));
|
||||
Ok(frame)
|
||||
impl Show for Packed<PathElem> {
|
||||
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
|
||||
Ok(BlockElem::single_layouter(self.clone(), layout_path).pack())
|
||||
}
|
||||
}
|
||||
|
||||
/// Layout the path.
|
||||
#[typst_macros::time(span = elem.span())]
|
||||
fn layout_path(
|
||||
elem: &Packed<PathElem>,
|
||||
_: &mut Engine,
|
||||
styles: StyleChain,
|
||||
region: Region,
|
||||
) -> SourceResult<Frame> {
|
||||
let resolve = |axes: Axes<Rel<Length>>| {
|
||||
axes.resolve(styles).zip_map(region.size, Rel::relative_to).to_point()
|
||||
};
|
||||
|
||||
let vertices = elem.vertices();
|
||||
let points: Vec<Point> = vertices.iter().map(|c| resolve(c.vertex())).collect();
|
||||
|
||||
let mut size = Size::zero();
|
||||
if points.is_empty() {
|
||||
return Ok(Frame::soft(size));
|
||||
}
|
||||
|
||||
// Only create a path if there are more than zero points.
|
||||
// Construct a closed path given all points.
|
||||
let mut path = Path::new();
|
||||
path.move_to(points[0]);
|
||||
|
||||
let mut add_cubic = |from_point: Point,
|
||||
to_point: Point,
|
||||
from: PathVertex,
|
||||
to: PathVertex| {
|
||||
let from_control_point = resolve(from.control_point_from()) + from_point;
|
||||
let to_control_point = resolve(to.control_point_to()) + to_point;
|
||||
path.cubic_to(from_control_point, to_control_point, to_point);
|
||||
|
||||
let p0 = kurbo::Point::new(from_point.x.to_raw(), from_point.y.to_raw());
|
||||
let p1 = kurbo::Point::new(
|
||||
from_control_point.x.to_raw(),
|
||||
from_control_point.y.to_raw(),
|
||||
);
|
||||
let p2 =
|
||||
kurbo::Point::new(to_control_point.x.to_raw(), to_control_point.y.to_raw());
|
||||
let p3 = kurbo::Point::new(to_point.x.to_raw(), to_point.y.to_raw());
|
||||
let extrema = CubicBez::new(p0, p1, p2, p3).bounding_box();
|
||||
size.x.set_max(Abs::raw(extrema.x1));
|
||||
size.y.set_max(Abs::raw(extrema.y1));
|
||||
};
|
||||
|
||||
for (vertex_window, point_window) in vertices.windows(2).zip(points.windows(2)) {
|
||||
let from = vertex_window[0];
|
||||
let to = vertex_window[1];
|
||||
let from_point = point_window[0];
|
||||
let to_point = point_window[1];
|
||||
|
||||
add_cubic(from_point, to_point, from, to);
|
||||
}
|
||||
|
||||
if elem.closed(styles) {
|
||||
let from = *vertices.last().unwrap(); // We checked that we have at least one element.
|
||||
let to = vertices[0];
|
||||
let from_point = *points.last().unwrap();
|
||||
let to_point = points[0];
|
||||
|
||||
add_cubic(from_point, to_point, from, to);
|
||||
path.close_path();
|
||||
}
|
||||
|
||||
// Prepare fill and stroke.
|
||||
let fill = elem.fill(styles);
|
||||
let stroke = match elem.stroke(styles) {
|
||||
Smart::Auto if fill.is_none() => Some(FixedStroke::default()),
|
||||
Smart::Auto => None,
|
||||
Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default),
|
||||
};
|
||||
|
||||
let mut frame = Frame::soft(size);
|
||||
let shape = Shape { geometry: Geometry::Path(path), stroke, fill };
|
||||
frame.push(Point::zero(), FrameItem::Shape(shape, elem.span()));
|
||||
Ok(frame)
|
||||
}
|
||||
|
||||
/// A component used for path creation.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum PathVertex {
|
||||
|
@ -6,7 +6,7 @@ use ecow::{eco_format, EcoString};
|
||||
use crate::diag::{bail, SourceResult};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{func, repr, scope, ty, Content, Smart, StyleChain};
|
||||
use crate::layout::{Abs, Axes, Frame, LayoutMultiple, Length, Regions, Size};
|
||||
use crate::layout::{Abs, Axes, Frame, Length, Regions, Size};
|
||||
use crate::syntax::{Span, Spanned};
|
||||
use crate::utils::{LazyHash, Numeric};
|
||||
use crate::visualize::RelativeTo;
|
||||
|
@ -3,11 +3,9 @@ use std::f64::consts::PI;
|
||||
use crate::diag::{bail, SourceResult};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{
|
||||
elem, func, scope, Content, NativeElement, Packed, Resolve, Smart, StyleChain,
|
||||
};
|
||||
use crate::layout::{
|
||||
Axes, Em, Frame, FrameItem, LayoutSingle, Length, Point, Regions, Rel,
|
||||
elem, func, scope, Content, NativeElement, Packed, Resolve, Show, Smart, StyleChain,
|
||||
};
|
||||
use crate::layout::{Axes, BlockElem, Em, Frame, FrameItem, Length, Point, Region, Rel};
|
||||
use crate::syntax::Span;
|
||||
use crate::utils::Numeric;
|
||||
use crate::visualize::{FixedStroke, Geometry, Paint, Path, Shape, Stroke};
|
||||
@ -27,7 +25,7 @@ use crate::visualize::{FixedStroke, Geometry, Paint, Path, Shape, Stroke};
|
||||
/// (0%, 2cm),
|
||||
/// )
|
||||
/// ```
|
||||
#[elem(scope, LayoutSingle)]
|
||||
#[elem(scope, Show)]
|
||||
pub struct PolygonElem {
|
||||
/// How to fill the polygon.
|
||||
///
|
||||
@ -125,52 +123,55 @@ impl PolygonElem {
|
||||
}
|
||||
}
|
||||
|
||||
impl LayoutSingle for Packed<PolygonElem> {
|
||||
#[typst_macros::time(name = "polygon", span = self.span())]
|
||||
fn layout(
|
||||
&self,
|
||||
_: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Frame> {
|
||||
let points: Vec<Point> = self
|
||||
.vertices()
|
||||
.iter()
|
||||
.map(|c| {
|
||||
c.resolve(styles).zip_map(regions.base(), Rel::relative_to).to_point()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let size = points.iter().fold(Point::zero(), |max, c| c.max(max)).to_size();
|
||||
if !size.is_finite() {
|
||||
bail!(self.span(), "cannot create polygon with infinite size");
|
||||
}
|
||||
|
||||
let mut frame = Frame::hard(size);
|
||||
|
||||
// Only create a path if there are more than zero points.
|
||||
if points.is_empty() {
|
||||
return Ok(frame);
|
||||
}
|
||||
|
||||
// Prepare fill and stroke.
|
||||
let fill = self.fill(styles);
|
||||
let stroke = match self.stroke(styles) {
|
||||
Smart::Auto if fill.is_none() => Some(FixedStroke::default()),
|
||||
Smart::Auto => None,
|
||||
Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default),
|
||||
};
|
||||
|
||||
// Construct a closed path given all points.
|
||||
let mut path = Path::new();
|
||||
path.move_to(points[0]);
|
||||
for &point in &points[1..] {
|
||||
path.line_to(point);
|
||||
}
|
||||
path.close_path();
|
||||
|
||||
let shape = Shape { geometry: Geometry::Path(path), stroke, fill };
|
||||
frame.push(Point::zero(), FrameItem::Shape(shape, self.span()));
|
||||
Ok(frame)
|
||||
impl Show for Packed<PolygonElem> {
|
||||
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
|
||||
Ok(BlockElem::single_layouter(self.clone(), layout_polygon).pack())
|
||||
}
|
||||
}
|
||||
|
||||
/// Layout the polygon.
|
||||
#[typst_macros::time(span = elem.span())]
|
||||
fn layout_polygon(
|
||||
elem: &Packed<PolygonElem>,
|
||||
_: &mut Engine,
|
||||
styles: StyleChain,
|
||||
region: Region,
|
||||
) -> SourceResult<Frame> {
|
||||
let points: Vec<Point> = elem
|
||||
.vertices()
|
||||
.iter()
|
||||
.map(|c| c.resolve(styles).zip_map(region.size, Rel::relative_to).to_point())
|
||||
.collect();
|
||||
|
||||
let size = points.iter().fold(Point::zero(), |max, c| c.max(max)).to_size();
|
||||
if !size.is_finite() {
|
||||
bail!(elem.span(), "cannot create polygon with infinite size");
|
||||
}
|
||||
|
||||
let mut frame = Frame::hard(size);
|
||||
|
||||
// Only create a path if there are more than zero points.
|
||||
if points.is_empty() {
|
||||
return Ok(frame);
|
||||
}
|
||||
|
||||
// Prepare fill and stroke.
|
||||
let fill = elem.fill(styles);
|
||||
let stroke = match elem.stroke(styles) {
|
||||
Smart::Auto if fill.is_none() => Some(FixedStroke::default()),
|
||||
Smart::Auto => None,
|
||||
Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default),
|
||||
};
|
||||
|
||||
// Construct a closed path given all points.
|
||||
let mut path = Path::new();
|
||||
path.move_to(points[0]);
|
||||
for &point in &points[1..] {
|
||||
path.line_to(point);
|
||||
}
|
||||
path.close_path();
|
||||
|
||||
let shape = Shape { geometry: Geometry::Path(path), stroke, fill };
|
||||
frame.push(Point::zero(), FrameItem::Shape(shape, elem.span()));
|
||||
Ok(frame)
|
||||
}
|
||||
|
@ -2,10 +2,10 @@ use std::f64::consts::SQRT_2;
|
||||
|
||||
use crate::diag::SourceResult;
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{elem, Content, Packed, Resolve, Smart, StyleChain};
|
||||
use crate::foundations::{elem, Content, NativeElement, Packed, Show, Smart, StyleChain};
|
||||
use crate::layout::{
|
||||
Abs, Axes, Corner, Corners, Frame, FrameItem, LayoutMultiple, LayoutSingle, Length,
|
||||
Point, Ratio, Regions, Rel, Sides, Size,
|
||||
Abs, Axes, BlockElem, Corner, Corners, Frame, FrameItem, Length, Point, Ratio,
|
||||
Region, Regions, Rel, Sides, Size,
|
||||
};
|
||||
use crate::syntax::Span;
|
||||
use crate::utils::Get;
|
||||
@ -24,7 +24,7 @@ use crate::visualize::{FixedStroke, Paint, Path, Stroke};
|
||||
/// to fit the content.
|
||||
/// ]
|
||||
/// ```
|
||||
#[elem(title = "Rectangle", LayoutSingle)]
|
||||
#[elem(title = "Rectangle", Show)]
|
||||
pub struct RectElem {
|
||||
/// The rectangle's width, relative to its parent container.
|
||||
pub width: Smart<Rel<Length>>,
|
||||
@ -128,31 +128,30 @@ pub struct RectElem {
|
||||
/// When this is omitted, the rectangle takes on a default size of at most
|
||||
/// `{45pt}` by `{30pt}`.
|
||||
#[positional]
|
||||
#[borrowed]
|
||||
pub body: Option<Content>,
|
||||
}
|
||||
|
||||
impl LayoutSingle for Packed<RectElem> {
|
||||
#[typst_macros::time(name = "rect", span = self.span())]
|
||||
fn layout(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Frame> {
|
||||
layout(
|
||||
engine,
|
||||
styles,
|
||||
regions,
|
||||
ShapeKind::Rect,
|
||||
&self.body(styles),
|
||||
Axes::new(self.width(styles), self.height(styles)),
|
||||
self.fill(styles),
|
||||
self.stroke(styles),
|
||||
self.inset(styles),
|
||||
self.outset(styles),
|
||||
self.radius(styles),
|
||||
self.span(),
|
||||
)
|
||||
impl Show for Packed<RectElem> {
|
||||
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
||||
Ok(BlockElem::single_layouter(self.clone(), |elem, engine, styles, region| {
|
||||
layout_shape(
|
||||
engine,
|
||||
styles,
|
||||
region,
|
||||
ShapeKind::Rect,
|
||||
elem.body(styles),
|
||||
elem.fill(styles),
|
||||
elem.stroke(styles),
|
||||
elem.inset(styles),
|
||||
elem.outset(styles),
|
||||
elem.radius(styles),
|
||||
elem.span(),
|
||||
)
|
||||
})
|
||||
.with_width(self.width(styles))
|
||||
.with_height(self.height(styles))
|
||||
.pack())
|
||||
}
|
||||
}
|
||||
|
||||
@ -169,7 +168,7 @@ impl LayoutSingle for Packed<RectElem> {
|
||||
/// sized to fit.
|
||||
/// ]
|
||||
/// ```
|
||||
#[elem(LayoutSingle)]
|
||||
#[elem(Show)]
|
||||
pub struct SquareElem {
|
||||
/// The square's side length. This is mutually exclusive with `width` and
|
||||
/// `height`.
|
||||
@ -234,31 +233,30 @@ pub struct SquareElem {
|
||||
/// When this is omitted, the square takes on a default size of at most
|
||||
/// `{30pt}`.
|
||||
#[positional]
|
||||
#[borrowed]
|
||||
pub body: Option<Content>,
|
||||
}
|
||||
|
||||
impl LayoutSingle for Packed<SquareElem> {
|
||||
#[typst_macros::time(name = "square", span = self.span())]
|
||||
fn layout(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Frame> {
|
||||
layout(
|
||||
engine,
|
||||
styles,
|
||||
regions,
|
||||
ShapeKind::Square,
|
||||
&self.body(styles),
|
||||
Axes::new(self.width(styles), self.height(styles)),
|
||||
self.fill(styles),
|
||||
self.stroke(styles),
|
||||
self.inset(styles),
|
||||
self.outset(styles),
|
||||
self.radius(styles),
|
||||
self.span(),
|
||||
)
|
||||
impl Show for Packed<SquareElem> {
|
||||
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
||||
Ok(BlockElem::single_layouter(self.clone(), |elem, engine, styles, regions| {
|
||||
layout_shape(
|
||||
engine,
|
||||
styles,
|
||||
regions,
|
||||
ShapeKind::Square,
|
||||
elem.body(styles),
|
||||
elem.fill(styles),
|
||||
elem.stroke(styles),
|
||||
elem.inset(styles),
|
||||
elem.outset(styles),
|
||||
elem.radius(styles),
|
||||
elem.span(),
|
||||
)
|
||||
})
|
||||
.with_width(self.width(styles))
|
||||
.with_height(self.height(styles))
|
||||
.pack())
|
||||
}
|
||||
}
|
||||
|
||||
@ -276,7 +274,7 @@ impl LayoutSingle for Packed<SquareElem> {
|
||||
/// to fit the content.
|
||||
/// ]
|
||||
/// ```
|
||||
#[elem(LayoutSingle)]
|
||||
#[elem(Show)]
|
||||
pub struct EllipseElem {
|
||||
/// The ellipse's width, relative to its parent container.
|
||||
pub width: Smart<Rel<Length>>,
|
||||
@ -312,31 +310,30 @@ pub struct EllipseElem {
|
||||
/// When this is omitted, the ellipse takes on a default size of at most
|
||||
/// `{45pt}` by `{30pt}`.
|
||||
#[positional]
|
||||
#[borrowed]
|
||||
pub body: Option<Content>,
|
||||
}
|
||||
|
||||
impl LayoutSingle for Packed<EllipseElem> {
|
||||
#[typst_macros::time(name = "ellipse", span = self.span())]
|
||||
fn layout(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Frame> {
|
||||
layout(
|
||||
engine,
|
||||
styles,
|
||||
regions,
|
||||
ShapeKind::Ellipse,
|
||||
&self.body(styles),
|
||||
Axes::new(self.width(styles), self.height(styles)),
|
||||
self.fill(styles),
|
||||
self.stroke(styles).map(|s| Sides::splat(Some(s))),
|
||||
self.inset(styles),
|
||||
self.outset(styles),
|
||||
Corners::splat(None),
|
||||
self.span(),
|
||||
)
|
||||
impl Show for Packed<EllipseElem> {
|
||||
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
||||
Ok(BlockElem::single_layouter(self.clone(), |elem, engine, styles, regions| {
|
||||
layout_shape(
|
||||
engine,
|
||||
styles,
|
||||
regions,
|
||||
ShapeKind::Ellipse,
|
||||
elem.body(styles),
|
||||
elem.fill(styles),
|
||||
elem.stroke(styles).map(|s| Sides::splat(Some(s))),
|
||||
elem.inset(styles),
|
||||
elem.outset(styles),
|
||||
Corners::splat(None),
|
||||
elem.span(),
|
||||
)
|
||||
})
|
||||
.with_width(self.width(styles))
|
||||
.with_height(self.height(styles))
|
||||
.pack())
|
||||
}
|
||||
}
|
||||
|
||||
@ -354,7 +351,7 @@ impl LayoutSingle for Packed<EllipseElem> {
|
||||
/// sized to fit.
|
||||
/// ]
|
||||
/// ```
|
||||
#[elem(LayoutSingle)]
|
||||
#[elem(Show)]
|
||||
pub struct CircleElem {
|
||||
/// The circle's radius. This is mutually exclusive with `width` and
|
||||
/// `height`.
|
||||
@ -415,43 +412,42 @@ pub struct CircleElem {
|
||||
/// The content to place into the circle. The circle expands to fit this
|
||||
/// content, keeping the 1-1 aspect ratio.
|
||||
#[positional]
|
||||
#[borrowed]
|
||||
pub body: Option<Content>,
|
||||
}
|
||||
|
||||
impl LayoutSingle for Packed<CircleElem> {
|
||||
#[typst_macros::time(name = "circle", span = self.span())]
|
||||
fn layout(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Frame> {
|
||||
layout(
|
||||
engine,
|
||||
styles,
|
||||
regions,
|
||||
ShapeKind::Circle,
|
||||
&self.body(styles),
|
||||
Axes::new(self.width(styles), self.height(styles)),
|
||||
self.fill(styles),
|
||||
self.stroke(styles).map(|s| Sides::splat(Some(s))),
|
||||
self.inset(styles),
|
||||
self.outset(styles),
|
||||
Corners::splat(None),
|
||||
self.span(),
|
||||
)
|
||||
impl Show for Packed<CircleElem> {
|
||||
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
||||
Ok(BlockElem::single_layouter(self.clone(), |elem, engine, styles, regions| {
|
||||
layout_shape(
|
||||
engine,
|
||||
styles,
|
||||
regions,
|
||||
ShapeKind::Circle,
|
||||
elem.body(styles),
|
||||
elem.fill(styles),
|
||||
elem.stroke(styles).map(|s| Sides::splat(Some(s))),
|
||||
elem.inset(styles),
|
||||
elem.outset(styles),
|
||||
Corners::splat(None),
|
||||
elem.span(),
|
||||
)
|
||||
})
|
||||
.with_width(self.width(styles))
|
||||
.with_height(self.height(styles))
|
||||
.pack())
|
||||
}
|
||||
}
|
||||
|
||||
/// Layout a shape.
|
||||
#[typst_macros::time(span = span)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout(
|
||||
fn layout_shape(
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
region: Region,
|
||||
kind: ShapeKind,
|
||||
body: &Option<Content>,
|
||||
sizing: Axes<Smart<Rel<Length>>>,
|
||||
fill: Option<Paint>,
|
||||
stroke: Smart<Sides<Option<Option<Stroke<Abs>>>>>,
|
||||
inset: Sides<Option<Rel<Abs>>>,
|
||||
@ -459,47 +455,41 @@ fn layout(
|
||||
radius: Corners<Option<Rel<Abs>>>,
|
||||
span: Span,
|
||||
) -> SourceResult<Frame> {
|
||||
let resolved = sizing
|
||||
.zip_map(regions.base(), |s, r| s.map(|v| v.resolve(styles).relative_to(r)));
|
||||
|
||||
let mut frame;
|
||||
let mut inset = inset.unwrap_or_default();
|
||||
|
||||
if let Some(child) = body {
|
||||
let region = resolved.unwrap_or(regions.base());
|
||||
|
||||
let mut inset = inset.unwrap_or_default();
|
||||
if kind.is_round() {
|
||||
inset = inset.map(|side| side + Ratio::new(0.5 - SQRT_2 / 4.0));
|
||||
// Apply extra inset to round shapes.
|
||||
inset = inset.map(|v| v + Ratio::new(0.5 - SQRT_2 / 4.0));
|
||||
}
|
||||
let has_inset = !inset.is_zero();
|
||||
|
||||
// Take the inset, if any, into account.
|
||||
let mut pod = region;
|
||||
if has_inset {
|
||||
pod.size = crate::layout::shrink(region.size, &inset);
|
||||
}
|
||||
|
||||
// Pad the child.
|
||||
let child = child.clone().padded(inset.map(|side| side.map(Length::from)));
|
||||
let expand = sizing.as_ref().map(Smart::is_custom);
|
||||
let pod = Regions::one(region, expand);
|
||||
frame = child.layout(engine, styles, pod)?.into_frame();
|
||||
// Layout the child.
|
||||
frame = child.layout(engine, styles, pod.into_regions())?.into_frame();
|
||||
|
||||
// Enforce correct size.
|
||||
*frame.size_mut() = expand.select(region, frame.size());
|
||||
|
||||
// Relayout with full expansion into square region to make sure
|
||||
// the result is really a square or circle.
|
||||
// If the child is a square or circle, relayout with full expansion into
|
||||
// square region to make sure the result is really quadratic.
|
||||
if kind.is_quadratic() {
|
||||
frame.set_size(Size::splat(frame.size().max_by_side()));
|
||||
let length = frame.size().max_by_side().min(region.min_by_side());
|
||||
let pod = Regions::one(Size::splat(length), Axes::splat(true));
|
||||
frame = child.layout(engine, styles, pod)?.into_frame();
|
||||
let length = frame.size().max_by_side().min(pod.size.min_by_side());
|
||||
let quad_pod = Regions::one(Size::splat(length), Axes::splat(true));
|
||||
frame = child.layout(engine, styles, quad_pod)?.into_frame();
|
||||
}
|
||||
|
||||
// Enforce correct size again.
|
||||
*frame.size_mut() = expand.select(region, frame.size());
|
||||
if kind.is_quadratic() {
|
||||
frame.set_size(Size::splat(frame.size().max_by_side()));
|
||||
// Apply the inset.
|
||||
if has_inset {
|
||||
crate::layout::grow(&mut frame, &inset);
|
||||
}
|
||||
} else {
|
||||
// The default size that a shape takes on if it has no child and
|
||||
// enough space.
|
||||
let default = Size::new(Abs::pt(45.0), Abs::pt(30.0));
|
||||
let mut size = resolved.unwrap_or(default.min(regions.base()));
|
||||
let mut size = region.expand.select(region.size, default.min(region.size));
|
||||
if kind.is_quadratic() {
|
||||
size = Size::splat(size.min_by_side());
|
||||
}
|
||||
@ -526,9 +516,9 @@ fn layout(
|
||||
} else {
|
||||
frame.fill_and_stroke(
|
||||
fill,
|
||||
stroke,
|
||||
outset.unwrap_or_default(),
|
||||
radius.unwrap_or_default(),
|
||||
&stroke,
|
||||
&outset.unwrap_or_default(),
|
||||
&radius.unwrap_or_default(),
|
||||
span,
|
||||
);
|
||||
}
|
||||
@ -633,7 +623,7 @@ pub(crate) fn ellipse(
|
||||
/// Creates a new rectangle as a path.
|
||||
pub(crate) fn clip_rect(
|
||||
size: Size,
|
||||
radius: Corners<Rel<Abs>>,
|
||||
radius: &Corners<Rel<Abs>>,
|
||||
stroke: &Sides<Option<FixedStroke>>,
|
||||
) -> Path {
|
||||
let stroke_widths = stroke
|
||||
@ -644,8 +634,7 @@ pub(crate) fn clip_rect(
|
||||
+ stroke_widths.iter().cloned().min().unwrap_or(Abs::zero());
|
||||
|
||||
let radius = radius.map(|side| side.relative_to(max_radius * 2.0).min(max_radius));
|
||||
|
||||
let corners = corners_control_points(size, radius, stroke, stroke_widths);
|
||||
let corners = corners_control_points(size, &radius, stroke, &stroke_widths);
|
||||
|
||||
let mut path = Path::new();
|
||||
if corners.top_left.arc_inner() {
|
||||
@ -674,12 +663,12 @@ pub(crate) fn clip_rect(
|
||||
/// - use fill for sides for best looks
|
||||
pub(crate) fn styled_rect(
|
||||
size: Size,
|
||||
radius: Corners<Rel<Abs>>,
|
||||
radius: &Corners<Rel<Abs>>,
|
||||
fill: Option<Paint>,
|
||||
stroke: Sides<Option<FixedStroke>>,
|
||||
stroke: &Sides<Option<FixedStroke>>,
|
||||
) -> Vec<Shape> {
|
||||
if stroke.is_uniform() && radius.iter().cloned().all(Rel::is_zero) {
|
||||
simple_rect(size, fill, stroke.top)
|
||||
simple_rect(size, fill, stroke.top.clone())
|
||||
} else {
|
||||
segmented_rect(size, radius, fill, stroke)
|
||||
}
|
||||
@ -696,9 +685,9 @@ fn simple_rect(
|
||||
|
||||
fn corners_control_points(
|
||||
size: Size,
|
||||
radius: Corners<Abs>,
|
||||
radius: &Corners<Abs>,
|
||||
strokes: &Sides<Option<FixedStroke>>,
|
||||
stroke_widths: Sides<Abs>,
|
||||
stroke_widths: &Sides<Abs>,
|
||||
) -> Corners<ControlPoints> {
|
||||
Corners {
|
||||
top_left: Corner::TopLeft,
|
||||
@ -726,9 +715,9 @@ fn corners_control_points(
|
||||
/// Use stroke and fill for the rectangle
|
||||
fn segmented_rect(
|
||||
size: Size,
|
||||
radius: Corners<Rel<Abs>>,
|
||||
radius: &Corners<Rel<Abs>>,
|
||||
fill: Option<Paint>,
|
||||
strokes: Sides<Option<FixedStroke>>,
|
||||
strokes: &Sides<Option<FixedStroke>>,
|
||||
) -> Vec<Shape> {
|
||||
let mut res = vec![];
|
||||
let stroke_widths = strokes
|
||||
@ -739,8 +728,7 @@ fn segmented_rect(
|
||||
+ stroke_widths.iter().cloned().min().unwrap_or(Abs::zero());
|
||||
|
||||
let radius = radius.map(|side| side.relative_to(max_radius * 2.0).min(max_radius));
|
||||
|
||||
let corners = corners_control_points(size, radius, &strokes, stroke_widths);
|
||||
let corners = corners_control_points(size, &radius, strokes, &stroke_widths);
|
||||
|
||||
// insert stroked sides below filled sides
|
||||
let mut stroke_insert = 0;
|
||||
@ -786,10 +774,7 @@ fn segmented_rect(
|
||||
let start = last;
|
||||
let end = current;
|
||||
last = current;
|
||||
let stroke = match strokes.get_ref(start.side_cw()) {
|
||||
None => continue,
|
||||
Some(stroke) => stroke.clone(),
|
||||
};
|
||||
let Some(stroke) = strokes.get_ref(start.side_cw()) else { continue };
|
||||
let (shape, ontop) = segment(start, end, &corners, stroke);
|
||||
if ontop {
|
||||
res.push(shape);
|
||||
@ -798,7 +783,7 @@ fn segmented_rect(
|
||||
stroke_insert += 1;
|
||||
}
|
||||
}
|
||||
} else if let Some(stroke) = strokes.top {
|
||||
} else if let Some(stroke) = &strokes.top {
|
||||
// single segment
|
||||
let (shape, _) = segment(Corner::TopLeft, Corner::TopLeft, &corners, stroke);
|
||||
res.push(shape);
|
||||
@ -848,7 +833,7 @@ fn segment(
|
||||
start: Corner,
|
||||
end: Corner,
|
||||
corners: &Corners<ControlPoints>,
|
||||
stroke: FixedStroke,
|
||||
stroke: &FixedStroke,
|
||||
) -> (Shape, bool) {
|
||||
fn fill_corner(corner: &ControlPoints) -> bool {
|
||||
corner.stroke_before != corner.stroke_after
|
||||
@ -883,12 +868,12 @@ fn segment(
|
||||
.unwrap_or(true);
|
||||
|
||||
let use_fill = solid && fill_corners(start, end, corners);
|
||||
|
||||
let shape = if use_fill {
|
||||
fill_segment(start, end, corners, stroke)
|
||||
} else {
|
||||
stroke_segment(start, end, corners, stroke)
|
||||
stroke_segment(start, end, corners, stroke.clone())
|
||||
};
|
||||
|
||||
(shape, use_fill)
|
||||
}
|
||||
|
||||
@ -899,7 +884,7 @@ fn stroke_segment(
|
||||
corners: &Corners<ControlPoints>,
|
||||
stroke: FixedStroke,
|
||||
) -> Shape {
|
||||
// create start corner
|
||||
// Create start corner.
|
||||
let mut path = Path::new();
|
||||
path_segment(start, end, corners, &mut path);
|
||||
|
||||
@ -915,7 +900,7 @@ fn fill_segment(
|
||||
start: Corner,
|
||||
end: Corner,
|
||||
corners: &Corners<ControlPoints>,
|
||||
stroke: FixedStroke,
|
||||
stroke: &FixedStroke,
|
||||
) -> Shape {
|
||||
let mut path = Path::new();
|
||||
|
||||
@ -1004,7 +989,7 @@ fn fill_segment(
|
||||
Shape {
|
||||
geometry: Geometry::Path(path),
|
||||
stroke: None,
|
||||
fill: Some(stroke.paint),
|
||||
fill: Some(stroke.paint.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -14,19 +14,15 @@ use once_cell::sync::Lazy;
|
||||
use serde::Deserialize;
|
||||
use serde_yaml as yaml;
|
||||
use typst::diag::{bail, StrResult};
|
||||
use typst::foundations::AutoValue;
|
||||
use typst::foundations::Bytes;
|
||||
use typst::foundations::NoneValue;
|
||||
use typst::foundations::{
|
||||
CastInfo, Category, Func, Module, ParamInfo, Repr, Scope, Smart, Type, Value,
|
||||
FOUNDATIONS,
|
||||
AutoValue, Bytes, CastInfo, Category, Func, Module, NoneValue, ParamInfo, Repr,
|
||||
Scope, Smart, Type, Value, FOUNDATIONS,
|
||||
};
|
||||
use typst::introspection::INTROSPECTION;
|
||||
use typst::layout::{Abs, Margin, PageElem, LAYOUT};
|
||||
use typst::loading::DATA_LOADING;
|
||||
use typst::math::MATH;
|
||||
use typst::model::Document;
|
||||
use typst::model::MODEL;
|
||||
use typst::model::{Document, MODEL};
|
||||
use typst::symbols::SYMBOLS;
|
||||
use typst::text::{Font, FontBook, TEXT};
|
||||
use typst::utils::LazyHash;
|
||||
|
BIN
tests/ref/block-consistent-width.png
Normal file
After Width: | Height: | Size: 870 B |
Before Width: | Height: | Size: 879 B After Width: | Height: | Size: 881 B |
BIN
tests/ref/gradient-linear-relative-parent-block.png
Normal file
After Width: | Height: | Size: 327 B |
Before Width: | Height: | Size: 321 B After Width: | Height: | Size: 333 B |
Before Width: | Height: | Size: 305 B After Width: | Height: | Size: 289 B |
@ -96,6 +96,18 @@ Paragraph
|
||||
lorem(8) + colbreak(),
|
||||
)
|
||||
|
||||
--- block-consistent-width ---
|
||||
// Test that block enforces consistent width across regions. Also use some
|
||||
// introspection to check that measurement is working correctly.
|
||||
#block(stroke: 1pt, inset: 5pt)[
|
||||
#align(right)[Hi]
|
||||
#colbreak()
|
||||
Hello @netwok
|
||||
]
|
||||
|
||||
#show bibliography: none
|
||||
#bibliography("/assets/bib/works.bib")
|
||||
|
||||
--- box-clip-rect ---
|
||||
// Test box clipping with a rectangle
|
||||
Hello #box(width: 1em, height: 1em, clip: false)[#rect(width: 3em, height: 3em, fill: red)]
|
||||
|
@ -31,7 +31,7 @@ Placed item in the first region.
|
||||
// In-flow item with size zero in the first region.
|
||||
#set page(height: 5cm, margin: 1cm)
|
||||
In-flow, zero-sized item.
|
||||
#block(breakable: true, stroke: 1pt, inset: 0.5cm)[
|
||||
#block(breakable: true, stroke: 1pt, inset: 0.4cm)[
|
||||
#set block(spacing: 0pt)
|
||||
#line(length: 0pt)
|
||||
#rect(height: 2cm, fill: gray)
|
||||
|
@ -45,11 +45,10 @@
|
||||
fill: gradient.linear(red, purple, space: color.hsl)
|
||||
)
|
||||
|
||||
|
||||
--- gradient-linear-relative-parent ---
|
||||
// The image should look as if there is a single gradient that is being used for
|
||||
// both the page and the rectangles.
|
||||
#let grad = gradient.linear(red, blue, green, purple, relative: "parent");
|
||||
#let grad = gradient.linear(red, blue, green, purple, relative: "parent")
|
||||
#let my-rect = rect(width: 50%, height: 50%, fill: grad)
|
||||
#set page(
|
||||
height: 50pt,
|
||||
@ -64,7 +63,7 @@
|
||||
--- gradient-linear-relative-self ---
|
||||
// The image should look as if there are multiple gradients, one for each
|
||||
// rectangle.
|
||||
#let grad = gradient.linear(red, blue, green, purple, relative: "self");
|
||||
#let grad = gradient.linear(red, blue, green, purple, relative: "self")
|
||||
#let my-rect = rect(width: 50%, height: 50%, fill: grad)
|
||||
#set page(
|
||||
height: 50pt,
|
||||
@ -76,6 +75,29 @@
|
||||
#place(top + right, my-rect)
|
||||
#place(bottom + center, rotate(45deg, my-rect))
|
||||
|
||||
--- gradient-linear-relative-parent-block ---
|
||||
// The image should look as if there are two nested gradients, one for the page
|
||||
// and one for a nested block. The rotated rectangles are not visible because
|
||||
// they are relative to the block.
|
||||
#let grad = gradient.linear(red, blue, green, purple, relative: "parent")
|
||||
#let my-rect = rect(width: 50%, height: 50%, fill: grad)
|
||||
#set page(
|
||||
height: 50pt,
|
||||
width: 50pt,
|
||||
margin: 5pt,
|
||||
fill: grad,
|
||||
background: place(top + left, my-rect),
|
||||
)
|
||||
#block(
|
||||
width: 40pt,
|
||||
height: 40pt,
|
||||
inset: 2.5pt,
|
||||
fill: grad,
|
||||
)[
|
||||
#place(top + right, my-rect)
|
||||
#place(bottom + center, rotate(45deg, my-rect))
|
||||
]
|
||||
|
||||
--- gradient-linear-repeat-and-mirror-1 ---
|
||||
// Test repeated gradients.
|
||||
#rect(
|
||||
|
@ -69,7 +69,7 @@
|
||||
dir: ltr,
|
||||
spacing: 2pt,
|
||||
square(width: 20pt, height: 40pt),
|
||||
circle(width: 20%, height: 100pt),
|
||||
circle(width: 20%, height: 40pt),
|
||||
)
|
||||
|
||||
--- square-height-limited-stack ---
|
||||
|