diff options
Diffstat (limited to 'crates/typst-library/src/layout')
22 files changed, 0 insertions, 7927 deletions
diff --git a/crates/typst-library/src/layout/align.rs b/crates/typst-library/src/layout/align.rs deleted file mode 100644 index 9c18266d..00000000 --- a/crates/typst-library/src/layout/align.rs +++ /dev/null @@ -1,46 +0,0 @@ -use crate::prelude::*; - -/// Aligns content horizontally and vertically. -/// -/// # Example -/// ```example -/// #set align(center) -/// -/// Centered text, a sight to see \ -/// In perfect balance, visually \ -/// Not left nor right, it stands alone \ -/// A work of art, a visual throne -/// ``` -#[elem(Show)] -pub struct AlignElem { - /// The [alignment]($alignment) along both axes. - /// - /// ```example - /// #set page(height: 6cm) - /// #set text(lang: "ar") - /// - /// مثال - /// #align( - /// end + horizon, - /// rect(inset: 12pt)[ركن] - /// ) - /// ``` - #[positional] - #[fold] - #[default] - pub alignment: Align, - - /// The content to align. - #[required] - pub body: Content, -} - -impl Show for AlignElem { - #[tracing::instrument(name = "AlignElem::show", skip_all)] - fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { - Ok(self - .body() - .clone() - .styled(Self::set_alignment(self.alignment(styles)))) - } -} diff --git a/crates/typst-library/src/layout/columns.rs b/crates/typst-library/src/layout/columns.rs deleted file mode 100644 index bf111506..00000000 --- a/crates/typst-library/src/layout/columns.rs +++ /dev/null @@ -1,169 +0,0 @@ -use crate::prelude::*; -use crate::text::TextElem; - -/// Separates a region into multiple equally sized columns. -/// -/// The `column` function allows to separate the interior of any container into -/// multiple columns. It will not equalize the height of the columns, instead, -/// the columns will take up the height of their container or the remaining -/// height on the page. The columns function can break across pages if -/// necessary. -/// -/// If you need to insert columns across your whole document, you can use the -/// [`{page}` function's `columns` parameter]($page.columns) instead. -/// -/// # Example -/// ```example -/// = Towards Advanced Deep Learning -/// -/// #box(height: 68pt, -/// columns(2, gutter: 11pt)[ -/// #set par(justify: true) -/// This research was funded by the -/// National Academy of Sciences. -/// NAoS provided support for field -/// tests and interviews with a -/// grant of up to USD 40.000 for a -/// period of 6 months. -/// ] -/// ) -/// -/// In recent years, deep learning has -/// increasingly been used to solve a -/// variety of problems. -/// ``` -#[elem(Layout)] -pub struct ColumnsElem { - /// The number of columns. - #[positional] - #[default(NonZeroUsize::new(2).unwrap())] - pub count: NonZeroUsize, - - /// The size of the gutter space between each column. - #[resolve] - #[default(Ratio::new(0.04).into())] - pub gutter: Rel<Length>, - - /// The content that should be layouted into the columns. - #[required] - pub body: Content, -} - -impl Layout for ColumnsElem { - #[tracing::instrument(name = "ColumnsElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - let body = self.body(); - - // Separating the infinite space into infinite columns does not make - // much sense. - if !regions.size.x.is_finite() { - return body.layout(vt, styles, regions); - } - - // 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; - - let backlog: Vec<_> = std::iter::once(®ions.size.y) - .chain(regions.backlog) - .flat_map(|&height| std::iter::repeat(height).take(columns)) - .skip(1) - .collect(); - - // 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, - }; - - // Layout the children. - let mut frames = body.layout(vt, styles, pod)?.into_iter(); - let mut finished = vec![]; - - let dir = TextElem::dir_in(styles); - let total_regions = (frames.len() as f32 / columns as f32).ceil() as usize; - - // 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(); - - for _ in 0..columns { - let Some(frame) = frames.next() else { break }; - if !regions.expand.y { - output.size_mut().y.set_max(frame.height()); - } - - 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; - } - - finished.push(output); - } - - Ok(Fragment::frames(finished)) - } -} - -/// Forces a column break. -/// -/// The function will behave like a [page break]($pagebreak) when used in a -/// single column layout or the last column on a page. Otherwise, content after -/// the column break will be placed in the next column. -/// -/// # Example -/// ```example -/// #set page(columns: 2) -/// Preliminary findings from our -/// ongoing research project have -/// revealed a hitherto unknown -/// phenomenon of extraordinary -/// significance. -/// -/// #colbreak() -/// Through rigorous experimentation -/// and analysis, we have discovered -/// a hitherto uncharacterized process -/// that defies our current -/// understanding of the fundamental -/// laws of nature. -/// ``` -#[elem(title = "Column Break", Behave)] -pub struct ColbreakElem { - /// If `{true}`, the column break is skipped if the current column is - /// already empty. - #[default(false)] - pub weak: bool, -} - -impl Behave for ColbreakElem { - fn behaviour(&self) -> Behaviour { - if self.weak(StyleChain::default()) { - Behaviour::Weak(1) - } else { - Behaviour::Destructive - } - } -} diff --git a/crates/typst-library/src/layout/container.rs b/crates/typst-library/src/layout/container.rs deleted file mode 100644 index 9268f8df..00000000 --- a/crates/typst-library/src/layout/container.rs +++ /dev/null @@ -1,502 +0,0 @@ -use typst::eval::AutoValue; - -use crate::layout::{Spacing, VElem}; -use crate::prelude::*; - -/// An inline-level container that sizes content. -/// -/// All elements except inline math, text, and boxes are block-level and cannot -/// occur inside of a paragraph. The box function can be used to integrate such -/// elements into a paragraph. Boxes take the size of their contents by default -/// but can also be sized explicitly. -/// -/// # Example -/// ```example -/// Refer to the docs -/// #box( -/// height: 9pt, -/// image("docs.svg") -/// ) -/// for more information. -/// ``` -#[elem(Layout)] -pub struct BoxElem { - /// The width of the box. - /// - /// Boxes can have [fractional]($fraction) widths, as the example below - /// demonstrates. - /// - /// _Note:_ Currently, only boxes and only their widths might be fractionally - /// sized within paragraphs. Support for fractionally sized images, shapes, - /// and more might be added in the future. - /// - /// ```example - /// Line in #box(width: 1fr, line(length: 100%)) between. - /// ``` - pub width: Sizing, - - /// The height of the box. - pub height: Smart<Rel<Length>>, - - /// An amount to shift the box's baseline by. - /// - /// ```example - /// Image: #box(baseline: 40%, image("tiger.jpg", width: 2cm)). - /// ``` - #[resolve] - pub baseline: Rel<Length>, - - /// The box's background color. See the - /// [rectangle's documentation]($rect.fill) for more details. - pub fill: Option<Paint>, - - /// The box's border color. See the - /// [rectangle's documentation]($rect.stroke) for more details. - #[resolve] - #[fold] - pub stroke: Sides<Option<Option<Stroke>>>, - - /// How much to round the box's corners. See the - /// [rectangle's documentation]($rect.radius) for more details. - #[resolve] - #[fold] - pub radius: Corners<Option<Rel<Length>>>, - - /// How much to pad the box's content. - /// - /// _Note:_ When the box contains text, its exact size depends on the - /// current [text edges]($text.top-edge). - /// - /// ```example - /// #rect(inset: 0pt)[Tight] - /// ``` - #[resolve] - #[fold] - pub inset: Sides<Option<Rel<Length>>>, - - /// How much to expand the box's size without affecting the layout. - /// - /// This is useful to prevent padding from affecting line layout. For a - /// generalized version of the example below, see the documentation for the - /// [raw text's block parameter]($raw.block). - /// - /// ```example - /// An inline - /// #box( - /// fill: luma(235), - /// inset: (x: 3pt, y: 0pt), - /// outset: (y: 3pt), - /// radius: 2pt, - /// )[rectangle]. - /// ``` - #[resolve] - #[fold] - pub outset: Sides<Option<Rel<Length>>>, - - /// Whether to clip the content inside the box. - #[default(false)] - pub clip: bool, - - /// The contents of the box. - #[positional] - pub body: Option<Content>, -} - -impl Layout for BoxElem { - #[tracing::instrument(name = "BoxElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - let width = match self.width(styles) { - Sizing::Auto => Smart::Auto, - Sizing::Rel(rel) => Smart::Custom(rel), - Sizing::Fr(_) => Smart::Custom(Ratio::one().into()), - }; - - // Resolve the sizing to a concrete size. - let sizing = Axes::new(width, self.height(styles)); - let expand = sizing.as_ref().map(Smart::is_custom); - let size = sizing - .resolve(styles) - .zip_map(regions.base(), |s, b| s.map(|v| v.relative_to(b))) - .unwrap_or(regions.base()); - - // Apply inset. - let mut body = self.body(styles).unwrap_or_default(); - let inset = self.inset(styles); - if inset.iter().any(|v| !v.is_zero()) { - body = body.padded(inset.map(|side| side.map(Length::from))); - } - - // Select the appropriate base and expansion for the child depending - // on whether it is automatically or relatively sized. - let pod = Regions::one(size, expand); - let mut frame = body.layout(vt, styles, pod)?.into_frame(); - - // Enforce correct size. - *frame.size_mut() = expand.select(size, frame.size()); - - // Apply baseline shift. - let shift = self.baseline(styles).relative_to(frame.height()); - if !shift.is_zero() { - frame.set_baseline(frame.baseline() - shift); - } - - // Prepare fill and stroke. - let fill = self.fill(styles); - let stroke = self.stroke(styles).map(|s| s.map(Stroke::unwrap_or_default)); - - // Clip the contents - if self.clip(styles) { - let outset = self.outset(styles).relative_to(frame.size()); - let size = frame.size() + outset.sum_by_axis(); - let radius = self.radius(styles); - frame.clip(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); - let radius = self.radius(styles); - frame.fill_and_stroke(fill, stroke, outset, radius, self.span()); - } - - // Apply metadata. - frame.meta(styles, false); - frame.set_kind(FrameKind::Hard); - - Ok(Fragment::frame(frame)) - } -} - -/// A block-level container. -/// -/// Such a container can be used to separate content, size it, and give it a -/// background or border. -/// -/// # Examples -/// With a block, you can give a background to content while still allowing it -/// to break across multiple pages. -/// ```example -/// #set page(height: 100pt) -/// #block( -/// fill: luma(230), -/// inset: 8pt, -/// radius: 4pt, -/// lorem(30), -/// ) -/// ``` -/// -/// Blocks are also useful to force elements that would otherwise be inline to -/// become block-level, especially when writing show rules. -/// ```example -/// #show heading: it => it.body -/// = Blockless -/// More text. -/// -/// #show heading: it => block(it.body) -/// = Blocky -/// More text. -/// ``` -#[elem(Layout)] -pub struct BlockElem { - /// The block's width. - /// - /// ```example - /// #set align(center) - /// #block( - /// width: 60%, - /// inset: 8pt, - /// fill: silver, - /// lorem(10), - /// ) - /// ``` - pub width: Smart<Rel<Length>>, - - /// The block's height. When the height is larger than the remaining space - /// on a page and [`breakable`]($block.breakable) is `{true}`, the - /// block will continue on the next page with the remaining height. - /// - /// ```example - /// #set page(height: 80pt) - /// #set align(center) - /// #block( - /// width: 80%, - /// height: 150%, - /// fill: aqua, - /// ) - /// ``` - pub height: Smart<Rel<Length>>, - - /// Whether the block can be broken and continue on the next page. - /// - /// ```example - /// #set page(height: 80pt) - /// The following block will - /// jump to its own page. - /// #block( - /// breakable: false, - /// lorem(15), - /// ) - /// ``` - #[default(true)] - pub breakable: bool, - - /// The block's background color. See the - /// [rectangle's documentation]($rect.fill) for more details. - pub fill: Option<Paint>, - - /// The block's border color. See the - /// [rectangle's documentation]($rect.stroke) for more details. - #[resolve] - #[fold] - pub stroke: Sides<Option<Option<Stroke>>>, - - /// How much to round the block's corners. See the - /// [rectangle's documentation]($rect.radius) for more details. - #[resolve] - #[fold] - pub radius: Corners<Option<Rel<Length>>>, - - /// How much to pad the block's content. See the - /// [box's documentation]($box.inset) for more details. - #[resolve] - #[fold] - pub inset: Sides<Option<Rel<Length>>>, - - /// How much to expand the block's size without affecting the layout. See - /// the [box's documentation]($box.outset) for more details. - #[resolve] - #[fold] - pub outset: Sides<Option<Rel<Length>>>, - - /// The spacing around this block. This is shorthand to set `above` and - /// `below` to the same value. - /// - /// ```example - /// #set align(center) - /// #show math.equation: set block(above: 8pt, below: 16pt) - /// - /// This sum of $x$ and $y$: - /// $ x + y = z $ - /// A second paragraph. - /// ``` - #[external] - #[default(Em::new(1.2).into())] - pub spacing: Spacing, - - /// The spacing between this block and its predecessor. Takes precedence - /// over `spacing`. Can be used in combination with a show rule to adjust - /// the spacing around arbitrary block-level elements. - #[external] - #[default(Em::new(1.2).into())] - pub above: Spacing, - #[internal] - #[parse( - let spacing = args.named("spacing")?; - args.named("above")? - .map(VElem::block_around) - .or_else(|| spacing.map(VElem::block_spacing)) - )] - #[default(VElem::block_spacing(Em::new(1.2).into()))] - pub above: VElem, - - /// The spacing between this block and its successor. Takes precedence - /// over `spacing`. - #[external] - #[default(Em::new(1.2).into())] - pub below: Spacing, - #[internal] - #[parse( - args.named("below")? - .map(VElem::block_around) - .or_else(|| spacing.map(VElem::block_spacing)) - )] - #[default(VElem::block_spacing(Em::new(1.2).into()))] - pub below: VElem, - - /// Whether to clip the content inside the block. - #[default(false)] - pub clip: bool, - - /// The contents of the block. - #[positional] - pub body: Option<Content>, - - /// Whether this block must stick to the following one. - /// - /// Use this to prevent page breaks between e.g. a heading and its body. - #[internal] - #[default(false)] - pub sticky: bool, -} - -impl Layout for BlockElem { - #[tracing::instrument(name = "BlockElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - // Apply inset. - let mut body = self.body(styles).unwrap_or_default(); - let inset = self.inset(styles); - if inset.iter().any(|v| !v.is_zero()) { - body = body.clone().padded(inset.map(|side| side.map(Length::from))); - } - - // Resolve the sizing to a concrete size. - let sizing = Axes::new(self.width(styles), self.height(styles)); - let mut expand = sizing.as_ref().map(Smart::is_custom); - let mut size = sizing - .resolve(styles) - .zip_map(regions.base(), |s, b| s.map(|v| v.relative_to(b))) - .unwrap_or(regions.base()); - - // Layout the child. - let mut frames = if self.breakable(styles) { - // Measure to ensure frames for all regions have the same width. - if sizing.x == Smart::Auto { - let pod = Regions::one(size, Axes::splat(false)); - let frame = body.measure(vt, styles, pod)?.into_frame(); - size.x = frame.width(); - expand.x = true; - } - - let mut pod = regions; - pod.size.x = size.x; - pod.expand = expand; - - if expand.y { - pod.full = size.y; - } - - // Generate backlog for fixed height. - let mut heights = vec![]; - if sizing.y.is_custom() { - let mut remaining = size.y; - for region in regions.iter() { - let limited = region.y.min(remaining); - heights.push(limited); - remaining -= limited; - if Abs::zero().fits(remaining) { - break; - } - } - - if let Some(last) = heights.last_mut() { - *last += remaining; - } - - pod.size.y = heights[0]; - pod.backlog = &heights[1..]; - pod.last = None; - } - - let mut frames = body.layout(vt, styles, pod)?.into_frames(); - for (frame, &height) in frames.iter_mut().zip(&heights) { - *frame.size_mut() = - expand.select(Size::new(size.x, height), frame.size()); - } - frames - } else { - let pod = Regions::one(size, expand); - let mut frames = body.layout(vt, styles, pod)?.into_frames(); - *frames[0].size_mut() = expand.select(size, frames[0].size()); - frames - }; - - // Prepare fill and stroke. - let fill = self.fill(styles); - let stroke = self.stroke(styles).map(|s| s.map(Stroke::unwrap_or_default)); - - // Clip the contents - if self.clip(styles) { - for frame in frames.iter_mut() { - let outset = self.outset(styles).relative_to(frame.size()); - let size = frame.size() + outset.sum_by_axis(); - let radius = self.radius(styles); - frame.clip(clip_rect(size, radius, &stroke)); - } - } - - // Add fill and/or stroke. - if fill.is_some() || stroke.iter().any(Option::is_some) { - let mut skip = false; - if let [first, rest @ ..] = frames.as_slice() { - skip = first.is_empty() && rest.iter().any(|frame| !frame.is_empty()); - } - - let outset = self.outset(styles); - let radius = self.radius(styles); - for frame in frames.iter_mut().skip(skip as usize) { - frame.fill_and_stroke( - fill.clone(), - stroke.clone(), - outset, - radius, - self.span(), - ); - } - } - - // Apply metadata. - for frame in &mut frames { - frame.set_kind(FrameKind::Hard); - frame.meta(styles, false); - } - - Ok(Fragment::frames(frames)) - } -} - -/// Defines how to size a grid cell along an axis. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum Sizing { - /// A track that fits its cell's contents. - Auto, - /// A track size specified in absolute terms and relative to the parent's - /// size. - Rel(Rel<Length>), - /// A track size specified as a fraction of the remaining free space in the - /// parent. - Fr(Fr), -} - -impl Sizing { - /// Whether this is fractional sizing. - pub fn is_fractional(self) -> bool { - matches!(self, Self::Fr(_)) - } -} - -impl Default for Sizing { - fn default() -> Self { - Self::Auto - } -} - -impl<T: Into<Spacing>> From<T> for Sizing { - fn from(spacing: T) -> Self { - match spacing.into() { - Spacing::Rel(rel) => Self::Rel(rel), - Spacing::Fr(fr) => Self::Fr(fr), - } - } -} - -cast! { - Sizing, - self => match self { - Self::Auto => Value::Auto, - Self::Rel(rel) => rel.into_value(), - Self::Fr(fr) => fr.into_value(), - }, - _: AutoValue => Self::Auto, - v: Rel<Length> => Self::Rel(v), - v: Fr => Self::Fr(v), -} diff --git a/crates/typst-library/src/layout/enum.rs b/crates/typst-library/src/layout/enum.rs deleted file mode 100644 index 0c98c18a..00000000 --- a/crates/typst-library/src/layout/enum.rs +++ /dev/null @@ -1,319 +0,0 @@ -use std::str::FromStr; - -use crate::layout::{BlockElem, GridLayouter, ParElem, Sizing, Spacing}; -use crate::meta::{Numbering, NumberingPattern}; -use crate::prelude::*; -use crate::text::TextElem; - -/// A numbered list. -/// -/// Displays a sequence of items vertically and numbers them consecutively. -/// -/// # Example -/// ```example -/// Automatically numbered: -/// + Preparations -/// + Analysis -/// + Conclusions -/// -/// Manually numbered: -/// 2. What is the first step? -/// 5. I am confused. -/// + Moving on ... -/// -/// Multiple lines: -/// + This enum item has multiple -/// lines because the next line -/// is indented. -/// -/// Function call. -/// #enum[First][Second] -/// ``` -/// -/// You can easily switch all your enumerations to a different numbering style -/// with a set rule. -/// ```example -/// #set enum(numbering: "a)") -/// -/// + Starting off ... -/// + Don't forget step two -/// ``` -/// -/// You can also use [`enum.item`]($enum.item) to programmatically customize the -/// number of each item in the enumeration: -/// -/// ```example -/// #enum( -/// enum.item(1)[First step], -/// enum.item(5)[Fifth step], -/// enum.item(10)[Tenth step] -/// ) -/// ``` -/// -/// # Syntax -/// This functions also has dedicated syntax: -/// -/// - Starting a line with a plus sign creates an automatically numbered -/// enumeration item. -/// - Starting a line with a number followed by a dot creates an explicitly -/// numbered enumeration item. -/// -/// 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", Layout)] -pub struct EnumElem { - /// If this is `{false}`, the items are spaced apart with - /// [enum spacing]($enum.spacing). If it is `{true}`, they use normal - /// [leading]($par.leading) instead. This makes the enumeration more - /// compact, which can look better if the items are short. - /// - /// In markup mode, the value of this parameter is determined based on - /// whether items are separated with a blank line. If items directly follow - /// each other, this is set to `{true}`; if items are separated by a blank - /// line, this is set to `{false}`. - /// - /// ```example - /// + If an enum has a lot of text, and - /// maybe other inline content, it - /// should not be tight anymore. - /// - /// + To make an enum wide, simply - /// insert a blank line between the - /// items. - /// ``` - #[default(true)] - pub tight: bool, - - /// How to number the enumeration. Accepts a - /// [numbering pattern or function]($numbering). - /// - /// If the numbering pattern contains multiple counting symbols, they apply - /// to nested enums. If given a function, the function receives one argument - /// if `full` is `{false}` and multiple arguments if `full` is `{true}`. - /// - /// ```example - /// #set enum(numbering: "1.a)") - /// + Different - /// + Numbering - /// + Nested - /// + Items - /// + Style - /// - /// #set enum(numbering: n => super[#n]) - /// + Superscript - /// + Numbering! - /// ``` - #[default(Numbering::Pattern(NumberingPattern::from_str("1.").unwrap()))] - #[borrowed] - pub numbering: Numbering, - - /// Which number to start the enumeration with. - /// - /// ```example - /// #enum( - /// start: 3, - /// [Skipping], - /// [Ahead], - /// ) - /// ``` - #[default(1)] - pub start: usize, - - /// Whether to display the full numbering, including the numbers of - /// all parent enumerations. - /// - /// - /// ```example - /// #set enum(numbering: "1.a)", full: true) - /// + Cook - /// + Heat water - /// + Add integredients - /// + Eat - /// ``` - #[default(false)] - pub full: bool, - - /// The indentation of each item. - #[resolve] - pub indent: Length, - - /// The space between the numbering and the body of each item. - #[resolve] - #[default(Em::new(0.5).into())] - pub body_indent: Length, - - /// The spacing between the items of a wide (non-tight) enumeration. - /// - /// If set to `{auto}`, uses the spacing [below blocks]($block.below). - pub spacing: Smart<Spacing>, - - /// The horizontal alignment that enum numbers should have. - /// - /// By default, this is set to `{end}`, which aligns enum numbers - /// towards end of the current text direction (in left-to-right script, - /// for example, this is the same as `{right}`). The choice of `{end}` - /// for horizontal alignment of enum numbers is usually preferred over - /// `{start}`, as numbers then grow away from the text instead of towards - /// it, avoiding certain visual issues. This option lets you override this - /// behavior, however. - /// - /// ````example - /// #set enum(number-align: start) - /// - /// Here are some powers of two: - /// 1. One - /// 2. Two - /// 4. Four - /// 8. Eight - /// 16. Sixteen - /// 32. Thirty two - /// ```` - #[default(HAlign::End)] - pub number_align: HAlign, - - /// The numbered list's items. - /// - /// When using the enum syntax, adjacent items are automatically collected - /// into enumerations, even through constructs like for loops. - /// - /// ```example - /// #for phase in ( - /// "Launch", - /// "Orbit", - /// "Descent", - /// ) [+ #phase] - /// ``` - #[variadic] - pub children: Vec<EnumItem>, - - /// The numbers of parent items. - #[internal] - #[fold] - parents: Parent, -} - -#[scope] -impl EnumElem { - #[elem] - type EnumItem; -} - -impl Layout for EnumElem { - #[tracing::instrument(name = "EnumElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - 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()) - }; - - let mut cells = vec![]; - let mut number = self.start(styles); - let mut parents = self.parents(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) + VAlign::Top; - - for item in self.children() { - number = item.number(styles).unwrap_or(number); - - let resolved = if full { - parents.push(number); - let content = numbering.apply_vt(vt, &parents)?.display(); - parents.pop(); - content - } else { - match numbering { - Numbering::Pattern(pattern) => { - TextElem::packed(pattern.apply_kth(parents.len(), number)) - } - other => other.apply_vt(vt, &[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(Content::empty()); - cells.push(resolved); - cells.push(Content::empty()); - cells.push(item.body().clone().styled(Self::set_parents(Parent(number)))); - number = number.saturating_add(1); - } - - let layouter = GridLayouter::new( - Axes::with_x(&[ - Sizing::Rel(indent.into()), - Sizing::Auto, - Sizing::Rel(body_indent.into()), - Sizing::Auto, - ]), - Axes::with_y(&[gutter.into()]), - &cells, - regions, - styles, - self.span(), - ); - - Ok(layouter.layout(vt)?.fragment) - } -} - -/// An enumeration item. -#[elem(name = "item", title = "Numbered List Item")] -pub struct EnumItem { - /// The item's number. - #[positional] - pub number: Option<usize>, - - /// The item's body. - #[required] - pub body: Content, -} - -cast! { - EnumItem, - array: Array => { - let mut iter = array.into_iter(); - let (number, body) = match (iter.next(), iter.next(), iter.next()) { - (Some(a), Some(b), None) => (a.cast()?, b.cast()?), - _ => bail!("array must contain exactly two entries"), - }; - Self::new(body).with_number(number) - }, - v: Content => v.to::<Self>().cloned().unwrap_or_else(|| Self::new(v.clone())), -} - -#[derive(Debug, Clone, Copy, PartialEq, Hash)] -struct Parent(usize); - -cast! { - Parent, - self => self.0.into_value(), - v: usize => Self(v), -} - -impl Fold for Parent { - type Output = Vec<usize>; - - fn fold(self, mut outer: Self::Output) -> Self::Output { - outer.push(self.0); - outer - } -} diff --git a/crates/typst-library/src/layout/flow.rs b/crates/typst-library/src/layout/flow.rs deleted file mode 100644 index 17a39254..00000000 --- a/crates/typst-library/src/layout/flow.rs +++ /dev/null @@ -1,714 +0,0 @@ -use std::mem; - -use comemo::Prehashed; - -use crate::layout::{ - AlignElem, BlockElem, ColbreakElem, ColumnsElem, ParElem, PlaceElem, Spacing, VElem, -}; -use crate::meta::{FootnoteElem, FootnoteEntry}; -use crate::prelude::*; -use crate::visualize::{ - CircleElem, EllipseElem, ImageElem, LineElem, PathElem, PolygonElem, RectElem, - SquareElem, -}; - -/// Arranges spacing, paragraphs and block-level elements into a flow. -/// -/// This element is responsible for layouting both the top-level content flow -/// and the contents of boxes. -#[elem(Layout)] -pub struct FlowElem { - /// The children that will be arranges into a flow. - #[variadic] - pub children: Vec<Prehashed<Content>>, -} - -impl Layout for FlowElem { - #[tracing::instrument(name = "FlowElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - if !regions.size.x.is_finite() && regions.expand.x { - bail!(error!(self.span(), "cannot expand into infinite width")); - } - if !regions.size.y.is_finite() && regions.expand.y { - bail!(error!(self.span(), "cannot expand into infinite height")); - } - let mut layouter = FlowLayouter::new(regions, styles); - - for mut child in self.children().iter().map(|c| &**c) { - let outer = styles; - let mut styles = styles; - if let Some((elem, map)) = child.to_styled() { - child = elem; - styles = outer.chain(map); - } - - if let Some(elem) = child.to::<VElem>() { - layouter.layout_spacing(vt, elem, styles)?; - } else if let Some(elem) = child.to::<ParElem>() { - layouter.layout_par(vt, elem, styles)?; - } else if child.is::<LineElem>() - || child.is::<RectElem>() - || child.is::<SquareElem>() - || child.is::<EllipseElem>() - || child.is::<CircleElem>() - || child.is::<ImageElem>() - || child.is::<PolygonElem>() - || child.is::<PathElem>() - { - let layoutable = child.with::<dyn Layout>().unwrap(); - layouter.layout_single(vt, layoutable, styles)?; - } else if child.is::<MetaElem>() { - let mut frame = Frame::soft(Size::zero()); - frame.meta(styles, true); - layouter.items.push(FlowItem::Frame { - frame, - align: Axes::splat(FixedAlign::Start), - sticky: true, - movable: false, - }); - } else if let Some(placed) = child.to::<PlaceElem>() { - layouter.layout_placed(vt, placed, styles)?; - } else if child.can::<dyn Layout>() { - layouter.layout_multiple(vt, child, styles)?; - } else if child.is::<ColbreakElem>() { - if !layouter.regions.backlog.is_empty() || layouter.regions.last.is_some() - { - layouter.finish_region(vt)?; - } - } else { - bail!(child.span(), "unexpected flow child"); - } - } - - layouter.finish(vt) - } -} - -/// Performs flow layout. -struct FlowLayouter<'a> { - /// Whether this is the root flow. - root: bool, - /// The regions to layout children into. - regions: Regions<'a>, - /// The shared styles. - styles: StyleChain<'a>, - /// Whether the flow should expand to fill the region. - expand: Axes<bool>, - /// The initial size of `regions.size` that was available before we started - /// subtracting. - initial: Size, - /// Whether the last block was a paragraph. - last_was_par: bool, - /// Spacing and layouted blocks for the current region. - items: Vec<FlowItem>, - /// A queue of floats. - pending_floats: Vec<FlowItem>, - /// Whether we have any footnotes in the current region. - has_footnotes: bool, - /// Footnote configuration. - footnote_config: FootnoteConfig, - /// Finished frames for previous regions. - finished: Vec<Frame>, -} - -/// Cached footnote configuration. -struct FootnoteConfig { - separator: Content, - clearance: Abs, - gap: Abs, -} - -/// A prepared item in a flow layout. -#[derive(Debug)] -enum FlowItem { - /// Spacing between other items and whether it is weak. - Absolute(Abs, bool), - /// Fractional spacing between other items. - Fractional(Fr), - /// A frame for a layouted block, how to align it, whether it sticks to the - /// item after it (for orphan prevention), and whether it is movable - /// (to keep it together with its footnotes). - Frame { frame: Frame, align: Axes<FixedAlign>, sticky: bool, movable: bool }, - /// An absolutely placed frame. - Placed { - frame: Frame, - x_align: FixedAlign, - y_align: Smart<Option<FixedAlign>>, - delta: Axes<Rel<Abs>>, - float: bool, - clearance: Abs, - }, - /// A footnote frame (can also be the separator). - Footnote(Frame), -} - -impl FlowItem { - /// The inherent height of the item. - fn height(&self) -> Abs { - match self { - Self::Absolute(v, _) => *v, - Self::Fractional(_) | Self::Placed { .. } => Abs::zero(), - Self::Frame { frame, .. } | Self::Footnote(frame) => frame.height(), - } - } -} - -impl<'a> FlowLayouter<'a> { - /// Create a new flow layouter. - fn new(mut regions: Regions<'a>, styles: StyleChain<'a>) -> Self { - let expand = regions.expand; - - // Disable vertical expansion & root for children. - regions.expand.y = false; - let root = mem::replace(&mut regions.root, false); - - Self { - root, - regions, - styles, - expand, - initial: regions.size, - last_was_par: false, - items: vec![], - pending_floats: vec![], - has_footnotes: false, - footnote_config: FootnoteConfig { - separator: FootnoteEntry::separator_in(styles), - clearance: FootnoteEntry::clearance_in(styles), - gap: FootnoteEntry::gap_in(styles), - }, - finished: vec![], - } - } - - /// Layout vertical spacing. - #[tracing::instrument(name = "FlowLayouter::layout_spacing", skip_all)] - fn layout_spacing( - &mut self, - vt: &mut Vt, - v: &VElem, - styles: StyleChain, - ) -> SourceResult<()> { - self.layout_item( - vt, - match v.amount() { - Spacing::Rel(rel) => FlowItem::Absolute( - rel.resolve(styles).relative_to(self.initial.y), - v.weakness(styles) > 0, - ), - Spacing::Fr(fr) => FlowItem::Fractional(*fr), - }, - ) - } - - /// Layout a paragraph. - #[tracing::instrument(name = "FlowLayouter::layout_par", skip_all)] - fn layout_par( - &mut self, - vt: &mut Vt, - par: &ParElem, - styles: StyleChain, - ) -> SourceResult<()> { - let align = AlignElem::alignment_in(styles).resolve(styles); - let leading = ParElem::leading_in(styles); - let consecutive = self.last_was_par; - let lines = par - .layout(vt, styles, consecutive, self.regions.base(), self.regions.expand.x)? - .into_frames(); - - let mut sticky = self.items.len(); - for (i, item) in self.items.iter().enumerate().rev() { - match *item { - FlowItem::Absolute(_, _) => {} - FlowItem::Frame { sticky: true, .. } => sticky = i, - _ => break, - } - } - - if let Some(first) = lines.first() { - if !self.regions.size.y.fits(first.height()) && !self.regions.in_last() { - let carry: Vec<_> = self.items.drain(sticky..).collect(); - self.finish_region(vt)?; - for item in carry { - self.layout_item(vt, item)?; - } - } - } - - for (i, frame) in lines.into_iter().enumerate() { - if i > 0 { - self.layout_item(vt, FlowItem::Absolute(leading, true))?; - } - - self.layout_item( - vt, - FlowItem::Frame { frame, align, sticky: false, movable: true }, - )?; - } - - self.last_was_par = true; - Ok(()) - } - - /// Layout into a single region. - #[tracing::instrument(name = "FlowLayouter::layout_single", skip_all)] - fn layout_single( - &mut self, - vt: &mut Vt, - content: &dyn Layout, - 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 frame = content.layout(vt, styles, pod)?.into_frame(); - self.layout_item(vt, FlowItem::Frame { frame, align, sticky, movable: true })?; - self.last_was_par = false; - Ok(()) - } - - /// Layout a placed element. - fn layout_placed( - &mut self, - vt: &mut Vt, - placed: &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(FixedAlign::Center, |align| { - align.x().unwrap_or_default().resolve(styles) - }); - let y_align = alignment.map(|align| align.y().map(VAlign::fix)); - let frame = placed.layout(vt, styles, self.regions)?.into_frame(); - let item = FlowItem::Placed { frame, x_align, y_align, delta, float, clearance }; - self.layout_item(vt, item) - } - - /// Layout into multiple regions. - fn layout_multiple( - &mut self, - vt: &mut Vt, - block: &Content, - styles: StyleChain, - ) -> SourceResult<()> { - // Temporarily delegerate rootness to the columns. - let is_root = self.root; - if is_root && block.is::<ColumnsElem>() { - 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(vt)?; - } - - // How to align the block. - let align = if let Some(align) = block.to::<AlignElem>() { - align.alignment(styles) - } else if let Some((_, local)) = block.to_styled() { - AlignElem::alignment_in(styles.chain(local)) - } else { - AlignElem::alignment_in(styles) - } - .resolve(styles); - - // Layout the block itself. - let sticky = BlockElem::sticky_in(styles); - let fragment = block.layout(vt, styles, self.regions)?; - - for (i, frame) in fragment.into_iter().enumerate() { - // Find footnotes in the frame. - if self.root { - find_footnotes(&mut notes, &frame); - } - - if i > 0 { - self.finish_region(vt)?; - } - - let item = FlowItem::Frame { frame, align, sticky, movable: false }; - self.layout_item(vt, item)?; - } - - self.try_handle_footnotes(vt, notes)?; - - self.root = is_root; - self.regions.root = false; - self.last_was_par = false; - - Ok(()) - } - - /// Layout a finished frame. - #[tracing::instrument(name = "FlowLayouter::layout_item", skip_all)] - fn layout_item(&mut self, vt: &mut Vt, mut item: FlowItem) -> SourceResult<()> { - match item { - FlowItem::Absolute(v, weak) => { - if weak - && !self - .items - .iter() - .any(|item| matches!(item, FlowItem::Frame { .. })) - { - return Ok(()); - } - self.regions.size.y -= v - } - FlowItem::Fractional(_) => {} - FlowItem::Frame { ref frame, movable, .. } => { - let height = frame.height(); - if !self.regions.size.y.fits(height) && !self.regions.in_last() { - self.finish_region(vt)?; - } - - self.regions.size.y -= height; - if self.root && movable { - let mut notes = Vec::new(); - find_footnotes(&mut notes, frame); - self.items.push(item); - if !self.handle_footnotes(vt, &mut notes, true, false)? { - let item = self.items.pop(); - self.finish_region(vt)?; - self.items.extend(item); - self.regions.size.y -= height; - self.handle_footnotes(vt, &mut notes, true, true)?; - } - return Ok(()); - } - } - FlowItem::Placed { float: false, .. } => {} - FlowItem::Placed { - ref mut frame, - ref mut y_align, - float: true, - clearance, - .. - } => { - // If the float doesn't fit, queue it for the next region. - if !self.regions.size.y.fits(frame.height() + clearance) - && !self.regions.in_last() - { - self.pending_floats.push(item); - return Ok(()); - } - - // Select the closer placement, top or bottom. - if y_align.is_auto() { - let ratio = (self.regions.size.y - - (frame.height() + clearance) / 2.0) - / self.regions.full; - let better_align = - if ratio <= 0.5 { FixedAlign::End } else { FixedAlign::Start }; - *y_align = Smart::Custom(Some(better_align)); - } - - // Add some clearance so that the float doesn't touch the main - // content. - frame.size_mut().y += clearance; - if *y_align == Smart::Custom(Some(FixedAlign::End)) { - frame.translate(Point::with_y(clearance)); - } - - self.regions.size.y -= frame.height(); - - // Find footnotes in the frame. - if self.root { - let mut notes = vec![]; - find_footnotes(&mut notes, frame); - self.try_handle_footnotes(vt, notes)?; - } - } - FlowItem::Footnote(_) => {} - } - - self.items.push(item); - Ok(()) - } - - /// Finish the frame for one region. - fn finish_region(&mut self, vt: &mut Vt) -> SourceResult<()> { - // Trim weak spacing. - while self - .items - .last() - .map_or(false, |item| matches!(item, FlowItem::Absolute(_, true))) - { - self.items.pop(); - } - - // Determine the used size. - let mut fr = Fr::zero(); - let mut used = Size::zero(); - let mut footnote_height = Abs::zero(); - let mut float_top_height = Abs::zero(); - let mut float_bottom_height = Abs::zero(); - let mut first_footnote = true; - for item in &self.items { - match item { - FlowItem::Absolute(v, _) => used.y += *v, - FlowItem::Fractional(v) => fr += *v, - FlowItem::Frame { frame, .. } => { - used.y += frame.height(); - used.x.set_max(frame.width()); - } - FlowItem::Placed { float: false, .. } => {} - FlowItem::Placed { frame, float: true, y_align, .. } => match y_align { - Smart::Custom(Some(FixedAlign::Start)) => { - float_top_height += frame.height() - } - Smart::Custom(Some(FixedAlign::End)) => { - float_bottom_height += frame.height() - } - _ => {} - }, - FlowItem::Footnote(frame) => { - footnote_height += frame.height(); - if !first_footnote { - footnote_height += self.footnote_config.gap; - } - first_footnote = false; - used.x.set_max(frame.width()); - } - } - } - used.y += footnote_height + float_top_height + float_bottom_height; - - // Determine the size of the flow in this region depending on whether - // the region expands. Also account for fractional spacing and - // footnotes. - let mut size = self.expand.select(self.initial, used).min(self.initial); - if (fr.get() > 0.0 || self.has_footnotes) && self.initial.y.is_finite() { - size.y = self.initial.y; - } - - let mut output = Frame::soft(size); - let mut ruler = FixedAlign::Start; - let mut float_top_offset = Abs::zero(); - let mut offset = float_top_height; - let mut float_bottom_offset = Abs::zero(); - let mut footnote_offset = Abs::zero(); - - // Place all frames. - for item in self.items.drain(..) { - match item { - FlowItem::Absolute(v, _) => { - offset += v; - } - FlowItem::Fractional(v) => { - let remaining = self.initial.y - used.y; - offset += v.share(fr, remaining); - } - FlowItem::Frame { frame, align, .. } => { - ruler = ruler.max(align.y); - let x = align.x.position(size.x - frame.width()); - let y = offset + ruler.position(size.y - used.y); - let pos = Point::new(x, y); - offset += frame.height(); - output.push_frame(pos, frame); - } - FlowItem::Placed { frame, x_align, y_align, delta, float, .. } => { - let x = x_align.position(size.x - frame.width()); - let y = if float { - match y_align { - Smart::Custom(Some(FixedAlign::Start)) => { - let y = float_top_offset; - float_top_offset += frame.height(); - y - } - Smart::Custom(Some(FixedAlign::End)) => { - let y = size.y - footnote_height - float_bottom_height - + float_bottom_offset; - float_bottom_offset += frame.height(); - y - } - _ => unreachable!("float must be y aligned"), - } - } else { - match y_align { - Smart::Custom(Some(align)) => { - align.position(size.y - frame.height()) - } - _ => offset + ruler.position(size.y - used.y), - } - }; - - let pos = Point::new(x, y) - + delta.zip_map(size, Rel::relative_to).to_point(); - - output.push_frame(pos, frame); - } - FlowItem::Footnote(frame) => { - let y = size.y - footnote_height + footnote_offset; - footnote_offset += frame.height() + self.footnote_config.gap; - output.push_frame(Point::with_y(y), frame); - } - } - } - - // Advance to the next region. - self.finished.push(output); - self.regions.next(); - self.initial = self.regions.size; - self.has_footnotes = false; - - // Try to place floats. - for item in mem::take(&mut self.pending_floats) { - self.layout_item(vt, item)?; - } - - Ok(()) - } - - /// Finish layouting and return the resulting fragment. - fn finish(mut self, vt: &mut Vt) -> SourceResult<Fragment> { - if self.expand.y { - while !self.regions.backlog.is_empty() { - self.finish_region(vt)?; - } - } - - self.finish_region(vt)?; - while !self.items.is_empty() { - self.finish_region(vt)?; - } - - Ok(Fragment::frames(self.finished)) - } -} - -impl FlowLayouter<'_> { - fn try_handle_footnotes( - &mut self, - vt: &mut Vt, - mut notes: Vec<FootnoteElem>, - ) -> SourceResult<()> { - if self.root && !self.handle_footnotes(vt, &mut notes, false, false)? { - self.finish_region(vt)?; - self.handle_footnotes(vt, &mut notes, false, true)?; - } - Ok(()) - } - - /// Processes all footnotes in the frame. - #[tracing::instrument(skip_all)] - fn handle_footnotes( - &mut self, - vt: &mut Vt, - notes: &mut Vec<FootnoteElem>, - movable: bool, - force: bool, - ) -> SourceResult<bool> { - let items_len = self.items.len(); - let notes_len = notes.len(); - - // Process footnotes one at a time. - let mut k = 0; - while k < notes.len() { - if notes[k].is_ref() { - k += 1; - continue; - } - - if !self.has_footnotes { - self.layout_footnote_separator(vt)?; - } - - self.regions.size.y -= self.footnote_config.gap; - let checkpoint = vt.locator.clone(); - let frames = FootnoteEntry::new(notes[k].clone()) - .pack() - .layout(vt, self.styles, self.regions.with_root(false))? - .into_frames(); - - // If the entries didn't fit, abort (to keep footnote and entry - // together). - if !force - && (k == 0 || movable) - && frames.first().map_or(false, Frame::is_empty) - { - // Remove existing footnotes attempts because we need to - // move the item to the next page. - notes.truncate(notes_len); - - // Undo region modifications. - for item in self.items.drain(items_len..) { - self.regions.size.y -= item.height(); - } - - // Undo Vt modifications. - *vt.locator = checkpoint; - - return Ok(false); - } - - let prev = notes.len(); - for (i, frame) in frames.into_iter().enumerate() { - find_footnotes(notes, &frame); - if i > 0 { - self.finish_region(vt)?; - self.layout_footnote_separator(vt)?; - self.regions.size.y -= self.footnote_config.gap; - } - self.regions.size.y -= frame.height(); - self.items.push(FlowItem::Footnote(frame)); - } - - k += 1; - - // Process the nested notes before dealing with further top-level - // notes. - let nested = notes.len() - prev; - if nested > 0 { - notes[k..].rotate_right(nested); - } - } - - Ok(true) - } - - /// Layout and save the footnote separator, typically a line. - #[tracing::instrument(skip_all)] - fn layout_footnote_separator(&mut self, vt: &mut Vt) -> SourceResult<()> { - let expand = Axes::new(self.regions.expand.x, false); - let pod = Regions::one(self.regions.base(), expand); - let separator = &self.footnote_config.separator; - - let mut frame = separator.layout(vt, self.styles, pod)?.into_frame(); - frame.size_mut().y += self.footnote_config.clearance; - frame.translate(Point::with_y(self.footnote_config.clearance)); - - self.has_footnotes = true; - self.regions.size.y -= frame.height(); - self.items.push(FlowItem::Footnote(frame)); - - Ok(()) - } -} - -/// Finds all footnotes in the frame. -#[tracing::instrument(skip_all)] -fn find_footnotes(notes: &mut Vec<FootnoteElem>, frame: &Frame) { - for (_, item) in frame.items() { - match item { - FrameItem::Group(group) => find_footnotes(notes, &group.frame), - FrameItem::Meta(Meta::Elem(content), _) - if !notes.iter().any(|note| note.location() == content.location()) => - { - let Some(footnote) = content.to::<FootnoteElem>() else { continue }; - notes.push(footnote.clone()); - } - _ => {} - } - } -} diff --git a/crates/typst-library/src/layout/fragment.rs b/crates/typst-library/src/layout/fragment.rs deleted file mode 100644 index 3550df2a..00000000 --- a/crates/typst-library/src/layout/fragment.rs +++ /dev/null @@ -1,87 +0,0 @@ -use crate::prelude::*; - -/// A partial layout result. -#[derive(Clone)] -pub struct Fragment(Vec<Frame>); - -impl Fragment { - /// Create a fragment from a single frame. - pub fn frame(frame: Frame) -> Self { - Self(vec![frame]) - } - - /// Create a fragment from multiple frames. - pub fn frames(frames: Vec<Frame>) -> Self { - Self(frames) - } - - /// Return `true` if the length is 0. - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - - /// The number of frames in the fragment. - pub fn len(&self) -> usize { - self.0.len() - } - - /// Extract the first and only frame. - /// - /// Panics if there are multiple frames. - #[track_caller] - pub fn into_frame(self) -> Frame { - assert_eq!(self.0.len(), 1, "expected exactly one frame"); - self.0.into_iter().next().unwrap() - } - - /// Extract the frames. - pub fn into_frames(self) -> Vec<Frame> { - self.0 - } - - /// Iterate over the contained frames. - pub fn iter(&self) -> std::slice::Iter<Frame> { - self.0.iter() - } - - /// Iterate over the contained frames. - pub fn iter_mut(&mut self) -> std::slice::IterMut<Frame> { - self.0.iter_mut() - } -} - -impl Debug for Fragment { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self.0.as_slice() { - [frame] => frame.fmt(f), - frames => frames.fmt(f), - } - } -} - -impl IntoIterator for Fragment { - type Item = Frame; - type IntoIter = std::vec::IntoIter<Frame>; - - fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() - } -} - -impl<'a> IntoIterator for &'a Fragment { - type Item = &'a Frame; - type IntoIter = std::slice::Iter<'a, Frame>; - - fn into_iter(self) -> Self::IntoIter { - self.0.iter() - } -} - -impl<'a> IntoIterator for &'a mut Fragment { - type Item = &'a mut Frame; - type IntoIter = std::slice::IterMut<'a, Frame>; - - fn into_iter(self) -> Self::IntoIter { - self.0.iter_mut() - } -} diff --git a/crates/typst-library/src/layout/grid.rs b/crates/typst-library/src/layout/grid.rs deleted file mode 100644 index 1cde6564..00000000 --- a/crates/typst-library/src/layout/grid.rs +++ /dev/null @@ -1,734 +0,0 @@ -use smallvec::{smallvec, SmallVec}; - -use crate::layout::Sizing; -use crate::prelude::*; -use crate::text::TextElem; - -/// Arranges content in a grid. -/// -/// The grid element allows you to arrange content in a grid. You can define the -/// number of rows and columns, as well as the size of the gutters between them. -/// There are multiple sizing modes for columns and rows that can be used to -/// create complex layouts. -/// -/// The sizing of the grid is determined by the track sizes specified in the -/// arguments. Because each of the sizing parameters accepts the same values, we -/// will explain them just once, here. Each sizing argument accepts an array of -/// individual track sizes. A track size is either: -/// -/// - `{auto}`: The track will be sized to fit its contents. It will be at most -/// as large as the remaining space. If there is more than one `{auto}` track -/// which, and together they claim more than the available space, the `{auto}` -/// tracks will fairly distribute the available space among themselves. -/// -/// - A fixed or relative length (e.g. `{10pt}` or `{20% - 1cm}`): The track -/// will be exactly of this size. -/// -/// - A fractional length (e.g. `{1fr}`): Once all other tracks have been sized, -/// the remaining space will be divided among the fractional tracks according -/// to their fractions. For example, if there are two fractional tracks, each -/// with a fraction of `{1fr}`, they will each take up half of the remaining -/// space. -/// -/// To specify a single track, the array can be omitted in favor of a single -/// value. To specify multiple `{auto}` tracks, enter the number of tracks -/// instead of an array. For example, `columns:` `{3}` is equivalent to -/// `columns:` `{(auto, auto, auto)}`. -/// -/// # Examples -/// The example below demonstrates the different track sizing options. -/// -/// ```example -/// // We use `rect` to emphasize the -/// // area of cells. -/// #set rect( -/// inset: 8pt, -/// fill: rgb("e4e5ea"), -/// width: 100%, -/// ) -/// -/// #grid( -/// columns: (60pt, 1fr, 2fr), -/// rows: (auto, 60pt), -/// gutter: 3pt, -/// rect[Fixed width, auto height], -/// rect[1/3 of the remains], -/// rect[2/3 of the remains], -/// rect(height: 100%)[Fixed height], -/// image("tiger.jpg", height: 100%), -/// image("tiger.jpg", height: 100%), -/// ) -/// ``` -/// -/// You can also [spread]($arguments/#spreading) an array of strings or content -/// into a grid to populate its cells. -/// -/// ```example -/// #grid( -/// columns: 5, -/// gutter: 5pt, -/// ..range(25).map(str) -/// ) -/// ``` -#[elem(Layout)] -pub struct GridElem { - /// The column sizes. - /// - /// Either specify a track size array or provide an integer to create a grid - /// with that many `{auto}`-sized columns. Note that opposed to rows and - /// gutters, providing a single track size will only ever create a single - /// column. - #[borrowed] - pub columns: TrackSizings, - - /// The row sizes. - /// - /// If there are more cells than fit the defined rows, the last row is - /// repeated until there are no more cells. - #[borrowed] - pub rows: TrackSizings, - - /// The gaps between rows & columns. - /// - /// If there are more gutters than defined sizes, the last gutter is repeated. - #[external] - pub gutter: TrackSizings, - - /// The gaps between columns. Takes precedence over `gutter`. - #[parse( - let gutter = args.named("gutter")?; - args.named("column-gutter")?.or_else(|| gutter.clone()) - )] - #[borrowed] - pub column_gutter: TrackSizings, - - /// The gaps between rows. Takes precedence over `gutter`. - #[parse(args.named("row-gutter")?.or_else(|| gutter.clone()))] - #[borrowed] - pub row_gutter: TrackSizings, - - /// The contents of the grid cells. - /// - /// The cells are populated in row-major order. - #[variadic] - pub children: Vec<Content>, -} - -impl Layout for GridElem { - #[tracing::instrument(name = "GridElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - let columns = self.columns(styles); - let rows = self.rows(styles); - let column_gutter = self.column_gutter(styles); - let row_gutter = self.row_gutter(styles); - - // Prepare grid layout by unifying content and gutter tracks. - let layouter = GridLayouter::new( - Axes::new(&columns.0, &rows.0), - Axes::new(&column_gutter.0, &row_gutter.0), - &self.children, - regions, - styles, - self.span(), - ); - - // Measure the columns and layout the grid row-by-row. - Ok(layouter.layout(vt)?.fragment) - } -} - -/// Track sizing definitions. -#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] -pub struct TrackSizings(pub SmallVec<[Sizing; 4]>); - -cast! { - TrackSizings, - self => self.0.into_value(), - sizing: Sizing => Self(smallvec![sizing]), - count: NonZeroUsize => Self(smallvec![Sizing::Auto; count.get()]), - values: Array => Self(values.into_iter().map(Value::cast).collect::<StrResult<_>>()?), -} - -/// Performs grid layout. -pub struct GridLayouter<'a> { - /// The grid cells. - cells: &'a [Content], - /// Whether this is an RTL grid. - is_rtl: bool, - /// Whether this grid has gutters. - has_gutter: bool, - /// The column tracks including gutter tracks. - cols: Vec<Sizing>, - /// The row tracks including gutter tracks. - rows: Vec<Sizing>, - /// The regions to layout children into. - regions: Regions<'a>, - /// The inherited styles. - styles: StyleChain<'a>, - /// Resolved column sizes. - rcols: Vec<Abs>, - /// The sum of `rcols`. - width: Abs, - /// Resolve row sizes, by region. - rrows: Vec<Vec<RowPiece>>, - /// Rows in the current region. - lrows: Vec<Row>, - /// The initial size of the current region before we started subtracting. - initial: Size, - /// Frames for finished regions. - finished: Vec<Frame>, - /// The span of the grid element. - span: Span, -} - -/// The resulting sizes of columns and rows in a grid. -#[derive(Debug)] -pub struct GridLayout { - /// The fragment. - pub fragment: Fragment, - /// The column widths. - pub cols: Vec<Abs>, - /// The heights of the resulting rows segments, by region. - pub rows: Vec<Vec<RowPiece>>, -} - -/// Details about a resulting row piece. -#[derive(Debug)] -pub struct RowPiece { - /// The height of the segment. - pub height: Abs, - /// The index of the row. - pub y: usize, -} - -/// Produced by initial row layout, auto and relative rows are already finished, -/// fractional rows not yet. -enum Row { - /// Finished row frame of auto or relative row with y index. - Frame(Frame, usize), - /// Fractional row with y index. - Fr(Fr, usize), -} - -impl<'a> GridLayouter<'a> { - /// Create a new grid layouter. - /// - /// This prepares grid layout by unifying content and gutter tracks. - pub fn new( - tracks: Axes<&[Sizing]>, - gutter: Axes<&[Sizing]>, - cells: &'a [Content], - regions: Regions<'a>, - styles: StyleChain<'a>, - span: Span, - ) -> Self { - let mut cols = vec![]; - let mut rows = vec![]; - - // Number of content columns: Always at least one. - let c = tracks.x.len().max(1); - - // Number of content rows: At least as many as given, but also at least - // as many as needed to place each item. - let r = { - let len = cells.len(); - let given = tracks.y.len(); - let needed = len / c + (len % c).clamp(0, 1); - given.max(needed) - }; - - let has_gutter = gutter.any(|tracks| !tracks.is_empty()); - let auto = Sizing::Auto; - let zero = Sizing::Rel(Rel::zero()); - let get_or = |tracks: &[_], idx, default| { - tracks.get(idx).or(tracks.last()).copied().unwrap_or(default) - }; - - // Collect content and gutter columns. - for x in 0..c { - cols.push(get_or(tracks.x, x, auto)); - if has_gutter { - cols.push(get_or(gutter.x, x, zero)); - } - } - - // Collect content and gutter rows. - for y in 0..r { - rows.push(get_or(tracks.y, y, auto)); - if has_gutter { - rows.push(get_or(gutter.y, y, zero)); - } - } - - // Remove superfluous gutter tracks. - if has_gutter { - cols.pop(); - rows.pop(); - } - - // Reverse for RTL. - let is_rtl = TextElem::dir_in(styles) == Dir::RTL; - if is_rtl { - cols.reverse(); - } - - // We use these regions for auto row measurement. Since at that moment, - // columns are already sized, we can enable horizontal expansion. - let mut regions = regions; - regions.expand = Axes::new(true, false); - - Self { - cells, - is_rtl, - has_gutter, - rows, - regions, - styles, - rcols: vec![Abs::zero(); cols.len()], - cols, - width: Abs::zero(), - rrows: vec![], - lrows: vec![], - initial: regions.size, - finished: vec![], - span, - } - } - - /// Determines the columns sizes and then layouts the grid row-by-row. - pub fn layout(mut self, vt: &mut Vt) -> SourceResult<GridLayout> { - self.measure_columns(vt)?; - - for y in 0..self.rows.len() { - // Skip to next region if current one is full, but only for content - // rows, not for gutter rows. - if self.regions.is_full() && (!self.has_gutter || y % 2 == 0) { - self.finish_region(vt)?; - } - - match self.rows[y] { - Sizing::Auto => self.layout_auto_row(vt, y)?, - Sizing::Rel(v) => self.layout_relative_row(vt, v, y)?, - Sizing::Fr(v) => self.lrows.push(Row::Fr(v, y)), - } - } - - self.finish_region(vt)?; - - Ok(GridLayout { - fragment: Fragment::frames(self.finished), - cols: self.rcols, - rows: self.rrows, - }) - } - - /// Determine all column sizes. - #[tracing::instrument(name = "GridLayouter::measure_columns", skip_all)] - fn measure_columns(&mut self, vt: &mut Vt) -> SourceResult<()> { - // Sum of sizes of resolved relative tracks. - let mut rel = Abs::zero(); - - // Sum of fractions of all fractional tracks. - let mut fr = Fr::zero(); - - // Resolve the size of all relative columns and compute the sum of all - // fractional tracks. - for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) { - match col { - Sizing::Auto => {} - Sizing::Rel(v) => { - let resolved = - v.resolve(self.styles).relative_to(self.regions.base().x); - *rcol = resolved; - rel += resolved; - } - Sizing::Fr(v) => fr += v, - } - } - - // Size that is not used by fixed-size columns. - let available = self.regions.size.x - rel; - if available >= Abs::zero() { - // Determine size of auto columns. - let (auto, count) = self.measure_auto_columns(vt, available)?; - - // If there is remaining space, distribute it to fractional columns, - // otherwise shrink auto columns. - let remaining = available - auto; - if remaining >= Abs::zero() { - self.grow_fractional_columns(remaining, fr); - } else { - self.shrink_auto_columns(available, count); - } - } - - // Sum up the resolved column sizes once here. - self.width = self.rcols.iter().sum(); - - Ok(()) - } - - /// Measure the size that is available to auto columns. - fn measure_auto_columns( - &mut self, - vt: &mut Vt, - available: Abs, - ) -> SourceResult<(Abs, usize)> { - let mut auto = Abs::zero(); - let mut count = 0; - - // Determine size of auto columns by laying out all cells in those - // columns, measuring them and finding the largest one. - for (x, &col) in self.cols.iter().enumerate() { - if col != Sizing::Auto { - continue; - } - - let mut resolved = Abs::zero(); - for y in 0..self.rows.len() { - if let Some(cell) = self.cell(x, y) { - // For relative rows, we can already resolve the correct - // base and for auto and fr we could only guess anyway. - let height = match self.rows[y] { - Sizing::Rel(v) => { - v.resolve(self.styles).relative_to(self.regions.base().y) - } - _ => self.regions.base().y, - }; - - let size = Size::new(available, height); - let pod = Regions::one(size, Axes::splat(false)); - let frame = cell.measure(vt, self.styles, pod)?.into_frame(); - resolved.set_max(frame.width()); - } - } - - self.rcols[x] = resolved; - auto += resolved; - count += 1; - } - - Ok((auto, count)) - } - - /// Distribute remaining space to fractional columns. - fn grow_fractional_columns(&mut self, remaining: Abs, fr: Fr) { - if fr.is_zero() { - return; - } - - for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) { - if let Sizing::Fr(v) = col { - *rcol = v.share(fr, remaining); - } - } - } - - /// Redistribute space to auto columns so that each gets a fair share. - fn shrink_auto_columns(&mut self, available: Abs, count: usize) { - let mut last; - let mut fair = -Abs::inf(); - let mut redistribute = available; - let mut overlarge = count; - let mut changed = true; - - // Iteratively remove columns that don't need to be shrunk. - while changed && overlarge > 0 { - changed = false; - last = fair; - fair = redistribute / (overlarge as f64); - - for (&col, &rcol) in self.cols.iter().zip(&self.rcols) { - // Remove an auto column if it is not overlarge (rcol <= fair), - // but also hasn't already been removed (rcol > last). - if col == Sizing::Auto && rcol <= fair && rcol > last { - redistribute -= rcol; - overlarge -= 1; - changed = true; - } - } - } - - // Redistribute space fairly among overlarge columns. - for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) { - if col == Sizing::Auto && *rcol > fair { - *rcol = fair; - } - } - } - - /// Layout a row with automatic height. Such a row may break across multiple - /// regions. - fn layout_auto_row(&mut self, vt: &mut Vt, y: usize) -> SourceResult<()> { - // Determine the size for each region of the row. If the first region - // ends up empty for some column, skip the region and remeasure. - let mut resolved = match self.measure_auto_row(vt, y, true)? { - Some(resolved) => resolved, - None => { - self.finish_region(vt)?; - self.measure_auto_row(vt, y, false)?.unwrap() - } - }; - - // Nothing to layout. - if resolved.is_empty() { - return Ok(()); - } - - // Layout into a single region. - if let &[first] = resolved.as_slice() { - let frame = self.layout_single_row(vt, first, y)?; - self.push_row(frame, y); - return Ok(()); - } - - // Expand all but the last region. - // Skip the first region if the space is eaten up by an fr row. - let len = resolved.len(); - for (region, target) in self - .regions - .iter() - .zip(&mut resolved[..len - 1]) - .skip(self.lrows.iter().any(|row| matches!(row, Row::Fr(..))) as usize) - { - target.set_max(region.y); - } - - // Layout into multiple regions. - let fragment = self.layout_multi_row(vt, &resolved, y)?; - let len = fragment.len(); - for (i, frame) in fragment.into_iter().enumerate() { - self.push_row(frame, y); - if i + 1 < len { - self.finish_region(vt)?; - } - } - - Ok(()) - } - - /// Measure the regions sizes of an auto row. The option is always `Some(_)` - /// if `can_skip` is false. - fn measure_auto_row( - &mut self, - vt: &mut Vt, - y: usize, - can_skip: bool, - ) -> SourceResult<Option<Vec<Abs>>> { - let mut resolved: Vec<Abs> = vec![]; - - for (x, &rcol) in self.rcols.iter().enumerate() { - if let Some(cell) = self.cell(x, y) { - let mut pod = self.regions; - pod.size.x = rcol; - - let frames = cell.measure(vt, self.styles, pod)?.into_frames(); - - // Skip the first region if one cell in it is empty. Then, - // remeasure. - if let [first, rest @ ..] = frames.as_slice() { - if can_skip - && first.is_empty() - && rest.iter().any(|frame| !frame.is_empty()) - { - return Ok(None); - } - } - - let mut sizes = frames.iter().map(|frame| frame.height()); - for (target, size) in resolved.iter_mut().zip(&mut sizes) { - target.set_max(size); - } - - // New heights are maximal by virtue of being new. Note that - // this extend only uses the rest of the sizes iterator. - resolved.extend(sizes); - } - } - - Ok(Some(resolved)) - } - - /// Layout a row with relative height. Such a row cannot break across - /// multiple regions, but it may force a region break. - fn layout_relative_row( - &mut self, - vt: &mut Vt, - v: Rel<Length>, - y: usize, - ) -> SourceResult<()> { - let resolved = v.resolve(self.styles).relative_to(self.regions.base().y); - let frame = self.layout_single_row(vt, resolved, y)?; - - // Skip to fitting region. - let height = frame.height(); - while !self.regions.size.y.fits(height) && !self.regions.in_last() { - self.finish_region(vt)?; - - // Don't skip multiple regions for gutter and don't push a row. - if self.has_gutter && y % 2 == 1 { - return Ok(()); - } - } - - self.push_row(frame, y); - - Ok(()) - } - - /// Layout a row with fixed height and return its frame. - fn layout_single_row( - &mut self, - vt: &mut Vt, - height: Abs, - y: usize, - ) -> SourceResult<Frame> { - if !height.is_finite() { - bail!(error!(self.span, "cannot create grid with infinite height")); - } - - let mut output = Frame::soft(Size::new(self.width, height)); - let mut pos = Point::zero(); - - for (x, &rcol) in self.rcols.iter().enumerate() { - if let Some(cell) = self.cell(x, y) { - let size = Size::new(rcol, height); - let mut pod = Regions::one(size, Axes::splat(true)); - if self.rows[y] == Sizing::Auto { - pod.full = self.regions.full; - } - let frame = cell.layout(vt, self.styles, pod)?.into_frame(); - output.push_frame(pos, frame); - } - - pos.x += rcol; - } - - Ok(output) - } - - /// Layout a row spanning multiple regions. - fn layout_multi_row( - &mut self, - vt: &mut Vt, - heights: &[Abs], - y: usize, - ) -> SourceResult<Fragment> { - // Prepare frames. - let mut outputs: Vec<_> = heights - .iter() - .map(|&h| Frame::soft(Size::new(self.width, h))) - .collect(); - - // Prepare regions. - let size = Size::new(self.width, heights[0]); - let mut pod = Regions::one(size, Axes::splat(true)); - pod.full = self.regions.full; - pod.backlog = &heights[1..]; - - // Layout the row. - let mut pos = Point::zero(); - for (x, &rcol) in self.rcols.iter().enumerate() { - if let Some(cell) = self.cell(x, y) { - pod.size.x = rcol; - - // Push the layouted frames into the individual output frames. - let fragment = cell.layout(vt, self.styles, pod)?; - for (output, frame) in outputs.iter_mut().zip(fragment) { - output.push_frame(pos, frame); - } - } - - pos.x += rcol; - } - - Ok(Fragment::frames(outputs)) - } - - /// Push a row frame into the current region. - fn push_row(&mut self, frame: Frame, y: usize) { - self.regions.size.y -= frame.height(); - self.lrows.push(Row::Frame(frame, y)); - } - - /// Finish rows for one region. - fn finish_region(&mut self, vt: &mut Vt) -> SourceResult<()> { - // Determine the height of existing rows in the region. - let mut used = Abs::zero(); - let mut fr = Fr::zero(); - for row in &self.lrows { - match row { - Row::Frame(frame, _) => used += frame.height(), - Row::Fr(v, _) => fr += *v, - } - } - - // Determine the size of the grid in this region, expanding fully if - // there are fr rows. - let mut size = Size::new(self.width, used).min(self.initial); - if fr.get() > 0.0 && self.initial.y.is_finite() { - size.y = self.initial.y; - } - - // The frame for the region. - let mut output = Frame::soft(size); - let mut pos = Point::zero(); - let mut rrows = vec![]; - - // Place finished rows and layout fractional rows. - for row in std::mem::take(&mut self.lrows) { - let (frame, y) = match row { - Row::Frame(frame, y) => (frame, y), - Row::Fr(v, y) => { - let remaining = self.regions.full - used; - let height = v.share(fr, remaining); - (self.layout_single_row(vt, height, y)?, y) - } - }; - - let height = frame.height(); - output.push_frame(pos, frame); - rrows.push(RowPiece { height, y }); - pos.y += height; - } - - self.finished.push(output); - self.rrows.push(rrows); - self.regions.next(); - self.initial = self.regions.size; - - Ok(()) - } - - /// Get the content of the cell in column `x` and row `y`. - /// - /// Returns `None` if it's a gutter cell. - #[track_caller] - fn cell(&self, mut x: usize, y: usize) -> Option<&'a Content> { - assert!(x < self.cols.len()); - assert!(y < self.rows.len()); - - // Columns are reorder, but the cell slice is not. - if self.is_rtl { - x = self.cols.len() - 1 - x; - } - - if self.has_gutter { - // Even columns and rows are children, odd ones are gutter. - if x % 2 == 0 && y % 2 == 0 { - let c = 1 + self.cols.len() / 2; - self.cells.get((y / 2) * c + x / 2) - } else { - None - } - } else { - let c = self.cols.len(); - self.cells.get(y * c + x) - } - } -} diff --git a/crates/typst-library/src/layout/hide.rs b/crates/typst-library/src/layout/hide.rs deleted file mode 100644 index af3d0631..00000000 --- a/crates/typst-library/src/layout/hide.rs +++ /dev/null @@ -1,29 +0,0 @@ -use smallvec::smallvec; - -use crate::prelude::*; - -/// Hides content without affecting layout. -/// -/// The `hide` function allows you to hide content while the layout still 'sees' -/// it. This is useful to create whitespace that is exactly as large as some -/// content. It may also be useful to redact content because its arguments are -/// not included in the output. -/// -/// # Example -/// ```example -/// Hello Jane \ -/// #hide[Hello] Joe -/// ``` -#[elem(Show)] -pub struct HideElem { - /// The content to hide. - #[required] - pub body: Content, -} - -impl Show for HideElem { - #[tracing::instrument(name = "HideElem::show", skip(self))] - fn show(&self, _: &mut Vt, _: StyleChain) -> SourceResult<Content> { - Ok(self.body().clone().styled(MetaElem::set_data(smallvec![Meta::Hide]))) - } -} diff --git a/crates/typst-library/src/layout/list.rs b/crates/typst-library/src/layout/list.rs deleted file mode 100644 index 7c089760..00000000 --- a/crates/typst-library/src/layout/list.rs +++ /dev/null @@ -1,238 +0,0 @@ -use crate::layout::{BlockElem, GridLayouter, ParElem, Sizing, Spacing}; -use crate::prelude::*; -use crate::text::TextElem; - -/// A bullet list. -/// -/// Displays a sequence of items vertically, with each item introduced by a -/// marker. -/// -/// # Example -/// ```example -/// Normal list. -/// - Text -/// - Math -/// - Layout -/// - ... -/// -/// Multiple lines. -/// - This list item spans multiple -/// lines because it is indented. -/// -/// Function call. -/// #list( -/// [Foundations], -/// [Calculate], -/// [Construct], -/// [Data Loading], -/// ) -/// ``` -/// -/// # Syntax -/// This functions also has dedicated syntax: Start a line with a hyphen, -/// 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", Layout)] -pub struct ListElem { - /// If this is `{false}`, the items are spaced apart with - /// [list spacing]($list.spacing). If it is `{true}`, they use normal - /// [leading]($par.leading) instead. This makes the list more compact, which - /// can look better if the items are short. - /// - /// In markup mode, the value of this parameter is determined based on - /// whether items are separated with a blank line. If items directly follow - /// each other, this is set to `{true}`; if items are separated by a blank - /// line, this is set to `{false}`. - /// - /// ```example - /// - If a list has a lot of text, and - /// maybe other inline content, it - /// should not be tight anymore. - /// - /// - To make a list wide, simply insert - /// a blank line between the items. - /// ``` - #[default(true)] - pub tight: bool, - - /// The marker which introduces each item. - /// - /// Instead of plain content, you can also pass an array with multiple - /// markers that should be used for nested lists. If the list nesting depth - /// exceeds the number of markers, the last one is repeated. For total - /// control, you may pass a function that maps the list's nesting depth - /// (starting from `{0}`) to a desired marker. - /// - /// ```example - /// #set list(marker: [--]) - /// - A more classic list - /// - With en-dashes - /// - /// #set list(marker: ([•], [--])) - /// - Top-level - /// - Nested - /// - Items - /// - Items - /// ``` - #[borrowed] - #[default(ListMarker::Content(vec![TextElem::packed('•')]))] - pub marker: ListMarker, - - /// The indent of each item. - #[resolve] - pub indent: Length, - - /// The spacing between the marker and the body of each item. - #[resolve] - #[default(Em::new(0.5).into())] - pub body_indent: Length, - - /// The spacing between the items of a wide (non-tight) list. - /// - /// If set to `{auto}`, uses the spacing [below blocks]($block.below). - pub spacing: Smart<Spacing>, - - /// The bullet list's children. - /// - /// When using the list syntax, adjacent items are automatically collected - /// into lists, even through constructs like for loops. - /// - /// ```example - /// #for letter in "ABC" [ - /// - Letter #letter - /// ] - /// ``` - #[variadic] - pub children: Vec<ListItem>, - - /// The nesting depth. - #[internal] - #[fold] - depth: Depth, -} - -#[scope] -impl ListElem { - #[elem] - type ListItem; -} - -impl Layout for ListElem { - #[tracing::instrument(name = "ListElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - 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()) - }; - - let depth = self.depth(styles); - let marker = self - .marker(styles) - .resolve(vt, depth)? - // avoid '#set align' interference with the list - .aligned(HAlign::Start + VAlign::Top); - - let mut cells = vec![]; - for item in self.children() { - cells.push(Content::empty()); - cells.push(marker.clone()); - cells.push(Content::empty()); - cells.push(item.body().clone().styled(Self::set_depth(Depth))); - } - - let layouter = GridLayouter::new( - Axes::with_x(&[ - Sizing::Rel(indent.into()), - Sizing::Auto, - Sizing::Rel(body_indent.into()), - Sizing::Auto, - ]), - Axes::with_y(&[gutter.into()]), - &cells, - regions, - styles, - self.span(), - ); - - Ok(layouter.layout(vt)?.fragment) - } -} - -/// A bullet list item. -#[elem(name = "item", title = "Bullet List Item")] -pub struct ListItem { - /// The item's body. - #[required] - pub body: Content, -} - -cast! { - ListItem, - v: Content => v.to::<Self>().cloned().unwrap_or_else(|| Self::new(v.clone())), -} - -/// A list's marker. -#[derive(Debug, Clone, PartialEq, Hash)] -pub enum ListMarker { - Content(Vec<Content>), - Func(Func), -} - -impl ListMarker { - /// Resolve the marker for the given depth. - fn resolve(&self, vt: &mut Vt, depth: usize) -> SourceResult<Content> { - Ok(match self { - Self::Content(list) => { - list.get(depth).or(list.last()).cloned().unwrap_or_default() - } - Self::Func(func) => func.call_vt(vt, [depth])?.display(), - }) - } -} - -cast! { - ListMarker, - self => match self { - Self::Content(vec) => if vec.len() == 1 { - vec.into_iter().next().unwrap().into_value() - } else { - vec.into_value() - }, - Self::Func(func) => func.into_value(), - }, - v: Content => Self::Content(vec![v]), - array: Array => { - if array.is_empty() { - bail!("array must contain at least one marker"); - } - Self::Content(array.into_iter().map(Value::display).collect()) - }, - v: Func => Self::Func(v), -} - -#[derive(Debug, Clone, Copy, PartialEq, Hash)] -struct Depth; - -cast! { - Depth, - self => Value::None, - _: Value => Self, -} - -impl Fold for Depth { - type Output = usize; - - fn fold(self, outer: Self::Output) -> Self::Output { - outer + 1 - } -} diff --git a/crates/typst-library/src/layout/measure.rs b/crates/typst-library/src/layout/measure.rs deleted file mode 100644 index d41b7f95..00000000 --- a/crates/typst-library/src/layout/measure.rs +++ /dev/null @@ -1,53 +0,0 @@ -use crate::prelude::*; - -/// Measures the layouted size of content. -/// -/// The `measure` function lets you determine the layouted size of content. Note -/// that an infinite space is assumed, therefore the measured height/width may -/// not necessarily match the final height/width of the measured content. If you -/// want to measure in the current layout dimensions, you can combine `measure` -/// and [`layout`]($layout). -/// -/// # Example -/// The same content can have a different size depending on the styles that -/// are active when it is layouted. For example, in the example below -/// `[#content]` is of course bigger when we increase the font size. -/// -/// ```example -/// #let content = [Hello!] -/// #content -/// #set text(14pt) -/// #content -/// ``` -/// -/// To do a meaningful measurement, you therefore first need to retrieve the -/// active styles with the [`style`]($style) function. You can then pass them to -/// the `measure` function. -/// -/// ```example -/// #let thing(body) = style(styles => { -/// let size = measure(body, styles) -/// [Width of "#body" is #size.width] -/// }) -/// -/// #thing[Hey] \ -/// #thing[Welcome] -/// ``` -/// -/// The measure function returns a dictionary with the entries `width` and -/// `height`, both of type [`length`]($length). -#[func] -pub fn measure( - /// The virtual machine. - vm: &mut Vm, - /// The content whose size to measure. - content: Content, - /// The styles with which to layout the content. - styles: Styles, -) -> SourceResult<Dict> { - let pod = Regions::one(Axes::splat(Abs::inf()), Axes::splat(false)); - let styles = StyleChain::new(&styles); - let frame = content.measure(&mut vm.vt, styles, pod)?.into_frame(); - let Size { x, y } = frame.size(); - Ok(dict! { "width" => x, "height" => y }) -} diff --git a/crates/typst-library/src/layout/mod.rs b/crates/typst-library/src/layout/mod.rs deleted file mode 100644 index bcbe3b2b..00000000 --- a/crates/typst-library/src/layout/mod.rs +++ /dev/null @@ -1,786 +0,0 @@ -//! Composable layouts. - -mod align; -mod columns; -mod container; -#[path = "enum.rs"] -mod enum_; -mod flow; -mod fragment; -mod grid; -mod hide; -mod list; -#[path = "measure.rs"] -mod measure_; -mod pad; -mod page; -mod par; -mod place; -mod regions; -mod repeat; -mod spacing; -mod stack; -mod table; -mod terms; -mod transform; - -pub use self::align::*; -pub use self::columns::*; -pub use self::container::*; -pub use self::enum_::*; -pub use self::flow::*; -pub use self::fragment::*; -pub use self::grid::*; -pub use self::hide::*; -pub use self::list::*; -pub use self::measure_::*; -pub use self::pad::*; -pub use self::page::*; -pub use self::par::*; -pub use self::place::*; -pub use self::regions::*; -pub use self::repeat::*; -pub use self::spacing::*; -pub use self::stack::*; -pub use self::table::*; -pub use self::terms::*; -pub use self::transform::*; - -use std::borrow::Cow; -use std::mem; - -use typed_arena::Arena; -use typst::diag::SourceResult; -use typst::eval::Tracer; -use typst::model::{applicable, realize, DelayedErrors, StyleVecBuilder}; - -use crate::math::{EquationElem, LayoutMath}; -use crate::meta::{CiteElem, CiteGroup, DocumentElem}; -use crate::prelude::*; -use crate::shared::BehavedBuilder; -use crate::text::{LinebreakElem, SmartquoteElem, SpaceElem, TextElem}; -use crate::visualize::{ - CircleElem, EllipseElem, ImageElem, LineElem, PathElem, PolygonElem, RectElem, - SquareElem, -}; - -/// Hook up all layout definitions. -pub(super) fn define(global: &mut Scope) { - global.category("layout"); - global.define_type::<Length>(); - global.define_type::<Angle>(); - global.define_type::<Ratio>(); - global.define_type::<Rel<Length>>(); - global.define_type::<Fr>(); - global.define_type::<Dir>(); - global.define_type::<Align>(); - global.define_elem::<PageElem>(); - global.define_elem::<PagebreakElem>(); - global.define_elem::<VElem>(); - global.define_elem::<ParElem>(); - global.define_elem::<ParbreakElem>(); - global.define_elem::<HElem>(); - global.define_elem::<BoxElem>(); - global.define_elem::<BlockElem>(); - global.define_elem::<ListElem>(); - global.define_elem::<EnumElem>(); - global.define_elem::<TermsElem>(); - global.define_elem::<TableElem>(); - global.define_elem::<StackElem>(); - global.define_elem::<GridElem>(); - global.define_elem::<ColumnsElem>(); - global.define_elem::<ColbreakElem>(); - global.define_elem::<PlaceElem>(); - global.define_elem::<AlignElem>(); - global.define_elem::<PadElem>(); - global.define_elem::<RepeatElem>(); - global.define_elem::<MoveElem>(); - global.define_elem::<ScaleElem>(); - global.define_elem::<RotateElem>(); - global.define_elem::<HideElem>(); - global.define_func::<measure>(); -} - -/// Root-level layout. -pub trait LayoutRoot { - /// Layout into one frame per page. - fn layout_root(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Document>; -} - -impl LayoutRoot for Content { - #[tracing::instrument(name = "Content::layout_root", skip_all)] - fn layout_root(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Document> { - #[comemo::memoize] - fn cached( - content: &Content, - world: Tracked<dyn World + '_>, - introspector: Tracked<Introspector>, - locator: Tracked<Locator>, - delayed: TrackedMut<DelayedErrors>, - tracer: TrackedMut<Tracer>, - styles: StyleChain, - ) -> SourceResult<Document> { - let mut locator = Locator::chained(locator); - let mut vt = Vt { - world, - introspector, - locator: &mut locator, - delayed, - tracer, - }; - let scratch = Scratch::default(); - let (realized, styles) = realize_root(&mut vt, &scratch, content, styles)?; - realized - .with::<dyn LayoutRoot>() - .unwrap() - .layout_root(&mut vt, styles) - } - - tracing::info!("Starting layout"); - cached( - self, - vt.world, - vt.introspector, - vt.locator.track(), - TrackedMut::reborrow_mut(&mut vt.delayed), - TrackedMut::reborrow_mut(&mut vt.tracer), - styles, - ) - } -} - -/// Layout into regions. -pub trait Layout { - /// Layout into one frame per region. - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment>; - - /// Layout without side effects. - /// - /// This element must be layouted again in the same order for the results to - /// be valid. - #[tracing::instrument(name = "Layout::measure", skip_all)] - fn measure( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - let mut locator = Locator::chained(vt.locator.track()); - let mut vt = Vt { - world: vt.world, - introspector: vt.introspector, - locator: &mut locator, - tracer: TrackedMut::reborrow_mut(&mut vt.tracer), - delayed: TrackedMut::reborrow_mut(&mut vt.delayed), - }; - self.layout(&mut vt, styles, regions) - } -} - -impl Layout for Content { - #[tracing::instrument(name = "Content::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - #[allow(clippy::too_many_arguments)] - #[comemo::memoize] - fn cached( - content: &Content, - world: Tracked<dyn World + '_>, - introspector: Tracked<Introspector>, - locator: Tracked<Locator>, - delayed: TrackedMut<DelayedErrors>, - tracer: TrackedMut<Tracer>, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - let mut locator = Locator::chained(locator); - let mut vt = Vt { - world, - introspector, - locator: &mut locator, - delayed, - tracer, - }; - let scratch = Scratch::default(); - let (realized, styles) = realize_block(&mut vt, &scratch, content, styles)?; - realized - .with::<dyn Layout>() - .unwrap() - .layout(&mut vt, styles, regions) - } - - tracing::info!("Layouting `Content`"); - - let fragment = cached( - self, - vt.world, - vt.introspector, - vt.locator.track(), - TrackedMut::reborrow_mut(&mut vt.delayed), - TrackedMut::reborrow_mut(&mut vt.tracer), - styles, - regions, - )?; - - vt.locator.visit_frames(&fragment); - Ok(fragment) - } -} - -/// Realize into an element that is capable of root-level layout. -#[tracing::instrument(skip_all)] -fn realize_root<'a>( - vt: &mut Vt, - scratch: &'a Scratch<'a>, - content: &'a Content, - styles: StyleChain<'a>, -) -> SourceResult<(Cow<'a, Content>, StyleChain<'a>)> { - if content.can::<dyn LayoutRoot>() && !applicable(content, styles) { - return Ok((Cow::Borrowed(content), styles)); - } - - let mut builder = Builder::new(vt, scratch, true); - builder.accept(content, styles)?; - builder.interrupt_page(Some(styles), true)?; - let (pages, shared) = builder.doc.unwrap().pages.finish(); - Ok((Cow::Owned(DocumentElem::new(pages.to_vec()).pack()), shared)) -} - -/// Realize into an element that is capable of block-level layout. -#[tracing::instrument(skip_all)] -fn realize_block<'a>( - vt: &mut Vt, - scratch: &'a Scratch<'a>, - content: &'a Content, - styles: StyleChain<'a>, -) -> SourceResult<(Cow<'a, Content>, StyleChain<'a>)> { - // These elements implement `Layout` but still require a flow for - // proper layout. - if content.can::<dyn Layout>() - && !content.is::<BoxElem>() - && !content.is::<LineElem>() - && !content.is::<RectElem>() - && !content.is::<SquareElem>() - && !content.is::<EllipseElem>() - && !content.is::<CircleElem>() - && !content.is::<ImageElem>() - && !content.is::<PolygonElem>() - && !content.is::<PathElem>() - && !content.is::<PlaceElem>() - && !applicable(content, styles) - { - return Ok((Cow::Borrowed(content), styles)); - } - - let mut builder = Builder::new(vt, scratch, false); - builder.accept(content, styles)?; - builder.interrupt_par()?; - let (children, shared) = builder.flow.0.finish(); - Ok((Cow::Owned(FlowElem::new(children.to_vec()).pack()), shared)) -} - -/// Builds a document or a flow element from content. -struct Builder<'a, 'v, 't> { - /// The virtual typesetter. - vt: &'v mut Vt<'t>, - /// Scratch arenas for building. - scratch: &'a Scratch<'a>, - /// The current document building state. - doc: Option<DocBuilder<'a>>, - /// The current flow building state. - flow: FlowBuilder<'a>, - /// The current paragraph building state. - par: ParBuilder<'a>, - /// The current list building state. - list: ListBuilder<'a>, - /// The current citation grouping state. - cites: CiteGroupBuilder<'a>, -} - -/// Temporary storage arenas for building. -#[derive(Default)] -struct Scratch<'a> { - /// An arena where intermediate style chains are stored. - styles: Arena<StyleChain<'a>>, - /// An arena where intermediate content resulting from show rules is stored. - content: Arena<Content>, -} - -impl<'a, 'v, 't> Builder<'a, 'v, 't> { - fn new(vt: &'v mut Vt<'t>, scratch: &'a Scratch<'a>, top: bool) -> Self { - Self { - vt, - scratch, - doc: top.then(DocBuilder::default), - flow: FlowBuilder::default(), - par: ParBuilder::default(), - list: ListBuilder::default(), - cites: CiteGroupBuilder::default(), - } - } - - fn accept( - &mut self, - mut content: &'a Content, - styles: StyleChain<'a>, - ) -> SourceResult<()> { - if content.can::<dyn LayoutMath>() && !content.is::<EquationElem>() { - content = - self.scratch.content.alloc(EquationElem::new(content.clone()).pack()); - } - - if let Some(realized) = realize(self.vt, content, styles)? { - let stored = self.scratch.content.alloc(realized); - return self.accept(stored, styles); - } - - if let Some((elem, local)) = content.to_styled() { - return self.styled(elem, local, styles); - } - - if let Some(children) = content.to_sequence() { - for elem in children { - self.accept(elem, styles)?; - } - return Ok(()); - } - - if self.cites.accept(content, styles) { - return Ok(()); - } - - self.interrupt_cites()?; - - if self.list.accept(content, styles) { - return Ok(()); - } - - self.interrupt_list()?; - - if self.list.accept(content, styles) { - return Ok(()); - } - - if self.par.accept(content, styles) { - return Ok(()); - } - - self.interrupt_par()?; - - if self.flow.accept(content, styles) { - return Ok(()); - } - - let keep = content - .to::<PagebreakElem>() - .map_or(false, |pagebreak| !pagebreak.weak(styles)); - - self.interrupt_page(keep.then_some(styles), false)?; - - if let Some(doc) = &mut self.doc { - if doc.accept(content, styles) { - return Ok(()); - } - } - - if content.is::<PagebreakElem>() { - bail!(content.span(), "pagebreaks are not allowed inside of containers"); - } else { - bail!(content.span(), "{} is not allowed here", content.func().name()); - } - } - - fn styled( - &mut self, - elem: &'a Content, - map: &'a Styles, - styles: StyleChain<'a>, - ) -> SourceResult<()> { - let stored = self.scratch.styles.alloc(styles); - let styles = stored.chain(map); - self.interrupt_style(map, None)?; - self.accept(elem, styles)?; - self.interrupt_style(map, Some(styles))?; - Ok(()) - } - - fn interrupt_style( - &mut self, - local: &Styles, - outer: Option<StyleChain<'a>>, - ) -> SourceResult<()> { - if let Some(Some(span)) = local.interruption::<DocumentElem>() { - if self.doc.is_none() { - bail!(span, "document set rules are not allowed inside of containers"); - } - if outer.is_none() - && (!self.flow.0.is_empty() - || !self.par.0.is_empty() - || !self.list.items.is_empty()) - { - bail!(span, "document set rules must appear before any content"); - } - } else if let Some(Some(span)) = local.interruption::<PageElem>() { - if self.doc.is_none() { - bail!(span, "page configuration is not allowed inside of containers"); - } - self.interrupt_page(outer, false)?; - } else if local.interruption::<ParElem>().is_some() - || local.interruption::<AlignElem>().is_some() - { - self.interrupt_par()?; - } else if local.interruption::<ListElem>().is_some() - || local.interruption::<EnumElem>().is_some() - || local.interruption::<TermsElem>().is_some() - { - self.interrupt_list()?; - } - Ok(()) - } - - fn interrupt_cites(&mut self) -> SourceResult<()> { - if !self.cites.items.is_empty() { - let staged = mem::take(&mut self.cites.staged); - let (group, styles) = mem::take(&mut self.cites).finish(); - let stored = self.scratch.content.alloc(group); - self.accept(stored, styles)?; - for (content, styles) in staged { - self.accept(content, styles)?; - } - } - Ok(()) - } - - fn interrupt_list(&mut self) -> SourceResult<()> { - self.interrupt_cites()?; - if !self.list.items.is_empty() { - let staged = mem::take(&mut self.list.staged); - let (list, styles) = mem::take(&mut self.list).finish(); - let stored = self.scratch.content.alloc(list); - self.accept(stored, styles)?; - for (content, styles) in staged { - self.accept(content, styles)?; - } - } - Ok(()) - } - - fn interrupt_par(&mut self) -> SourceResult<()> { - self.interrupt_list()?; - if !self.par.0.is_empty() { - let (par, styles) = mem::take(&mut self.par).finish(); - let stored = self.scratch.content.alloc(par); - self.accept(stored, styles)?; - } - - Ok(()) - } - - fn interrupt_page( - &mut self, - styles: Option<StyleChain<'a>>, - last: bool, - ) -> SourceResult<()> { - self.interrupt_par()?; - let Some(doc) = &mut self.doc else { return Ok(()) }; - if (doc.keep_next && styles.is_some()) || self.flow.0.has_strong_elements(last) { - let (flow, shared) = mem::take(&mut self.flow).0.finish(); - let styles = if shared == StyleChain::default() { - styles.unwrap_or_default() - } else { - shared - }; - let page = PageElem::new(FlowElem::new(flow.to_vec()).pack()); - let stored = self.scratch.content.alloc(page.pack()); - self.accept(stored, styles)?; - } - Ok(()) - } -} - -/// Accepts pagebreaks and pages. -struct DocBuilder<'a> { - /// The page runs built so far. - pages: StyleVecBuilder<'a, Cow<'a, Content>>, - /// Whether to keep a following page even if it is empty. - keep_next: bool, - /// Whether the next page should be cleared to an even or odd number. - clear_next: Option<Parity>, -} - -impl<'a> DocBuilder<'a> { - fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool { - if let Some(pagebreak) = content.to::<PagebreakElem>() { - self.keep_next = !pagebreak.weak(styles); - self.clear_next = pagebreak.to(styles); - return true; - } - - if let Some(page) = content.to::<PageElem>() { - let elem = if let Some(clear_to) = self.clear_next.take() { - let mut page = page.clone(); - page.push_clear_to(Some(clear_to)); - Cow::Owned(page.pack()) - } else { - Cow::Borrowed(content) - }; - - self.pages.push(elem, styles); - self.keep_next = false; - return true; - } - - false - } -} - -impl Default for DocBuilder<'_> { - fn default() -> Self { - Self { - pages: StyleVecBuilder::new(), - keep_next: true, - clear_next: None, - } - } -} - -/// Accepts flow content. -#[derive(Default)] -struct FlowBuilder<'a>(BehavedBuilder<'a>, bool); - -impl<'a> FlowBuilder<'a> { - fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool { - if content.is::<ParbreakElem>() { - self.1 = true; - return true; - } - - let last_was_parbreak = self.1; - self.1 = false; - - if content.is::<VElem>() - || content.is::<ColbreakElem>() - || content.is::<MetaElem>() - || content.is::<PlaceElem>() - { - self.0.push(Cow::Borrowed(content), styles); - return true; - } - - if content.can::<dyn Layout>() || content.is::<ParElem>() { - let is_tight_list = if let Some(elem) = content.to::<ListElem>() { - elem.tight(styles) - } else if let Some(elem) = content.to::<EnumElem>() { - elem.tight(styles) - } else if let Some(elem) = content.to::<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(Cow::Owned(spacing.pack()), styles); - } - - let (above, below) = if let Some(block) = content.to::<BlockElem>() { - (block.above(styles), block.below(styles)) - } else { - (BlockElem::above_in(styles), BlockElem::below_in(styles)) - }; - - self.0.push(Cow::Owned(above.pack()), styles); - self.0.push(Cow::Borrowed(content), styles); - self.0.push(Cow::Owned(below.pack()), styles); - return true; - } - - false - } -} - -/// Accepts paragraph content. -#[derive(Default)] -struct ParBuilder<'a>(BehavedBuilder<'a>); - -impl<'a> ParBuilder<'a> { - fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool { - if content.is::<MetaElem>() { - if self.0.has_strong_elements(false) { - self.0.push(Cow::Borrowed(content), styles); - return true; - } - } else if content.is::<SpaceElem>() - || content.is::<TextElem>() - || content.is::<HElem>() - || content.is::<LinebreakElem>() - || content.is::<SmartquoteElem>() - || content.to::<EquationElem>().map_or(false, |elem| !elem.block(styles)) - || content.is::<BoxElem>() - { - self.0.push(Cow::Borrowed(content), styles); - return true; - } - - false - } - - fn finish(self) -> (Content, StyleChain<'a>) { - let (children, shared) = self.0.finish(); - - // Find the first span that isn't detached. - let span = children - .iter() - .map(|(cnt, _)| cnt.span()) - .find(|span| !span.is_detached()) - .unwrap_or_else(Span::detached); - - (ParElem::new(children.to_vec()).spanned(span).pack(), shared) - } -} - -/// Accepts list / enum items, spaces, paragraph breaks. -struct ListBuilder<'a> { - /// The list items collected so far. - items: StyleVecBuilder<'a, Cow<'a, Content>>, - /// Whether the list contains no paragraph breaks. - tight: bool, - /// Trailing content for which it is unclear whether it is part of the list. - staged: Vec<(&'a Content, StyleChain<'a>)>, -} - -impl<'a> ListBuilder<'a> { - fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool { - if !self.items.is_empty() - && (content.is::<SpaceElem>() || content.is::<ParbreakElem>()) - { - self.staged.push((content, styles)); - return true; - } - - if (content.is::<ListItem>() - || content.is::<EnumItem>() - || content.is::<TermItem>()) - && self - .items - .elems() - .next() - .map_or(true, |first| first.func() == content.func()) - { - self.items.push(Cow::Borrowed(content), styles); - self.tight &= self.staged.drain(..).all(|(t, _)| !t.is::<ParbreakElem>()); - return true; - } - - false - } - - fn finish(self) -> (Content, StyleChain<'a>) { - let (items, shared) = self.items.finish(); - let item = items.items().next().unwrap(); - let output = if item.is::<ListItem>() { - ListElem::new( - items - .iter() - .map(|(item, local)| { - let item = item.to::<ListItem>().unwrap(); - item.clone() - .with_body(item.body().clone().styled_with_map(local.clone())) - }) - .collect::<Vec<_>>(), - ) - .with_tight(self.tight) - .pack() - } else if item.is::<EnumItem>() { - EnumElem::new( - items - .iter() - .map(|(item, local)| { - let item = item.to::<EnumItem>().unwrap(); - item.clone() - .with_body(item.body().clone().styled_with_map(local.clone())) - }) - .collect::<Vec<_>>(), - ) - .with_tight(self.tight) - .pack() - } else if item.is::<TermItem>() { - TermsElem::new( - items - .iter() - .map(|(item, local)| { - let item = item.to::<TermItem>().unwrap(); - item.clone() - .with_term(item.term().clone().styled_with_map(local.clone())) - .with_description( - item.description().clone().styled_with_map(local.clone()), - ) - }) - .collect::<Vec<_>>(), - ) - .with_tight(self.tight) - .pack() - } else { - unreachable!() - }; - (output, shared) - } -} - -impl Default for ListBuilder<'_> { - fn default() -> Self { - Self { - items: StyleVecBuilder::default(), - tight: true, - staged: vec![], - } - } -} - -/// Accepts citations. -#[derive(Default)] -struct CiteGroupBuilder<'a> { - /// The styles. - styles: StyleChain<'a>, - /// The citations. - items: Vec<CiteElem>, - /// Trailing content for which it is unclear whether it is part of the list. - staged: Vec<(&'a Content, StyleChain<'a>)>, -} - -impl<'a> CiteGroupBuilder<'a> { - fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool { - if !self.items.is_empty() - && (content.is::<SpaceElem>() || content.is::<MetaElem>()) - { - self.staged.push((content, styles)); - return true; - } - - if let Some(citation) = content.to::<CiteElem>() { - if self.items.is_empty() { - self.styles = styles; - } - self.staged.retain(|(elem, _)| !elem.is::<SpaceElem>()); - self.items.push(citation.clone()); - return true; - } - - false - } - - fn finish(self) -> (Content, StyleChain<'a>) { - let span = self.items.first().map(|cite| cite.span()).unwrap_or(Span::detached()); - (CiteGroup::new(self.items).pack().spanned(span), self.styles) - } -} diff --git a/crates/typst-library/src/layout/pad.rs b/crates/typst-library/src/layout/pad.rs deleted file mode 100644 index d1b0cb1f..00000000 --- a/crates/typst-library/src/layout/pad.rs +++ /dev/null @@ -1,121 +0,0 @@ -use crate::prelude::*; - -/// Adds spacing around content. -/// -/// The spacing can be specified for each side individually, or for all sides at -/// once by specifying a positional argument. -/// -/// # Example -/// ```example -/// #set align(center) -/// -/// #pad(x: 16pt, image("typing.jpg")) -/// _Typing speeds can be -/// measured in words per minute._ -/// ``` -#[elem(title = "Padding", Layout)] -pub struct PadElem { - /// The padding at the left side. - #[parse( - let all = args.named("rest")?.or(args.find()?); - let x = args.named("x")?.or(all); - let y = args.named("y")?.or(all); - args.named("left")?.or(x) - )] - pub left: Rel<Length>, - - /// The padding at the top side. - #[parse(args.named("top")?.or(y))] - pub top: Rel<Length>, - - /// The padding at the right side. - #[parse(args.named("right")?.or(x))] - pub right: Rel<Length>, - - /// The padding at the bottom side. - #[parse(args.named("bottom")?.or(y))] - pub bottom: Rel<Length>, - - /// The horizontal padding. Both `left` and `right` take precedence over - /// this. - #[external] - pub x: Rel<Length>, - - /// The vertical padding. Both `top` and `bottom` take precedence over this. - #[external] - pub y: Rel<Length>, - - /// The padding for all sides. All other parameters take precedence over - /// this. - #[external] - pub rest: Rel<Length>, - - /// The content to pad at the sides. - #[required] - pub body: Content, -} - -impl Layout for PadElem { - #[tracing::instrument(name = "PadElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - 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(vt, 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) - } -} - -/// 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() -} - -/// Grow a size by padding relative to the grown size. -/// This is the inverse operation to `shrink()`. -/// -/// For the horizontal axis the derivation looks as follows. -/// (Vertical axis is analogous.) -/// -/// Let w be the grown target width, -/// s be the given width, -/// l be the left padding, -/// r be the right padding, -/// p = l + r. -/// -/// We want that: w - l.resolve(w) - r.resolve(w) = s -/// -/// Thus: w - l.resolve(w) - r.resolve(w) = s -/// <=> w - p.resolve(w) = s -/// <=> 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).safe_div(1.0 - p.rel.get())) -} diff --git a/crates/typst-library/src/layout/page.rs b/crates/typst-library/src/layout/page.rs deleted file mode 100644 index 53646c7d..00000000 --- a/crates/typst-library/src/layout/page.rs +++ /dev/null @@ -1,949 +0,0 @@ -use std::borrow::Cow; -use std::ptr; -use std::str::FromStr; - -use typst::eval::AutoValue; - -use crate::layout::{AlignElem, ColumnsElem}; -use crate::meta::{Counter, CounterKey, ManualPageCounter, Numbering}; -use crate::prelude::*; -use crate::text::TextElem; - -/// Layouts its child onto one or multiple pages. -/// -/// Although this function is primarily used in set rules to affect page -/// properties, it can also be used to explicitly render its argument onto -/// a set of pages of its own. -/// -/// Pages can be set to use `{auto}` as their width or height. In this case, the -/// pages will grow to fit their content on the respective axis. -/// -/// The [Guide for Page Setup]($guides/page-setup-guide) explains how to use -/// this and related functions to set up a document with many examples. -/// -/// # Example -/// ```example -/// >>> #set page(margin: auto) -/// #set page("us-letter") -/// -/// There you go, US friends! -/// ``` -#[elem] -pub struct PageElem { - /// A standard paper size to set width and height. - #[external] - #[default(Paper::A4)] - pub paper: Paper, - - /// The width of the page. - /// - /// ```example - /// #set page( - /// width: 3cm, - /// margin: (x: 0cm), - /// ) - /// - /// #for i in range(3) { - /// box(square(width: 1cm)) - /// } - /// ``` - #[resolve] - #[parse( - let paper = args.named_or_find::<Paper>("paper")?; - args.named("width")? - .or_else(|| paper.map(|paper| Smart::Custom(paper.width().into()))) - )] - #[default(Smart::Custom(Paper::A4.width().into()))] - pub width: Smart<Length>, - - /// The height of the page. - /// - /// If this is set to `{auto}`, page breaks can only be triggered manually - /// by inserting a [page break]($pagebreak). Most examples throughout this - /// documentation use `{auto}` for the height of the page to dynamically - /// grow and shrink to fit their content. - #[resolve] - #[parse( - args.named("height")? - .or_else(|| paper.map(|paper| Smart::Custom(paper.height().into()))) - )] - #[default(Smart::Custom(Paper::A4.height().into()))] - pub height: Smart<Length>, - - /// Whether the page is flipped into landscape orientation. - /// - /// ```example - /// #set page( - /// "us-business-card", - /// flipped: true, - /// fill: rgb("f2e5dd"), - /// ) - /// - /// #set align(bottom + end) - /// #text(14pt)[*Sam H. Richards*] \ - /// _Procurement Manager_ - /// - /// #set text(10pt) - /// 17 Main Street \ - /// New York, NY 10001 \ - /// +1 555 555 5555 - /// ``` - #[default(false)] - pub flipped: bool, - - /// The page's margins. - /// - /// - `{auto}`: The margins are set automatically to 2.5/21 times the smaller - /// dimension of the page. This results in 2.5cm margins for an A4 page. - /// - A single length: The same margin on all sides. - /// - A dictionary: With a dictionary, the margins can be set individually. - /// The dictionary can contain the following keys in order of precedence: - /// - `top`: The top margin. - /// - `right`: The right margin. - /// - `bottom`: The bottom margin. - /// - `left`: The left margin. - /// - `inside`: The margin at the inner side of the page (where the - /// [binding]($page.binding) is). - /// - `outside`: The margin at the outer side of the page (opposite to the - /// [binding]($page.binding)). - /// - `x`: The horizontal margins. - /// - `y`: The vertical margins. - /// - `rest`: The margins on all sides except those for which the - /// dictionary explicitly sets a size. - /// - /// The values for `left` and `right` are mutually exclusive with - /// the values for `inside` and `outside`. - /// - /// ```example - /// #set page( - /// width: 3cm, - /// height: 4cm, - /// margin: (x: 8pt, y: 4pt), - /// ) - /// - /// #rect( - /// width: 100%, - /// height: 100%, - /// fill: aqua, - /// ) - /// ``` - #[fold] - pub margin: Margin, - - /// On which side the pages will be bound. - /// - /// - `{auto}`: Equivalent to `left` if the [text direction]($text.dir) - /// is left-to-right and `right` if it is right-to-left. - /// - `left`: Bound on the left side. - /// - `right`: Bound on the right side. - /// - /// This affects the meaning of the `inside` and `outside` options for - /// margins. - pub binding: Smart<Binding>, - - /// How many columns the page has. - /// - /// If you need to insert columns into a page or other container, you can - /// also use the [`columns` function]($columns). - /// - /// ```example:single - /// #set page(columns: 2, height: 4.8cm) - /// Climate change is one of the most - /// pressing issues of our time, with - /// the potential to devastate - /// communities, ecosystems, and - /// economies around the world. It's - /// clear that we need to take urgent - /// action to reduce our carbon - /// emissions and mitigate the impacts - /// of a rapidly changing climate. - /// ``` - #[default(NonZeroUsize::ONE)] - pub columns: NonZeroUsize, - - /// The page's background color. - /// - /// This instructs the printer to color the complete page with the given - /// color. If you are considering larger production runs, it may be more - /// environmentally friendly and cost-effective to source pre-dyed pages and - /// not set this property. - /// - /// ```example - /// #set page(fill: rgb("444352")) - /// #set text(fill: rgb("fdfdfd")) - /// *Dark mode enabled.* - /// ``` - #[borrowed] - pub fill: Option<Paint>, - - /// How to [number]($numbering) the pages. - /// - /// If an explicit `footer` (or `header` for top-aligned numbering) is - /// given, the numbering is ignored. - /// - /// ```example - /// #set page( - /// height: 100pt, - /// margin: (top: 16pt, bottom: 24pt), - /// numbering: "1 / 1", - /// ) - /// - /// #lorem(48) - /// ``` - #[borrowed] - pub numbering: Option<Numbering>, - - /// The alignment of the page numbering. - /// - /// If the vertical component is `top`, the numbering is placed into the - /// header and if it is `bottom`, it is placed in the footer. Horizon - /// alignment is forbidden. If an explicit matching `header` or `footer` is - /// given, the numbering is ignored. - /// - /// ```example - /// #set page( - /// margin: (top: 16pt, bottom: 24pt), - /// numbering: "1", - /// number-align: right, - /// ) - /// - /// #lorem(30) - /// ``` - #[default(HAlign::Center + VAlign::Bottom)] - #[parse({ - let option: Option<Spanned<Align>> = args.named("number-align")?; - if let Some(Spanned { v: align, span }) = option { - if align.y() == Some(VAlign::Horizon) { - bail!(span, "page number cannot be `horizon`-aligned"); - } - } - option.map(|spanned| spanned.v) - })] - pub number_align: Align, - - /// The page's header. Fills the top margin of each page. - /// - /// ```example - /// #set par(justify: true) - /// #set page( - /// margin: (top: 32pt, bottom: 20pt), - /// header: [ - /// #set text(8pt) - /// #smallcaps[Typst Academcy] - /// #h(1fr) _Exercise Sheet 3_ - /// ], - /// ) - /// - /// #lorem(19) - /// ``` - #[borrowed] - pub header: Option<Content>, - - /// The amount the header is raised into the top margin. - #[resolve] - #[default(Ratio::new(0.3).into())] - pub header_ascent: Rel<Length>, - - /// The page's footer. Fills the bottom margin of each page. - /// - /// For just a page number, the `numbering` property, typically suffices. If - /// you want to create a custom footer, but still display the page number, - /// you can directly access the [page counter]($counter). - /// - /// ```example - /// #set par(justify: true) - /// #set page( - /// height: 100pt, - /// margin: 20pt, - /// footer: [ - /// #set align(right) - /// #set text(8pt) - /// #counter(page).display( - /// "1 of I", - /// both: true, - /// ) - /// ] - /// ) - /// - /// #lorem(48) - /// ``` - #[borrowed] - pub footer: Option<Content>, - - /// The amount the footer is lowered into the bottom margin. - #[resolve] - #[default(Ratio::new(0.3).into())] - pub footer_descent: Rel<Length>, - - /// Content in the page's background. - /// - /// This content will be placed behind the page's body. It can be - /// used to place a background image or a watermark. - /// - /// ```example - /// #set page(background: rotate(24deg, - /// text(18pt, fill: rgb("FFCBC4"))[ - /// *CONFIDENTIAL* - /// ] - /// )) - /// - /// = Typst's secret plans - /// In the year 2023, we plan to take - /// over the world (of typesetting). - /// ``` - #[borrowed] - pub background: Option<Content>, - - /// Content in the page's foreground. - /// - /// This content will overlay the page's body. - /// - /// ```example - /// #set page(foreground: text(24pt)[🥸]) - /// - /// Reviewer 2 has marked our paper - /// "Weak Reject" because they did - /// not understand our approach... - /// ``` - #[borrowed] - pub foreground: Option<Content>, - - /// The contents of the page(s). - /// - /// Multiple pages will be created if the content does not fit on a single - /// page. A new page with the page properties prior to the function invocation - /// will be created after the body has been typeset. - #[required] - pub body: Content, - - /// Whether the page should be aligned to an even or odd page. - #[internal] - pub clear_to: Option<Parity>, -} - -impl PageElem { - /// A document can consist of multiple `PageElem`s, one per run of pages - /// with equal properties (not one per actual output page!). The `number` is - /// the physical page number of the first page of this run. It is mutated - /// while we post-process the pages in this function. This function returns - /// a fragment consisting of multiple frames, one per output page of this - /// page run. - #[tracing::instrument(skip_all)] - pub fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - page_counter: &mut ManualPageCounter, - extend_to: Option<Parity>, - ) -> SourceResult<Fragment> { - tracing::info!("Page layout"); - - // When one of the lengths is infinite the page fits its content along - // that axis. - let width = self.width(styles).unwrap_or(Abs::inf()); - let height = self.height(styles).unwrap_or(Abs::inf()); - let mut size = Size::new(width, height); - if self.flipped(styles) { - std::mem::swap(&mut size.x, &mut size.y); - } - - let mut min = width.min(height); - if !min.is_finite() { - min = Paper::A4.width(); - } - - // Determine the margins. - let default = Rel::<Length>::from((2.5 / 21.0) * min); - let margin = self.margin(styles); - let two_sided = margin.two_sided.unwrap_or(false); - let margin = margin - .sides - .map(|side| side.and_then(Smart::as_custom).unwrap_or(default)) - .resolve(styles) - .relative_to(size); - - // Determine the binding. - let binding = - self.binding(styles) - .unwrap_or_else(|| match TextElem::dir_in(styles) { - Dir::LTR => Binding::Left, - _ => Binding::Right, - }); - - // Realize columns. - let mut child = self.body().clone(); - let columns = self.columns(styles); - if columns.get() > 1 { - child = ColumnsElem::new(child).with_count(columns).pack(); - } - - let area = size - margin.sum_by_axis(); - let mut regions = Regions::repeat(area, area.map(Abs::is_finite)); - regions.root = true; - - // Layout the child. - let mut frames = child.layout(vt, styles, regions)?.into_frames(); - - // Align the child to the pagebreak's parity. - // Check for page count after adding the pending frames - if extend_to - .is_some_and(|p| !p.matches(page_counter.physical().get() + frames.len())) - { - // Insert empty page after the current pages. - let size = area.map(Abs::is_finite).select(area, Size::zero()); - frames.push(Frame::hard(size)); - } - - let fill = self.fill(styles); - let foreground = Cow::Borrowed(self.foreground(styles)); - let background = Cow::Borrowed(self.background(styles)); - let header_ascent = self.header_ascent(styles); - let footer_descent = self.footer_descent(styles); - let numbering = self.numbering(styles); - let numbering_meta = Meta::PageNumbering(numbering.clone().into_value()); - let number_align = self.number_align(styles); - let mut header = Cow::Borrowed(self.header(styles)); - let mut footer = Cow::Borrowed(self.footer(styles)); - - // Construct the numbering (for header or footer). - let numbering_marginal = Cow::Owned(numbering.as_ref().map(|numbering| { - let both = match numbering { - Numbering::Pattern(pattern) => pattern.pieces() >= 2, - Numbering::Func(_) => true, - }; - - let mut counter = - Counter::new(CounterKey::Page).display(Some(numbering.clone()), both); - - // We interpret the Y alignment as selecting header or footer - // and then ignore it for aligning the actual number. - if let Some(x) = number_align.x() { - counter = counter.aligned(x.into()); - } - - counter - })); - - if matches!(number_align.y(), Some(VAlign::Top)) { - header = if header.is_some() { header } else { numbering_marginal }; - } else { - footer = if footer.is_some() { footer } else { numbering_marginal }; - } - - // Post-process pages. - for frame in frames.iter_mut() { - tracing::info!("Layouting page #{}", page_counter.physical()); - - // The padded width of the page's content without margins. - let pw = frame.width(); - - // If two sided, left becomes inside and right becomes outside. - // Thus, for left-bound pages, we want to swap on even pages and - // for right-bound pages, we want to swap on odd pages. - let mut margin = margin; - if two_sided && binding.swap(page_counter.physical()) { - std::mem::swap(&mut margin.left, &mut margin.right); - } - - // Realize margins. - frame.set_size(frame.size() + margin.sum_by_axis()); - frame.translate(Point::new(margin.left, margin.top)); - frame.push_positionless_meta(numbering_meta.clone()); - - // The page size with margins. - let size = frame.size(); - - // Realize overlays. - for (name, marginal) in [ - ("header", &header), - ("footer", &footer), - ("background", &background), - ("foreground", &foreground), - ] { - tracing::info!("Layouting {name}"); - - let Some(content) = &**marginal else { continue }; - - let (pos, area, align); - if ptr::eq(marginal, &header) { - let ascent = header_ascent.relative_to(margin.top); - pos = Point::with_x(margin.left); - area = Size::new(pw, margin.top - ascent); - align = Align::BOTTOM; - } else if ptr::eq(marginal, &footer) { - let descent = footer_descent.relative_to(margin.bottom); - pos = Point::new(margin.left, size.y - margin.bottom + descent); - area = Size::new(pw, margin.bottom - descent); - align = Align::TOP; - } else { - pos = Point::zero(); - area = size; - align = HAlign::Center + VAlign::Horizon; - }; - - let pod = Regions::one(area, Axes::splat(true)); - let sub = content - .clone() - .styled(AlignElem::set_alignment(align)) - .layout(vt, styles, pod)? - .into_frame(); - - if ptr::eq(marginal, &header) || ptr::eq(marginal, &background) { - frame.prepend_frame(pos, sub); - } else { - frame.push_frame(pos, sub); - } - } - - if let Some(fill) = fill { - frame.fill(fill.clone()); - } - - page_counter.visit(vt, frame)?; - - // Add a PDF page label if there is a numbering. - if let Some(num) = numbering { - if let Some(page_label) = num.apply_pdf(page_counter.logical()) { - frame.push_positionless_meta(Meta::PdfPageLabel(page_label)); - } - } - - page_counter.step(); - } - - Ok(Fragment::frames(frames)) - } -} - -/// Specification of the page's margins. -#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] -pub struct Margin { - /// The margins for each side. - pub sides: Sides<Option<Smart<Rel<Length>>>>, - /// Whether to swap `left` and `right` to make them `inside` and `outside` - /// (when to swap depends on the binding). - pub two_sided: Option<bool>, -} - -impl Margin { - /// Create an instance with four equal components. - pub fn splat(value: Option<Smart<Rel<Length>>>) -> Self { - Self { sides: Sides::splat(value), two_sided: None } - } -} - -impl Fold for Margin { - type Output = Margin; - - fn fold(self, outer: Self::Output) -> Self::Output { - let sides = - self.sides - .zip(outer.sides) - .map(|(inner, outer)| match (inner, outer) { - (Some(value), Some(outer)) => Some(value.fold(outer)), - _ => inner.or(outer), - }); - let two_sided = self.two_sided.or(outer.two_sided); - Margin { sides, two_sided } - } -} - -cast! { - Margin, - self => { - let mut dict = Dict::new(); - let mut handle = |key: &str, component: Value| { - let value = component.into_value(); - if value != Value::None { - dict.insert(key.into(), value); - } - }; - - handle("top", self.sides.top.into_value()); - handle("bottom", self.sides.bottom.into_value()); - if self.two_sided.unwrap_or(false) { - handle("inside", self.sides.left.into_value()); - handle("outside", self.sides.right.into_value()); - } else { - handle("left", self.sides.left.into_value()); - handle("right", self.sides.right.into_value()); - } - - Value::Dict(dict) - }, - _: AutoValue => Self::splat(Some(Smart::Auto)), - v: Rel<Length> => Self::splat(Some(Smart::Custom(v))), - mut dict: Dict => { - let mut take = |key| dict.take(key).ok().map(Value::cast).transpose(); - - let rest = take("rest")?; - let x = take("x")?.or(rest); - let y = take("y")?.or(rest); - let top = take("top")?.or(y); - let bottom = take("bottom")?.or(y); - let outside = take("outside")?; - let inside = take("inside")?; - let left = take("left")?; - let right = take("right")?; - - let implicitly_two_sided = outside.is_some() || inside.is_some(); - let implicitly_not_two_sided = left.is_some() || right.is_some(); - if implicitly_two_sided && implicitly_not_two_sided { - bail!("`inside` and `outside` are mutually exclusive with `left` and `right`"); - } - - // - If 'implicitly_two_sided' is false here, then - // 'implicitly_not_two_sided' will be guaranteed to be true - // due to the previous two 'if' conditions. - // - If both are false, this means that this margin change does not - // affect lateral margins, and thus shouldn't make a difference on - // the 'two_sided' attribute of this margin. - let two_sided = (implicitly_two_sided || implicitly_not_two_sided) - .then_some(implicitly_two_sided); - - dict.finish(&[ - "left", "top", "right", "bottom", "outside", "inside", "x", "y", "rest", - ])?; - - Margin { - sides: Sides { - left: inside.or(left).or(x), - top, - right: outside.or(right).or(x), - bottom, - }, - two_sided, - } - } -} - -/// Specification of the page's binding. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum Binding { - /// Bound on the left, as customary in LTR languages. - Left, - /// Bound on the right, as customary in RTL languages. - Right, -} - -impl Binding { - /// Whether to swap left and right margin for the page with this number. - fn swap(self, number: NonZeroUsize) -> bool { - match self { - // Left-bound must swap on even pages - // (because it is correct on the first page). - Self::Left => number.get() % 2 == 0, - // Right-bound must swap on odd pages - // (because it is wrong on the first page). - Self::Right => number.get() % 2 == 1, - } - } -} - -cast! { - Binding, - self => match self { - Self::Left => Align::LEFT.into_value(), - Self::Right => Align::RIGHT.into_value(), - }, - v: Align => match v { - Align::LEFT => Self::Left, - Align::RIGHT => Self::Right, - _ => bail!("must be `left` or `right`"), - }, -} - -/// A header, footer, foreground or background definition. -#[derive(Debug, Clone, Hash)] -pub enum Marginal { - /// Bare content. - Content(Content), - /// A closure mapping from a page number to content. - Func(Func), -} - -impl Marginal { - /// Resolve the marginal based on the page number. - pub fn resolve(&self, vt: &mut Vt, page: usize) -> SourceResult<Cow<'_, Content>> { - Ok(match self { - Self::Content(content) => Cow::Borrowed(content), - Self::Func(func) => Cow::Owned(func.call_vt(vt, [page])?.display()), - }) - } -} - -cast! { - Marginal, - self => match self { - Self::Content(v) => v.into_value(), - Self::Func(v) => v.into_value(), - }, - v: Content => Self::Content(v), - v: Func => Self::Func(v), -} - -/// A manual page break. -/// -/// Must not be used inside any containers. -/// -/// # Example -/// ```example -/// The next page contains -/// more details on compound theory. -/// #pagebreak() -/// -/// == Compound Theory -/// In 1984, the first ... -/// ``` -#[elem(title = "Page Break")] -pub struct PagebreakElem { - /// If `{true}`, the page break is skipped if the current page is already - /// empty. - #[default(false)] - pub weak: bool, - - /// If given, ensures that the next page will be an even/odd page, with an - /// empty page in between if necessary. - /// - /// ```example - /// #set page(height: 30pt) - /// - /// First. - /// #pagebreak(to: "odd") - /// Third. - /// ``` - pub to: Option<Parity>, -} - -/// Whether something should be even or odd. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] -pub enum Parity { - /// Next page will be an even page. - Even, - /// Next page will be an odd page. - Odd, -} - -impl Parity { - /// Whether the given number matches the parity. - fn matches(self, number: usize) -> bool { - match self { - Self::Even => number % 2 == 0, - Self::Odd => number % 2 == 1, - } - } -} - -/// Specification of a paper. -#[derive(Debug, Copy, Clone, Hash)] -pub struct Paper { - /// The name of the paper. - name: &'static str, - /// The width of the paper in millimeters. - width: Scalar, - /// The height of the paper in millimeters. - height: Scalar, -} - -impl Paper { - /// The width of the paper. - pub fn width(self) -> Abs { - Abs::mm(self.width.get()) - } - - /// The height of the paper. - pub fn height(self) -> Abs { - Abs::mm(self.height.get()) - } -} - -/// Defines paper constants and a paper parsing implementation. -macro_rules! papers { - ($(($var:ident: $width:expr, $height: expr, $name:literal))*) => { - /// Predefined papers. - /// - /// Each paper is parsable from its name in kebab-case. - impl Paper { - $(pub const $var: Self = Self { - name: $name, - width: Scalar::new($width), - height: Scalar::new($height), - };)* - } - - impl FromStr for Paper { - type Err = &'static str; - - fn from_str(name: &str) -> Result<Self, Self::Err> { - match name.to_lowercase().as_str() { - $($name => Ok(Self::$var),)* - _ => Err("unknown paper size"), - } - } - } - - cast! { - Paper, - self => self.name.into_value(), - $( - /// Produces a paper of the respective size. - $name => Self::$var, - )* - } - }; -} - -// All paper sizes in mm. -// -// Resources: -// - https://papersizes.io/ -// - https://en.wikipedia.org/wiki/Paper_size -// - https://www.theedkins.co.uk/jo/units/oldunits/print.htm -// - https://vintagepaper.co/blogs/news/traditional-paper-sizes -papers! { - // ---------------------------------------------------------------------- // - // ISO 216 A Series - (A0: 841.0, 1189.0, "a0") - (A1: 594.0, 841.0, "a1") - (A2: 420.0, 594.0, "a2") - (A3: 297.0, 420.0, "a3") - (A4: 210.0, 297.0, "a4") - (A5: 148.0, 210.0, "a5") - (A6: 105.0, 148.0, "a6") - (A7: 74.0, 105.0, "a7") - (A8: 52.0, 74.0, "a8") - (A9: 37.0, 52.0, "a9") - (A10: 26.0, 37.0, "a10") - (A11: 18.0, 26.0, "a11") - - // ISO 216 B Series - (ISO_B1: 707.0, 1000.0, "iso-b1") - (ISO_B2: 500.0, 707.0, "iso-b2") - (ISO_B3: 353.0, 500.0, "iso-b3") - (ISO_B4: 250.0, 353.0, "iso-b4") - (ISO_B5: 176.0, 250.0, "iso-b5") - (ISO_B6: 125.0, 176.0, "iso-b6") - (ISO_B7: 88.0, 125.0, "iso-b7") - (ISO_B8: 62.0, 88.0, "iso-b8") - - // ISO 216 C Series - (ISO_C3: 324.0, 458.0, "iso-c3") - (ISO_C4: 229.0, 324.0, "iso-c4") - (ISO_C5: 162.0, 229.0, "iso-c5") - (ISO_C6: 114.0, 162.0, "iso-c6") - (ISO_C7: 81.0, 114.0, "iso-c7") - (ISO_C8: 57.0, 81.0, "iso-c8") - - // DIN D Series (extension to ISO) - (DIN_D3: 272.0, 385.0, "din-d3") - (DIN_D4: 192.0, 272.0, "din-d4") - (DIN_D5: 136.0, 192.0, "din-d5") - (DIN_D6: 96.0, 136.0, "din-d6") - (DIN_D7: 68.0, 96.0, "din-d7") - (DIN_D8: 48.0, 68.0, "din-d8") - - // SIS (used in academia) - (SIS_G5: 169.0, 239.0, "sis-g5") - (SIS_E5: 115.0, 220.0, "sis-e5") - - // ANSI Extensions - (ANSI_A: 216.0, 279.0, "ansi-a") - (ANSI_B: 279.0, 432.0, "ansi-b") - (ANSI_C: 432.0, 559.0, "ansi-c") - (ANSI_D: 559.0, 864.0, "ansi-d") - (ANSI_E: 864.0, 1118.0, "ansi-e") - - // ANSI Architectural Paper - (ARCH_A: 229.0, 305.0, "arch-a") - (ARCH_B: 305.0, 457.0, "arch-b") - (ARCH_C: 457.0, 610.0, "arch-c") - (ARCH_D: 610.0, 914.0, "arch-d") - (ARCH_E1: 762.0, 1067.0, "arch-e1") - (ARCH_E: 914.0, 1219.0, "arch-e") - - // JIS B Series - (JIS_B0: 1030.0, 1456.0, "jis-b0") - (JIS_B1: 728.0, 1030.0, "jis-b1") - (JIS_B2: 515.0, 728.0, "jis-b2") - (JIS_B3: 364.0, 515.0, "jis-b3") - (JIS_B4: 257.0, 364.0, "jis-b4") - (JIS_B5: 182.0, 257.0, "jis-b5") - (JIS_B6: 128.0, 182.0, "jis-b6") - (JIS_B7: 91.0, 128.0, "jis-b7") - (JIS_B8: 64.0, 91.0, "jis-b8") - (JIS_B9: 45.0, 64.0, "jis-b9") - (JIS_B10: 32.0, 45.0, "jis-b10") - (JIS_B11: 22.0, 32.0, "jis-b11") - - // SAC D Series - (SAC_D0: 764.0, 1064.0, "sac-d0") - (SAC_D1: 532.0, 760.0, "sac-d1") - (SAC_D2: 380.0, 528.0, "sac-d2") - (SAC_D3: 264.0, 376.0, "sac-d3") - (SAC_D4: 188.0, 260.0, "sac-d4") - (SAC_D5: 130.0, 184.0, "sac-d5") - (SAC_D6: 92.0, 126.0, "sac-d6") - - // ISO 7810 ID - (ISO_ID_1: 85.6, 53.98, "iso-id-1") - (ISO_ID_2: 74.0, 105.0, "iso-id-2") - (ISO_ID_3: 88.0, 125.0, "iso-id-3") - - // ---------------------------------------------------------------------- // - // Asia - (ASIA_F4: 210.0, 330.0, "asia-f4") - - // Japan - (JP_SHIROKU_BAN_4: 264.0, 379.0, "jp-shiroku-ban-4") - (JP_SHIROKU_BAN_5: 189.0, 262.0, "jp-shiroku-ban-5") - (JP_SHIROKU_BAN_6: 127.0, 188.0, "jp-shiroku-ban-6") - (JP_KIKU_4: 227.0, 306.0, "jp-kiku-4") - (JP_KIKU_5: 151.0, 227.0, "jp-kiku-5") - (JP_BUSINESS_CARD: 91.0, 55.0, "jp-business-card") - - // China - (CN_BUSINESS_CARD: 90.0, 54.0, "cn-business-card") - - // Europe - (EU_BUSINESS_CARD: 85.0, 55.0, "eu-business-card") - - // French Traditional (AFNOR) - (FR_TELLIERE: 340.0, 440.0, "fr-tellière") - (FR_COURONNE_ECRITURE: 360.0, 460.0, "fr-couronne-écriture") - (FR_COURONNE_EDITION: 370.0, 470.0, "fr-couronne-édition") - (FR_RAISIN: 500.0, 650.0, "fr-raisin") - (FR_CARRE: 450.0, 560.0, "fr-carré") - (FR_JESUS: 560.0, 760.0, "fr-jésus") - - // United Kingdom Imperial - (UK_BRIEF: 406.4, 342.9, "uk-brief") - (UK_DRAFT: 254.0, 406.4, "uk-draft") - (UK_FOOLSCAP: 203.2, 330.2, "uk-foolscap") - (UK_QUARTO: 203.2, 254.0, "uk-quarto") - (UK_CROWN: 508.0, 381.0, "uk-crown") - (UK_BOOK_A: 111.0, 178.0, "uk-book-a") - (UK_BOOK_B: 129.0, 198.0, "uk-book-b") - - // Unites States - (US_LETTER: 215.9, 279.4, "us-letter") - (US_LEGAL: 215.9, 355.6, "us-legal") - (US_TABLOID: 279.4, 431.8, "us-tabloid") - (US_EXECUTIVE: 84.15, 266.7, "us-executive") - (US_FOOLSCAP_FOLIO: 215.9, 342.9, "us-foolscap-folio") - (US_STATEMENT: 139.7, 215.9, "us-statement") - (US_LEDGER: 431.8, 279.4, "us-ledger") - (US_OFICIO: 215.9, 340.36, "us-oficio") - (US_GOV_LETTER: 203.2, 266.7, "us-gov-letter") - (US_GOV_LEGAL: 215.9, 330.2, "us-gov-legal") - (US_BUSINESS_CARD: 88.9, 50.8, "us-business-card") - (US_DIGEST: 139.7, 215.9, "us-digest") - (US_TRADE: 152.4, 228.6, "us-trade") - - // ---------------------------------------------------------------------- // - // Other - (NEWSPAPER_COMPACT: 280.0, 430.0, "newspaper-compact") - (NEWSPAPER_BERLINER: 315.0, 470.0, "newspaper-berliner") - (NEWSPAPER_BROADSHEET: 381.0, 578.0, "newspaper-broadsheet") - (PRESENTATION_16_9: 297.0, 167.0625, "presentation-16-9") - (PRESENTATION_4_3: 280.0, 210.0, "presentation-4-3") -} diff --git a/crates/typst-library/src/layout/par.rs b/crates/typst-library/src/layout/par.rs deleted file mode 100644 index 2d7c9080..00000000 --- a/crates/typst-library/src/layout/par.rs +++ /dev/null @@ -1,1520 +0,0 @@ -use comemo::Prehashed; -use typst::eval::Tracer; -use typst::model::DelayedErrors; -use unicode_bidi::{BidiInfo, Level as BidiLevel}; -use unicode_script::{Script, UnicodeScript}; - -use crate::layout::{AlignElem, BoxElem, HElem, Sizing, Spacing}; -use crate::math::EquationElem; -use crate::prelude::*; -use crate::text::{ - breakpoints, char_is_cjk_script, is_gb_style, shape, Breakpoint, LinebreakElem, - Quoter, Quotes, ShapedGlyph, ShapedText, SmartquoteElem, SpaceElem, TextElem, - BEGIN_PUNCT_PAT, END_PUNCT_PAT, -}; - -/// Arranges text, spacing and inline-level elements into a paragraph. -/// -/// Although this function is primarily used in set rules to affect paragraph -/// properties, it can also be used to explicitly render its argument onto a -/// paragraph of its own. -/// -/// # Example -/// ```example -/// #show par: set block(spacing: 0.65em) -/// #set par( -/// first-line-indent: 1em, -/// justify: true, -/// ) -/// -/// We proceed by contradiction. -/// Suppose that there exists a set -/// of positive integers $a$, $b$, and -/// $c$ that satisfies the equation -/// $a^n + b^n = c^n$ for some -/// integer value of $n > 2$. -/// -/// Without loss of generality, -/// let $a$ be the smallest of the -/// three integers. Then, we ... -/// ``` -#[elem(title = "Paragraph", Construct)] -pub struct ParElem { - /// The spacing between lines. - #[resolve] - #[ghost] - #[default(Em::new(0.65).into())] - pub leading: Length, - - /// Whether to justify text in its line. - /// - /// Hyphenation will be enabled for justified paragraphs if the - /// [text function's `hyphenate` property]($text.hyphenate) is set to - /// `{auto}` and the current language is known. - /// - /// Note that the current [alignment]($align) still has an effect on the - /// placement of the last line except if it ends with a - /// [justified line break]($linebreak.justify). - #[ghost] - #[default(false)] - pub justify: bool, - - /// How to determine line breaks. - /// - /// When this property is set to `{auto}`, its default value, optimized line - /// breaks will be used for justified paragraphs. Enabling optimized line - /// breaks for ragged paragraphs may also be worthwhile to improve the - /// appearance of the text. - /// - /// ```example - /// #set page(width: 207pt) - /// #set par(linebreaks: "simple") - /// Some texts feature many longer - /// words. Those are often exceedingly - /// challenging to break in a visually - /// pleasing way. - /// - /// #set par(linebreaks: "optimized") - /// Some texts feature many longer - /// words. Those are often exceedingly - /// challenging to break in a visually - /// pleasing way. - /// ``` - #[ghost] - pub linebreaks: Smart<Linebreaks>, - - /// The indent the first line of a paragraph should have. - /// - /// Only the first line of a consecutive paragraph will be indented (not - /// the first one in a block or on the page). - /// - /// By typographic convention, paragraph breaks are indicated either by some - /// space between paragraphs or by indented first lines. Consider reducing - /// the [paragraph spacing]($block.spacing) to the [`leading`] when - /// using this property (e.g. using - /// `[#show par: set block(spacing: 0.65em)]`). - #[ghost] - pub first_line_indent: Length, - - /// The indent all but the first line of a paragraph should have. - #[ghost] - #[resolve] - pub hanging_indent: Length, - - /// The contents of the paragraph. - #[external] - #[required] - pub body: Content, - - /// The paragraph's children. - #[internal] - #[variadic] - pub children: Vec<Prehashed<Content>>, -} - -impl Construct for ParElem { - fn construct(vm: &mut Vm, args: &mut Args) -> SourceResult<Content> { - // The paragraph constructor is special: It doesn't create a paragraph - // element. Instead, it just ensures that the passed content lives in a - // separate paragraph and styles it. - let styles = Self::set(vm, args)?; - let body = args.expect::<Content>("body")?; - Ok(Content::sequence([ - ParbreakElem::new().pack(), - body.styled_with_map(styles), - ParbreakElem::new().pack(), - ])) - } -} - -impl ParElem { - /// Layout the paragraph into a collection of lines. - #[tracing::instrument(name = "ParElement::layout", skip_all)] - pub fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - consecutive: bool, - region: Size, - expand: bool, - ) -> SourceResult<Fragment> { - #[comemo::memoize] - #[allow(clippy::too_many_arguments)] - fn cached( - par: &ParElem, - world: Tracked<dyn World + '_>, - introspector: Tracked<Introspector>, - locator: Tracked<Locator>, - delayed: TrackedMut<DelayedErrors>, - tracer: TrackedMut<Tracer>, - styles: StyleChain, - consecutive: bool, - region: Size, - expand: bool, - ) -> SourceResult<Fragment> { - let mut locator = Locator::chained(locator); - let mut vt = Vt { - world, - introspector, - locator: &mut locator, - delayed, - tracer, - }; - let children = par.children(); - - // Collect all text into one string for BiDi analysis. - let (text, segments, spans) = collect(children, &styles, consecutive)?; - - // Perform BiDi analysis and then prepare paragraph layout by building a - // representation on which we can do line breaking without layouting - // each and every line from scratch. - let p = prepare(&mut vt, children, &text, segments, spans, styles, region)?; - - // Break the paragraph into lines. - let lines = linebreak(&vt, &p, region.x - p.hang); - - // Stack the lines into one frame per region. - finalize(&mut vt, &p, &lines, region, expand) - } - - let fragment = cached( - self, - vt.world, - vt.introspector, - vt.locator.track(), - TrackedMut::reborrow_mut(&mut vt.delayed), - TrackedMut::reborrow_mut(&mut vt.tracer), - styles, - consecutive, - region, - expand, - )?; - - vt.locator.visit_frames(&fragment); - Ok(fragment) - } -} - -/// How to determine line breaks in a paragraph. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] -pub enum Linebreaks { - /// Determine the line breaks in a simple first-fit style. - Simple, - /// Optimize the line breaks for the whole paragraph. - /// - /// Typst will try to produce more evenly filled lines of text by - /// considering the whole paragraph when calculating line breaks. - Optimized, -} - -/// A paragraph break. -/// -/// This starts a new paragraph. Especially useful when used within code like -/// [for loops]($scripting/#loops). Multiple consecutive -/// paragraph breaks collapse into a single one. -/// -/// # Example -/// ```example -/// #for i in range(3) { -/// [Blind text #i: ] -/// lorem(5) -/// parbreak() -/// } -/// ``` -/// -/// # Syntax -/// Instead of calling this function, you can insert a blank line into your -/// markup to create a paragraph break. -#[elem(title = "Paragraph Break", Unlabellable)] -pub struct ParbreakElem {} - -impl Unlabellable for ParbreakElem {} - -/// Range of a substring of text. -type Range = std::ops::Range<usize>; - -// The characters by which spacing, inline content and pins are replaced in the -// paragraph's full text. -const SPACING_REPLACE: char = ' '; // Space -const OBJ_REPLACE: char = '\u{FFFC}'; // Object Replacement Character - -/// A paragraph representation in which children are already layouted and text -/// is already preshaped. -/// -/// In many cases, we can directly reuse these results when constructing a line. -/// Only when a line break falls onto a text index that is not safe-to-break per -/// rustybuzz, we have to reshape that portion. -pub(crate) struct Preparation<'a> { - /// Bidirectional text embedding levels for the paragraph. - pub bidi: BidiInfo<'a>, - /// Text runs, spacing and layouted elements. - pub items: Vec<Item<'a>>, - /// The span mapper. - pub spans: SpanMapper, - /// Whether to hyphenate if it's the same for all children. - pub hyphenate: Option<bool>, - /// The text language if it's the same for all children. - pub lang: Option<Lang>, - /// The paragraph's resolved horizontal alignment. - pub align: FixedAlign, - /// Whether to justify the paragraph. - pub justify: bool, - /// The paragraph's hanging indent. - pub hang: Abs, - /// Whether to add spacing between CJK and Latin characters. - pub cjk_latin_spacing: bool, - /// Whether font fallback is enabled for this paragraph. - pub fallback: bool, - /// The leading of the paragraph. - pub leading: Abs, - /// How to determine line breaks. - pub linebreaks: Smart<Linebreaks>, - /// The text size. - pub size: Abs, -} - -impl<'a> Preparation<'a> { - /// Find the item that contains the given `text_offset`. - pub fn find(&self, text_offset: usize) -> Option<&Item<'a>> { - let mut cursor = 0; - for item in &self.items { - let end = cursor + item.len(); - if (cursor..end).contains(&text_offset) { - return Some(item); - } - cursor = end; - } - None - } - - /// Return the items that intersect the given `text_range`. - /// - /// Returns the expanded range around the items and the items. - pub fn slice(&self, text_range: Range) -> (Range, &[Item<'a>]) { - let mut cursor = 0; - let mut start = 0; - let mut end = 0; - let mut expanded = text_range.clone(); - - for (i, item) in self.items.iter().enumerate() { - if cursor <= text_range.start { - start = i; - expanded.start = cursor; - } - - let len = item.len(); - if cursor < text_range.end || cursor + len <= text_range.end { - end = i + 1; - expanded.end = cursor + len; - } else { - break; - } - - cursor += len; - } - - (expanded, &self.items[start..end]) - } -} - -/// A segment of one or multiple collapsed children. -#[derive(Debug, Copy, Clone)] -enum Segment<'a> { - /// One or multiple collapsed text or text-equivalent children. Stores how - /// long the segment is (in bytes of the full text string). - Text(usize), - /// Horizontal spacing between other segments. - Spacing(Spacing), - /// A mathematical equation. - Equation(&'a EquationElem), - /// A box with arbitrary content. - Box(&'a BoxElem, bool), - /// Metadata. - Meta, -} - -impl Segment<'_> { - /// The text length of the item. - fn len(&self) -> usize { - match *self { - Self::Text(len) => len, - Self::Spacing(_) => SPACING_REPLACE.len_utf8(), - Self::Box(_, true) => SPACING_REPLACE.len_utf8(), - Self::Equation(_) | Self::Box(_, _) => OBJ_REPLACE.len_utf8(), - Self::Meta => 0, - } - } -} - -/// A prepared item in a paragraph layout. -#[derive(Debug)] -pub(crate) enum Item<'a> { - /// A shaped text run with consistent style and direction. - Text(ShapedText<'a>), - /// Absolute spacing between other items. - Absolute(Abs), - /// Fractional spacing between other items. - Fractional(Fr, Option<(&'a BoxElem, StyleChain<'a>)>), - /// Layouted inline-level content. - Frame(Frame), - /// Metadata. - Meta(Frame), -} - -impl<'a> Item<'a> { - /// If this a text item, return it. - pub fn text(&self) -> Option<&ShapedText<'a>> { - match self { - Self::Text(shaped) => Some(shaped), - _ => None, - } - } - - pub fn text_mut(&mut self) -> Option<&mut ShapedText<'a>> { - match self { - Self::Text(shaped) => Some(shaped), - _ => None, - } - } - - /// The text length of the item. - pub fn len(&self) -> usize { - match self { - Self::Text(shaped) => shaped.text.len(), - Self::Absolute(_) | Self::Fractional(_, _) => SPACING_REPLACE.len_utf8(), - Self::Frame(_) => OBJ_REPLACE.len_utf8(), - Self::Meta(_) => 0, - } - } - - /// The natural layouted width of the item. - pub fn width(&self) -> Abs { - match self { - Self::Text(shaped) => shaped.width, - Self::Absolute(v) => *v, - Self::Frame(frame) => frame.width(), - Self::Fractional(_, _) | Self::Meta(_) => Abs::zero(), - } - } -} - -/// Maps byte offsets back to spans. -#[derive(Default)] -pub struct SpanMapper(Vec<(usize, Span)>); - -impl SpanMapper { - /// Create a new span mapper. - pub fn new() -> Self { - Self::default() - } - - /// Push a span for a segment with the given length. - pub fn push(&mut self, len: usize, span: Span) { - self.0.push((len, span)); - } - - /// Determine the span at the given byte offset. - /// - /// May return a detached span. - pub fn span_at(&self, offset: usize) -> (Span, u16) { - let mut cursor = 0; - for &(len, span) in &self.0 { - if (cursor..=cursor + len).contains(&offset) { - return (span, u16::try_from(offset - cursor).unwrap_or(0)); - } - cursor += len; - } - (Span::detached(), 0) - } -} - -/// A layouted line, consisting of a sequence of layouted paragraph items that -/// are mostly borrowed from the preparation phase. This type enables you to -/// measure the size of a line in a range before committing to building the -/// line's frame. -/// -/// At most two paragraph items must be created individually for this line: The -/// first and last one since they may be broken apart by the start or end of the -/// line, respectively. But even those can partially reuse previous results when -/// the break index is safe-to-break per rustybuzz. -struct Line<'a> { - /// Bidi information about the paragraph. - bidi: &'a BidiInfo<'a>, - /// The trimmed range the line spans in the paragraph. - trimmed: Range, - /// The untrimmed end where the line ends. - end: usize, - /// A reshaped text item if the line sliced up a text item at the start. - first: Option<Item<'a>>, - /// Inner items which don't need to be reprocessed. - inner: &'a [Item<'a>], - /// A reshaped text item if the line sliced up a text item at the end. If - /// there is only one text item, this takes precedence over `first`. - last: Option<Item<'a>>, - /// The width of the line. - width: Abs, - /// Whether the line should be justified. - justify: bool, - /// Whether the line ends with a hyphen or dash, either naturally or through - /// hyphenation. - dash: bool, -} - -impl<'a> Line<'a> { - /// Iterate over the line's items. - fn items(&self) -> impl Iterator<Item = &Item<'a>> { - self.first.iter().chain(self.inner).chain(&self.last) - } - - /// Return items that intersect the given `text_range`. - fn slice(&self, text_range: Range) -> impl Iterator<Item = &Item<'a>> { - let mut cursor = self.trimmed.start; - let mut start = 0; - let mut end = 0; - - for (i, item) in self.items().enumerate() { - if cursor <= text_range.start { - start = i; - } - - let len = item.len(); - if cursor < text_range.end || cursor + len <= text_range.end { - end = i + 1; - } else { - break; - } - - cursor += len; - } - - self.items().skip(start).take(end - start) - } - - /// How many glyphs are in the text where we can insert additional - /// space when encountering underfull lines. - fn justifiables(&self) -> usize { - let mut count = 0; - for shaped in self.items().filter_map(Item::text) { - count += shaped.justifiables(); - } - // CJK character at line end should not be adjusted. - if self - .items() - .last() - .and_then(Item::text) - .map(|s| s.cjk_justifiable_at_last()) - .unwrap_or(false) - { - count -= 1; - } - - count - } - - /// How much can the line stretch - fn stretchability(&self) -> Abs { - self.items().filter_map(Item::text).map(|s| s.stretchability()).sum() - } - - /// How much can the line shrink - fn shrinkability(&self) -> Abs { - self.items().filter_map(Item::text).map(|s| s.shrinkability()).sum() - } - - /// The sum of fractions in the line. - fn fr(&self) -> Fr { - self.items() - .filter_map(|item| match item { - Item::Fractional(fr, _) => Some(*fr), - _ => None, - }) - .sum() - } -} - -/// Collect all text of the paragraph into one string. This also performs -/// string-level preprocessing like case transformations. -#[allow(clippy::type_complexity)] -fn collect<'a>( - children: &'a [Prehashed<Content>], - styles: &'a StyleChain<'a>, - consecutive: bool, -) -> SourceResult<(String, Vec<(Segment<'a>, StyleChain<'a>)>, SpanMapper)> { - let mut full = String::new(); - let mut quoter = Quoter::new(); - let mut segments = Vec::with_capacity(2 + children.len()); - let mut spans = SpanMapper::new(); - let mut iter = children.iter().map(|c| &**c).peekable(); - - let first_line_indent = ParElem::first_line_indent_in(*styles); - if !first_line_indent.is_zero() - && consecutive - && AlignElem::alignment_in(*styles).resolve(*styles).x - == TextElem::dir_in(*styles).start().into() - { - full.push(SPACING_REPLACE); - segments.push((Segment::Spacing(first_line_indent.into()), *styles)); - } - - let hang = ParElem::hanging_indent_in(*styles); - if !hang.is_zero() { - full.push(SPACING_REPLACE); - segments.push((Segment::Spacing((-hang).into()), *styles)); - } - - while let Some(mut child) = iter.next() { - let outer = styles; - let mut styles = *styles; - if let Some((elem, local)) = child.to_styled() { - child = elem; - styles = outer.chain(local); - } - - let segment = if child.is::<SpaceElem>() { - full.push(' '); - Segment::Text(1) - } else if let Some(elem) = child.to::<TextElem>() { - let prev = full.len(); - if let Some(case) = TextElem::case_in(styles) { - full.push_str(&case.apply(elem.text())); - } else { - full.push_str(elem.text()); - } - Segment::Text(full.len() - prev) - } else if let Some(elem) = child.to::<HElem>() { - if elem.amount().is_zero() { - continue; - } - - full.push(SPACING_REPLACE); - Segment::Spacing(*elem.amount()) - } else if let Some(elem) = child.to::<LinebreakElem>() { - let c = if elem.justify(styles) { '\u{2028}' } else { '\n' }; - full.push(c); - Segment::Text(c.len_utf8()) - } else if let Some(elem) = child.to::<SmartquoteElem>() { - let prev = full.len(); - if SmartquoteElem::enabled_in(styles) { - let quotes = SmartquoteElem::quotes_in(styles); - let lang = TextElem::lang_in(styles); - let region = TextElem::region_in(styles); - let quotes = Quotes::new( - quotes, - lang, - region, - SmartquoteElem::alternative_in(styles), - ); - let peeked = iter.peek().and_then(|child| { - let child = if let Some((child, _)) = child.to_styled() { - child - } else { - child - }; - if let Some(elem) = child.to::<TextElem>() { - elem.text().chars().next() - } else if child.is::<SmartquoteElem>() { - Some('"') - } else if child.is::<SpaceElem>() - || child.is::<HElem>() - || child.is::<LinebreakElem>() - { - Some(SPACING_REPLACE) - } else { - Some(OBJ_REPLACE) - } - }); - - full.push_str(quoter.quote("es, elem.double(styles), peeked)); - } else { - full.push(if elem.double(styles) { '"' } else { '\'' }); - } - Segment::Text(full.len() - prev) - } else if let Some(elem) = child.to::<EquationElem>() { - full.push(OBJ_REPLACE); - Segment::Equation(elem) - } else if let Some(elem) = child.to::<BoxElem>() { - let frac = elem.width(styles).is_fractional(); - full.push(if frac { SPACING_REPLACE } else { OBJ_REPLACE }); - Segment::Box(elem, frac) - } else if child.is::<MetaElem>() { - Segment::Meta - } else { - bail!(child.span(), "unexpected paragraph child"); - }; - - if let Some(last) = full.chars().last() { - quoter.last(last, child.is::<SmartquoteElem>()); - } - - spans.push(segment.len(), child.span()); - - if let (Some((Segment::Text(last_len), last_styles)), Segment::Text(len)) = - (segments.last_mut(), segment) - { - if *last_styles == styles { - *last_len += len; - continue; - } - } - - segments.push((segment, styles)); - } - - Ok((full, segments, spans)) -} - -/// Prepare paragraph layout by shaping the whole paragraph and layouting all -/// contained inline-level content. -fn prepare<'a>( - vt: &mut Vt, - children: &'a [Prehashed<Content>], - text: &'a str, - segments: Vec<(Segment<'a>, StyleChain<'a>)>, - spans: SpanMapper, - styles: StyleChain<'a>, - region: Size, -) -> SourceResult<Preparation<'a>> { - let dir = TextElem::dir_in(styles); - let bidi = BidiInfo::new( - text, - match dir { - Dir::LTR => Some(BidiLevel::ltr()), - Dir::RTL => Some(BidiLevel::rtl()), - _ => None, - }, - ); - - let mut cursor = 0; - let mut items = Vec::with_capacity(segments.len()); - - // Shape / layout the children and collect them into items. - for (segment, styles) in segments { - let end = cursor + segment.len(); - match segment { - Segment::Text(_) => { - shape_range(&mut items, vt, &bidi, cursor..end, &spans, styles); - } - Segment::Spacing(spacing) => match spacing { - Spacing::Rel(v) => { - let resolved = v.resolve(styles).relative_to(region.x); - items.push(Item::Absolute(resolved)); - } - Spacing::Fr(v) => { - items.push(Item::Fractional(v, None)); - } - }, - Segment::Equation(equation) => { - let pod = Regions::one(region, Axes::splat(false)); - let mut frame = equation.layout(vt, styles, pod)?.into_frame(); - frame.translate(Point::with_y(TextElem::baseline_in(styles))); - items.push(Item::Frame(frame)); - } - Segment::Box(elem, _) => { - if let Sizing::Fr(v) = elem.width(styles) { - items.push(Item::Fractional(v, Some((elem, styles)))); - } else { - let pod = Regions::one(region, Axes::splat(false)); - let mut frame = elem.layout(vt, styles, pod)?.into_frame(); - frame.translate(Point::with_y(TextElem::baseline_in(styles))); - items.push(Item::Frame(frame)); - } - } - Segment::Meta => { - let mut frame = Frame::soft(Size::zero()); - frame.meta(styles, true); - items.push(Item::Meta(frame)); - } - } - - cursor = end; - } - - let cjk_latin_spacing = TextElem::cjk_latin_spacing_in(styles).is_auto(); - if cjk_latin_spacing { - add_cjk_latin_spacing(&mut items); - } - - Ok(Preparation { - bidi, - items, - spans, - hyphenate: shared_get(styles, children, TextElem::hyphenate_in), - lang: shared_get(styles, children, TextElem::lang_in), - align: AlignElem::alignment_in(styles).resolve(styles).x, - justify: ParElem::justify_in(styles), - hang: ParElem::hanging_indent_in(styles), - cjk_latin_spacing, - fallback: TextElem::fallback_in(styles), - leading: ParElem::leading_in(styles), - linebreaks: ParElem::linebreaks_in(styles), - size: TextElem::size_in(styles), - }) -} - -/// Add some spacing between Han characters and western characters. -/// See Requirements for Chinese Text Layout, Section 3.2.2 Mixed Text Composition in Horizontal -/// Written Mode -fn add_cjk_latin_spacing(items: &mut [Item]) { - let mut items = items.iter_mut().filter(|x| !matches!(x, Item::Meta(_))).peekable(); - let mut prev: Option<&ShapedGlyph> = None; - while let Some(item) = items.next() { - let Some(text) = item.text_mut() else { - prev = None; - continue; - }; - - // Since we only call this function in [`prepare`], we can assume - // that the Cow is owned, and `to_mut` can be called without overhead. - debug_assert!(matches!(text.glyphs, std::borrow::Cow::Owned(_))); - let mut glyphs = text.glyphs.to_mut().iter_mut().peekable(); - - while let Some(glyph) = glyphs.next() { - let next = glyphs.peek().map(|n| n as _).or_else(|| { - items - .peek() - .and_then(|i| i.text()) - .and_then(|shaped| shaped.glyphs.first()) - }); - - // Case 1: CJK followed by a Latin character - if glyph.is_cjk_script() && next.map_or(false, |g| g.is_letter_or_number()) { - // The spacing is default to 1/4 em, and can be shrunk to 1/8 em. - glyph.x_advance += Em::new(0.25); - glyph.adjustability.shrinkability.1 += Em::new(0.125); - text.width += Em::new(0.25).at(text.size); - } - - // Case 2: Latin followed by a CJK character - if glyph.is_cjk_script() && prev.map_or(false, |g| g.is_letter_or_number()) { - glyph.x_advance += Em::new(0.25); - glyph.x_offset += Em::new(0.25); - glyph.adjustability.shrinkability.0 += Em::new(0.125); - text.width += Em::new(0.25).at(text.size); - } - - prev = Some(glyph); - } - } -} - -/// Group a range of text by BiDi level and script, shape the runs and generate -/// items for them. -fn shape_range<'a>( - items: &mut Vec<Item<'a>>, - vt: &Vt, - bidi: &BidiInfo<'a>, - range: Range, - spans: &SpanMapper, - styles: StyleChain<'a>, -) { - let script = TextElem::script_in(styles); - let lang = TextElem::lang_in(styles); - let region = TextElem::region_in(styles); - let mut process = |range: Range, level: BidiLevel| { - let dir = if level.is_ltr() { Dir::LTR } else { Dir::RTL }; - let shaped = - shape(vt, range.start, &bidi.text[range], spans, styles, dir, lang, region); - items.push(Item::Text(shaped)); - }; - - let mut prev_level = BidiLevel::ltr(); - let mut prev_script = Script::Unknown; - let mut cursor = range.start; - - // Group by embedding level and script. If the text's script is explicitly - // set (rather than inferred from the glyphs), we keep the script at an - // unchanging `Script::Unknown` so that only level changes cause breaks. - for i in range.clone() { - if !bidi.text.is_char_boundary(i) { - continue; - } - - let level = bidi.levels[i]; - let curr_script = match script { - Smart::Auto => { - bidi.text[i..].chars().next().map_or(Script::Unknown, |c| c.script()) - } - Smart::Custom(_) => Script::Unknown, - }; - - if level != prev_level || !is_compatible(curr_script, prev_script) { - if cursor < i { - process(cursor..i, prev_level); - } - cursor = i; - prev_level = level; - prev_script = curr_script; - } else if is_generic_script(prev_script) { - prev_script = curr_script; - } - } - - process(cursor..range.end, prev_level); -} - -/// Whether this is not a specific script. -fn is_generic_script(script: Script) -> bool { - matches!(script, Script::Unknown | Script::Common | Script::Inherited) -} - -/// Whether these script can be part of the same shape run. -fn is_compatible(a: Script, b: Script) -> bool { - is_generic_script(a) || is_generic_script(b) || a == b -} - -/// Get a style property, but only if it is the same for all children of the -/// paragraph. -fn shared_get<T: PartialEq>( - styles: StyleChain<'_>, - children: &[Prehashed<Content>], - getter: fn(StyleChain) -> T, -) -> Option<T> { - let value = getter(styles); - children - .iter() - .filter_map(|child| child.to_styled()) - .all(|(_, local)| getter(styles.chain(local)) == value) - .then_some(value) -} - -/// Find suitable linebreaks. -fn linebreak<'a>(vt: &Vt, p: &'a Preparation<'a>, width: Abs) -> Vec<Line<'a>> { - let linebreaks = p.linebreaks.unwrap_or_else(|| { - if p.justify { - Linebreaks::Optimized - } else { - Linebreaks::Simple - } - }); - - match linebreaks { - Linebreaks::Simple => linebreak_simple(vt, p, width), - Linebreaks::Optimized => linebreak_optimized(vt, p, width), - } -} - -/// Perform line breaking in simple first-fit style. This means that we build -/// lines greedily, always taking the longest possible line. This may lead to -/// very unbalanced line, but is fast and simple. -fn linebreak_simple<'a>(vt: &Vt, p: &'a Preparation<'a>, width: Abs) -> Vec<Line<'a>> { - let mut lines = Vec::with_capacity(16); - let mut start = 0; - let mut last = None; - - breakpoints(p, |end, breakpoint| { - // Compute the line and its size. - let mut attempt = line(vt, p, start..end, breakpoint); - - // If the line doesn't fit anymore, we push the last fitting attempt - // into the stack and rebuild the line from the attempt's end. The - // resulting line cannot be broken up further. - if !width.fits(attempt.width) { - if let Some((last_attempt, last_end)) = last.take() { - lines.push(last_attempt); - start = last_end; - attempt = line(vt, p, start..end, breakpoint); - } - } - - // Finish the current line if there is a mandatory line break (i.e. - // due to "\n") or if the line doesn't fit horizontally already - // since then no shorter line will be possible. - if breakpoint == Breakpoint::Mandatory || !width.fits(attempt.width) { - lines.push(attempt); - start = end; - last = None; - } else { - last = Some((attempt, end)); - } - }); - - if let Some((line, _)) = last { - lines.push(line); - } - - lines -} - -/// Perform line breaking in optimized Knuth-Plass style. Here, we use more -/// context to determine the line breaks than in the simple first-fit style. For -/// example, we might choose to cut a line short even though there is still a -/// bit of space to improve the fit of one of the following lines. The -/// Knuth-Plass algorithm is based on the idea of "cost". A line which has a -/// very tight or very loose fit has a higher cost than one that is just right. -/// Ending a line with a hyphen incurs extra cost and endings two successive -/// lines with hyphens even more. -/// -/// To find the layout with the minimal total cost the algorithm uses dynamic -/// programming: For each possible breakpoint it determines the optimal -/// paragraph layout _up to that point_. It walks over all possible start points -/// for a line ending at that point and finds the one for which the cost of the -/// line plus the cost of the optimal paragraph up to the start point (already -/// computed and stored in dynamic programming table) is minimal. The final -/// result is simply the layout determined for the last breakpoint at the end of -/// text. -fn linebreak_optimized<'a>(vt: &Vt, p: &'a Preparation<'a>, width: Abs) -> Vec<Line<'a>> { - /// The cost of a line or paragraph layout. - type Cost = f64; - - /// An entry in the dynamic programming table. - struct Entry<'a> { - pred: usize, - total: Cost, - line: Line<'a>, - } - - // Cost parameters. - const HYPH_COST: Cost = 0.5; - const RUNT_COST: Cost = 0.5; - const CONSECUTIVE_DASH_COST: Cost = 0.3; - const MAX_COST: Cost = 1_000_000.0; - const MIN_RATIO: f64 = -1.0; - - // Dynamic programming table. - let mut active = 0; - let mut table = vec![Entry { - pred: 0, - total: 0.0, - line: line(vt, p, 0..0, Breakpoint::Mandatory), - }]; - - let em = p.size; - let mut lines = Vec::with_capacity(16); - breakpoints(p, |end, breakpoint| { - let k = table.len(); - let eof = end == p.bidi.text.len(); - let mut best: Option<Entry> = None; - - // Find the optimal predecessor. - for (i, pred) in table.iter().enumerate().skip(active) { - // Layout the line. - let start = pred.line.end; - - let attempt = line(vt, p, start..end, breakpoint); - - // Determine how much the line's spaces would need to be stretched - // to make it the desired width. - let delta = width - attempt.width; - // Determine how much stretch are permitted. - let adjust = if delta >= Abs::zero() { - attempt.stretchability() - } else { - attempt.shrinkability() - }; - // Ideally, the ratio should between -1.0 and 1.0, but sometimes a value above 1.0 - // is possible, in which case the line is underfull. - let mut ratio = delta / adjust; - if ratio.is_nan() { - // The line is not stretchable, but it just fits. - // This often happens with monospace fonts and CJK texts. - ratio = 0.0; - } - if ratio > 1.0 { - // We should stretch the line above its stretchability. Now - // calculate the extra amount. Also, don't divide by zero. - let extra_stretch = - (delta - adjust) / attempt.justifiables().max(1) as f64; - // Normalize the amount by half Em size. - ratio = 1.0 + extra_stretch / (em / 2.0); - } - - // Determine the cost of the line. - let min_ratio = if p.justify { MIN_RATIO } else { 0.0 }; - let mut cost = if ratio < min_ratio { - // The line is overfull. This is the case if - // - justification is on, but we'd need to shrink too much - // - justification is off and the line just doesn't fit - // - // If this is the earliest breakpoint in the active set - // (active == i), remove it from the active set. If there is an - // earlier one (active < i), then the logically shorter line was - // in fact longer (can happen with negative spacing) and we - // can't trim the active set just yet. - if active == i { - active += 1; - } - MAX_COST - } else if breakpoint == Breakpoint::Mandatory || eof { - // This is a mandatory break and the line is not overfull, so - // all breakpoints before this one become inactive since no line - // can span above the mandatory break. - active = k; - // If ratio > 0, we need to stretch the line only when justify is needed. - // If ratio < 0, we always need to shrink the line. - if (ratio > 0.0 && attempt.justify) || ratio < 0.0 { - ratio.powi(3).abs() - } else { - 0.0 - } - } else { - // Normal line with cost of |ratio^3|. - ratio.powi(3).abs() - }; - - // Penalize runts. - if k == i + 1 && eof { - cost += RUNT_COST; - } - - // Penalize hyphens. - if breakpoint == Breakpoint::Hyphen { - cost += HYPH_COST; - } - - // In Knuth paper, cost = (1 + 100|r|^3 + p)^2 + a, - // where r is the ratio, p=50 is the penalty, and a=3000 is consecutive the penalty. - // We divide the whole formula by 10, resulting (0.01 + |r|^3 + p)^2 + a, - // where p=0.5 and a=0.3 - cost = (0.01 + cost).powi(2); - - // Penalize two consecutive dashes (not necessarily hyphens) extra. - if attempt.dash && pred.line.dash { - cost += CONSECUTIVE_DASH_COST; - } - - // The total cost of this line and its chain of predecessors. - let total = pred.total + cost; - - // If this attempt is better than what we had before, take it! - if best.as_ref().map_or(true, |best| best.total >= total) { - best = Some(Entry { pred: i, total, line: attempt }); - } - } - - table.push(best.unwrap()); - }); - - // Retrace the best path. - let mut idx = table.len() - 1; - while idx != 0 { - table.truncate(idx + 1); - let entry = table.pop().unwrap(); - lines.push(entry.line); - idx = entry.pred; - } - - lines.reverse(); - lines -} - -/// Create a line which spans the given range. -fn line<'a>( - vt: &Vt, - p: &'a Preparation, - mut range: Range, - breakpoint: Breakpoint, -) -> Line<'a> { - let end = range.end; - let mut justify = - p.justify && end < p.bidi.text.len() && breakpoint != Breakpoint::Mandatory; - - if range.is_empty() { - return Line { - bidi: &p.bidi, - end, - trimmed: range, - first: None, - inner: &[], - last: None, - width: Abs::zero(), - justify, - dash: false, - }; - } - - // Slice out the relevant items. - let (expanded, mut inner) = p.slice(range.clone()); - let mut width = Abs::zero(); - - // Reshape the last item if it's split in half or hyphenated. - let mut last = None; - let mut dash = false; - if let Some((Item::Text(shaped), before)) = inner.split_last() { - // Compute the range we want to shape, trimming whitespace at the - // end of the line. - let base = expanded.end - shaped.text.len(); - let start = range.start.max(base); - let text = &p.bidi.text[start..range.end]; - // U+200B ZERO WIDTH SPACE is used to provide a line break opportunity, - // we want to trim it too. - let trimmed = text.trim_end().trim_end_matches('\u{200B}'); - range.end = start + trimmed.len(); - - // Deal with hyphens, dashes and justification. - let shy = trimmed.ends_with('\u{ad}'); - let hyphen = breakpoint == Breakpoint::Hyphen; - dash = hyphen || shy || trimmed.ends_with(['-', '–', '—']); - justify |= text.ends_with('\u{2028}'); - - // Deal with CJK punctuation at line ends. - let gb_style = is_gb_style(shaped.lang, shaped.region); - let maybe_adjust_last_glyph = trimmed.ends_with(END_PUNCT_PAT) - || (p.cjk_latin_spacing && trimmed.ends_with(char_is_cjk_script)); - - // Usually, we don't want to shape an empty string because: - // - We don't want the height of trimmed whitespace in a different - // font to be considered for the line height. - // - Even if it's in the same font, its unnecessary. - // - // There is one exception though. When the whole line is empty, we - // need the shaped empty string to make the line the appropriate - // height. That is the case exactly if the string is empty and there - // are no other items in the line. - if hyphen || start + shaped.text.len() > range.end || maybe_adjust_last_glyph { - if hyphen || start < range.end || before.is_empty() { - let mut reshaped = shaped.reshape(vt, &p.spans, start..range.end); - if hyphen || shy { - reshaped.push_hyphen(vt, p.fallback); - } - - if let Some(last_glyph) = reshaped.glyphs.last() { - if last_glyph.is_cjk_left_aligned_punctuation(gb_style) { - // If the last glyph is a CJK punctuation, we want to shrink it. - // See Requirements for Chinese Text Layout, Section 3.1.6.3 - // Compression of punctuation marks at line start or line end - let shrink_amount = last_glyph.shrinkability().1; - let punct = reshaped.glyphs.to_mut().last_mut().unwrap(); - punct.shrink_right(shrink_amount); - reshaped.width -= shrink_amount.at(reshaped.size); - } else if p.cjk_latin_spacing - && last_glyph.is_cjk_script() - && (last_glyph.x_advance - last_glyph.x_offset) > Em::one() - { - // If the last glyph is a CJK character adjusted by [`add_cjk_latin_spacing`], - // restore the original width. - let shrink_amount = - last_glyph.x_advance - last_glyph.x_offset - Em::one(); - let glyph = reshaped.glyphs.to_mut().last_mut().unwrap(); - glyph.x_advance -= shrink_amount; - glyph.adjustability.shrinkability.1 = Em::zero(); - reshaped.width -= shrink_amount.at(reshaped.size); - } - } - - width += reshaped.width; - last = Some(Item::Text(reshaped)); - } - - inner = before; - } - } - - // Deal with CJK characters at line starts. - let text = &p.bidi.text[range.start..end]; - let maybe_adjust_first_glyph = text.starts_with(BEGIN_PUNCT_PAT) - || (p.cjk_latin_spacing && text.starts_with(char_is_cjk_script)); - - // Reshape the start item if it's split in half. - let mut first = None; - if let Some((Item::Text(shaped), after)) = inner.split_first() { - // Compute the range we want to shape. - let base = expanded.start; - let end = range.end.min(base + shaped.text.len()); - - // Reshape if necessary. - if range.start + shaped.text.len() > end || maybe_adjust_first_glyph { - // If the range is empty, we don't want to push an empty text item. - if range.start < end { - let reshaped = shaped.reshape(vt, &p.spans, range.start..end); - width += reshaped.width; - first = Some(Item::Text(reshaped)); - } - - inner = after; - } - } - - if maybe_adjust_first_glyph { - let reshaped = first.as_mut().or(last.as_mut()).and_then(Item::text_mut); - if let Some(reshaped) = reshaped { - if let Some(first_glyph) = reshaped.glyphs.first() { - if first_glyph.is_cjk_right_aligned_punctuation() { - // If the first glyph is a CJK punctuation, we want to shrink it. - let shrink_amount = first_glyph.shrinkability().0; - let glyph = reshaped.glyphs.to_mut().first_mut().unwrap(); - glyph.shrink_left(shrink_amount); - let amount_abs = shrink_amount.at(reshaped.size); - reshaped.width -= amount_abs; - width -= amount_abs; - } else if p.cjk_latin_spacing - && first_glyph.is_cjk_script() - && first_glyph.x_offset > Em::zero() - { - // If the first glyph is a CJK character adjusted by [`add_cjk_latin_spacing`], - // restore the original width. - let shrink_amount = first_glyph.x_offset; - let glyph = reshaped.glyphs.to_mut().first_mut().unwrap(); - glyph.x_advance -= shrink_amount; - glyph.x_offset = Em::zero(); - glyph.adjustability.shrinkability.0 = Em::zero(); - let amount_abs = shrink_amount.at(reshaped.size); - reshaped.width -= amount_abs; - width -= amount_abs; - } - } - } - } - - // Measure the inner items. - for item in inner { - width += item.width(); - } - - Line { - bidi: &p.bidi, - trimmed: range, - end, - first, - inner, - last, - width, - justify, - dash, - } -} - -/// Combine layouted lines into one frame per region. -fn finalize( - vt: &mut Vt, - p: &Preparation, - lines: &[Line], - region: Size, - expand: bool, -) -> SourceResult<Fragment> { - // Determine the paragraph's width: Full width of the region if we - // should expand or there's fractional spacing, fit-to-width otherwise. - let width = if !region.x.is_finite() - || (!expand && lines.iter().all(|line| line.fr().is_zero())) - { - region - .x - .min(p.hang + lines.iter().map(|line| line.width).max().unwrap_or_default()) - } else { - region.x - }; - - // Stack the lines into one frame per region. - let mut frames: Vec<Frame> = lines - .iter() - .map(|line| commit(vt, p, line, width, region.y)) - .collect::<SourceResult<_>>()?; - - // Prevent orphans. - if frames.len() >= 2 && !frames[1].is_empty() { - let second = frames.remove(1); - let first = &mut frames[0]; - merge(first, second, p.leading); - } - - // Prevent widows. - let len = frames.len(); - if len >= 2 && !frames[len - 2].is_empty() { - let second = frames.pop().unwrap(); - let first = frames.last_mut().unwrap(); - merge(first, second, p.leading); - } - - Ok(Fragment::frames(frames)) -} - -/// Merge two line frames -fn merge(first: &mut Frame, second: Frame, leading: Abs) { - let offset = first.height() + leading; - let total = offset + second.height(); - first.push_frame(Point::with_y(offset), second); - first.size_mut().y = total; -} - -/// Commit to a line and build its frame. -fn commit( - vt: &mut Vt, - p: &Preparation, - line: &Line, - width: Abs, - full: Abs, -) -> SourceResult<Frame> { - let mut remaining = width - line.width - p.hang; - let mut offset = Abs::zero(); - - // Reorder the line from logical to visual order. - let (reordered, starts_rtl) = reorder(line); - if !starts_rtl { - offset += p.hang; - } - - // Handle hanging punctuation to the left. - if let Some(Item::Text(text)) = reordered.first() { - if let Some(glyph) = text.glyphs.first() { - if !text.dir.is_positive() - && TextElem::overhang_in(text.styles) - && (reordered.len() > 1 || text.glyphs.len() > 1) - { - let amount = overhang(glyph.c) * glyph.x_advance.at(text.size); - offset -= amount; - remaining += amount; - } - } - } - - // Handle hanging punctuation to the right. - if let Some(Item::Text(text)) = reordered.last() { - if let Some(glyph) = text.glyphs.last() { - if text.dir.is_positive() - && TextElem::overhang_in(text.styles) - && (reordered.len() > 1 || text.glyphs.len() > 1) - { - let amount = overhang(glyph.c) * glyph.x_advance.at(text.size); - remaining += amount; - } - } - } - - // Determine how much additional space is needed. - // The justicication_ratio is for the first step justification, - // extra_justification is for the last step. - // For more info on multi-step justification, see Procedures for Inter- - // Character Space Expansion in W3C document Chinese Layout Requirements. - let fr = line.fr(); - let mut justification_ratio = 0.0; - let mut extra_justification = Abs::zero(); - - let shrink = line.shrinkability(); - let stretch = line.stretchability(); - if remaining < Abs::zero() && shrink > Abs::zero() { - // Attempt to reduce the length of the line, using shrinkability. - justification_ratio = (remaining / shrink).max(-1.0); - remaining = (remaining + shrink).min(Abs::zero()); - } else if line.justify && fr.is_zero() { - // Attempt to increase the length of the line, using stretchability. - if stretch > Abs::zero() { - justification_ratio = (remaining / stretch).min(1.0); - remaining = (remaining - stretch).max(Abs::zero()); - } - - let justifiables = line.justifiables(); - if justifiables > 0 && remaining > Abs::zero() { - // Underfull line, distribute the extra space. - extra_justification = remaining / justifiables as f64; - remaining = Abs::zero(); - } - } - - let mut top = Abs::zero(); - let mut bottom = Abs::zero(); - - // Build the frames and determine the height and baseline. - let mut frames = vec![]; - for item in reordered { - let mut push = |offset: &mut Abs, frame: Frame| { - let width = frame.width(); - top.set_max(frame.baseline()); - bottom.set_max(frame.size().y - frame.baseline()); - frames.push((*offset, frame)); - *offset += width; - }; - - match item { - Item::Absolute(v) => { - offset += *v; - } - Item::Fractional(v, elem) => { - 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(vt, *styles, pod)?.into_frame(); - frame.translate(Point::with_y(TextElem::baseline_in(*styles))); - push(&mut offset, frame); - } else { - offset += amount; - } - } - Item::Text(shaped) => { - let frame = shaped.build(vt, justification_ratio, extra_justification); - push(&mut offset, frame); - } - Item::Frame(frame) | Item::Meta(frame) => { - push(&mut offset, frame.clone()); - } - } - } - - // Remaining space is distributed now. - if !fr.is_zero() { - remaining = Abs::zero(); - } - - let size = Size::new(width, top + bottom); - let mut output = Frame::soft(size); - output.set_baseline(top); - - // Construct the line's frame. - for (offset, frame) in frames { - let x = offset + p.align.position(remaining); - let y = top - frame.baseline(); - output.push_frame(Point::new(x, y), frame); - } - - Ok(output) -} - -/// Return a line's items in visual order. -fn reorder<'a>(line: &'a Line<'a>) -> (Vec<&Item<'a>>, bool) { - let mut reordered = vec![]; - - // The bidi crate doesn't like empty lines. - if line.trimmed.is_empty() { - return (line.slice(line.trimmed.clone()).collect(), false); - } - - // Find the paragraph that contains the line. - let para = line - .bidi - .paragraphs - .iter() - .find(|para| para.range.contains(&line.trimmed.start)) - .unwrap(); - - // Compute the reordered ranges in visual order (left to right). - let (levels, runs) = line.bidi.visual_runs(para, line.trimmed.clone()); - let starts_rtl = levels.first().map_or(false, |level| level.is_rtl()); - - // Collect the reordered items. - for run in runs { - // Skip reset L1 runs because handling them would require reshaping - // again in some cases. - if line.bidi.levels[run.start] != levels[run.start] { - continue; - } - - let prev = reordered.len(); - reordered.extend(line.slice(run.clone())); - - if levels[run.start].is_rtl() { - reordered[prev..].reverse(); - } - } - - (reordered, starts_rtl) -} - -/// How much a character should hang into the end margin. -/// -/// For more discussion, see: -/// https://recoveringphysicist.com/21/ -fn overhang(c: char) -> f64 { - match c { - // Dashes. - '–' | '—' => 0.2, - '-' => 0.55, - - // Punctuation. - '.' | ',' => 0.8, - ':' | ';' => 0.3, - - // Arabic - '\u{60C}' | '\u{6D4}' => 0.4, - - _ => 0.0, - } -} diff --git a/crates/typst-library/src/layout/place.rs b/crates/typst-library/src/layout/place.rs deleted file mode 100644 index c8e83383..00000000 --- a/crates/typst-library/src/layout/place.rs +++ /dev/null @@ -1,124 +0,0 @@ -use crate::prelude::*; - -/// Places content at an absolute position. -/// -/// Placed content will not affect the position of other content. Place is -/// always relative to its parent container and will be in the foreground of all -/// other content in the container. Page margins will be respected. -/// -/// -/// # Example -/// ```example -/// #set page(height: 60pt) -/// Hello, world! -/// -/// #place( -/// top + right, -/// square( -/// width: 20pt, -/// stroke: 2pt + blue -/// ), -/// ) -/// ``` -#[elem(Layout, Behave)] -pub struct PlaceElem { - /// Relative to which position in the parent container to place the content. - /// - /// Cannot be `{auto}` if `float` is `{false}` and must be either - /// `{auto}`, `{top}`, or `{bottom}` if `float` is `{true}`. - /// - /// When an axis of the page is `{auto}` sized, all alignments relative to - /// that axis will be ignored, instead, the item will be placed in the - /// origin of the axis. - #[positional] - #[default(Smart::Custom(Align::START))] - pub alignment: Smart<Align>, - - /// Whether the placed element has floating layout. - /// - /// Floating elements are positioned at the top or bottom of the page, - /// displacing in-flow content. - /// - /// ```example - /// #set page(height: 150pt) - /// #let note(where, body) = place( - /// center + where, - /// float: true, - /// clearance: 6pt, - /// rect(body), - /// ) - /// - /// #lorem(10) - /// #note(bottom)[Bottom 1] - /// #note(bottom)[Bottom 2] - /// #lorem(40) - /// #note(top)[Top] - /// #lorem(10) - /// ``` - pub float: bool, - - /// The amount of clearance the placed element has in a floating layout. - #[default(Em::new(1.5).into())] - #[resolve] - pub clearance: Length, - - /// The horizontal displacement of the placed content. - /// - /// ```example - /// #set page(height: 100pt) - /// #for i in range(16) { - /// let amount = i * 4pt - /// place(center, dx: amount - 32pt, dy: amount)[A] - /// } - /// ``` - pub dx: Rel<Length>, - - /// The vertical displacement of the placed content. - pub dy: Rel<Length>, - - /// The content to place. - #[required] - pub body: Content, -} - -impl Layout for PlaceElem { - #[tracing::instrument(name = "PlaceElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - // The pod is the base area of the region because for absolute - // placement we don't really care about the already used area. - let base = regions.base(); - let float = self.float(styles); - let alignment = self.alignment(styles); - - if float - && alignment - .map_or(false, |align| matches!(align.y(), None | Some(VAlign::Horizon))) - { - bail!(self.span(), "floating placement must be `auto`, `top`, or `bottom`"); - } else if !float && alignment.is_auto() { - return Err("automatic positioning is only available for floating placement") - .hint("you can enable floating placement with `place(float: true, ..)`") - .at(self.span()); - } - - let child = self - .body() - .clone() - .aligned(alignment.unwrap_or_else(|| Align::CENTER)); - - let pod = Regions::one(base, Axes::splat(false)); - let frame = child.layout(vt, styles, pod)?.into_frame(); - Ok(Fragment::frame(frame)) - } -} - -impl Behave for PlaceElem { - fn behaviour(&self) -> Behaviour { - Behaviour::Ignorant - } -} diff --git a/crates/typst-library/src/layout/regions.rs b/crates/typst-library/src/layout/regions.rs deleted file mode 100644 index 6dd549b0..00000000 --- a/crates/typst-library/src/layout/regions.rs +++ /dev/null @@ -1,144 +0,0 @@ -use std::fmt::{self, Debug, Formatter}; - -use typst::geom::{Abs, Axes, Size}; - -/// A sequence of regions to layout into. -#[derive(Copy, Clone, Hash)] -pub struct Regions<'a> { - /// The remaining size of the first region. - pub size: Size, - /// The full height of the region for relative sizing. - pub full: Abs, - /// The height of followup regions. The width is the same for all regions. - pub backlog: &'a [Abs], - /// The height of the final region that is repeated once the backlog is - /// drained. The width is the same for all regions. - pub last: Option<Abs>, - /// Whether elements should expand to fill the regions instead of shrinking - /// to fit the content. - pub expand: Axes<bool>, - /// Whether these are the root regions or direct descendants. - /// - /// True for the padded page regions and columns directly in the page, - /// false otherwise. - pub root: bool, -} - -impl Regions<'_> { - /// Create a new region sequence with exactly one region. - pub fn one(size: Size, expand: Axes<bool>) -> Self { - Self { - size, - full: size.y, - backlog: &[], - last: None, - expand, - root: false, - } - } - - /// Create a new sequence of same-size regions that repeats indefinitely. - pub fn repeat(size: Size, expand: Axes<bool>) -> Self { - Self { - size, - full: size.y, - backlog: &[], - last: Some(size.y), - expand, - root: false, - } - } - - /// The base size, which doesn't take into account that the regions is - /// already partially used up. - /// - /// This is also used for relative sizing. - pub fn base(&self) -> Size { - Size::new(self.size.x, self.full) - } - - /// Create new regions where all sizes are mapped with `f`. - /// - /// Note that since all regions must have the same width, the width returned - /// by `f` is ignored for the backlog and the final region. - pub fn map<'v, F>(&self, backlog: &'v mut Vec<Abs>, mut f: F) -> Regions<'v> - where - F: FnMut(Size) -> Size, - { - let x = self.size.x; - backlog.clear(); - backlog.extend(self.backlog.iter().map(|&y| f(Size::new(x, y)).y)); - Regions { - size: f(self.size), - full: f(Size::new(x, self.full)).y, - backlog, - last: self.last.map(|y| f(Size::new(x, y)).y), - expand: self.expand, - root: false, - } - } - - /// Whether the first region is full and a region break is called for. - pub fn is_full(&self) -> bool { - Abs::zero().fits(self.size.y) && !self.in_last() - } - - /// Whether the first region is the last usable region. - /// - /// If this is true, calling `next()` will have no effect. - pub fn in_last(&self) -> bool { - self.backlog.is_empty() && self.last.map_or(true, |height| self.size.y == height) - } - - /// The same regions, but with different `root` configuration. - pub fn with_root(self, root: bool) -> Self { - Self { root, ..self } - } - - /// Advance to the next region if there is any. - pub fn next(&mut self) { - if let Some(height) = self - .backlog - .split_first() - .map(|(first, tail)| { - self.backlog = tail; - *first - }) - .or(self.last) - { - self.size.y = height; - self.full = height; - } - } - - /// An iterator that returns the sizes of the first and all following - /// regions, equivalently to what would be produced by calling - /// [`next()`](Self::next) repeatedly until all regions are exhausted. - /// This iterator may be infinite. - pub fn iter(&self) -> impl Iterator<Item = Size> + '_ { - let first = std::iter::once(self.size); - let backlog = self.backlog.iter(); - let last = self.last.iter().cycle(); - first.chain(backlog.chain(last).map(|&h| Size::new(self.size.x, h))) - } -} - -impl Debug for Regions<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - f.write_str("Regions ")?; - let mut list = f.debug_list(); - let mut prev = self.size.y; - list.entry(&self.size); - for &height in self.backlog { - list.entry(&Size::new(self.size.x, height)); - prev = height; - } - if let Some(last) = self.last { - if last != prev { - list.entry(&Size::new(self.size.x, last)); - } - list.entry(&(..)); - } - list.finish() - } -} diff --git a/crates/typst-library/src/layout/repeat.rs b/crates/typst-library/src/layout/repeat.rs deleted file mode 100644 index ce31164e..00000000 --- a/crates/typst-library/src/layout/repeat.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::layout::AlignElem; -use crate::prelude::*; - -/// Repeats content to the available space. -/// -/// This can be useful when implementing a custom index, reference, or outline. -/// -/// Space may be inserted between the instances of the body parameter, so be -/// sure to include negative space if you need the instances to overlap. -/// -/// Errors if there no bounds on the available space, as it would create -/// infinite content. -/// -/// # Example -/// ```example -/// Sign on the dotted line: -/// #box(width: 1fr, repeat[.]) -/// -/// #set text(10pt) -/// #v(8pt, weak: true) -/// #align(right)[ -/// Berlin, the 22nd of December, 2022 -/// ] -/// ``` -#[elem(Layout)] -pub struct RepeatElem { - /// The content to repeat. - #[required] - pub body: Content, -} - -impl Layout for RepeatElem { - #[tracing::instrument(name = "RepeatElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - let pod = Regions::one(regions.size, Axes::new(false, false)); - let piece = self.body().layout(vt, 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)) - } -} diff --git a/crates/typst-library/src/layout/spacing.rs b/crates/typst-library/src/layout/spacing.rs deleted file mode 100644 index 88b6e2cd..00000000 --- a/crates/typst-library/src/layout/spacing.rs +++ /dev/null @@ -1,262 +0,0 @@ -use std::borrow::Cow; - -use crate::prelude::*; - -/// Inserts horizontal spacing into a paragraph. -/// -/// The spacing can be absolute, relative, or fractional. In the last case, the -/// remaining space on the line is distributed among all fractional spacings -/// according to their relative fractions. -/// -/// # Example -/// ```example -/// First #h(1cm) Second \ -/// First #h(30%) Second \ -/// First #h(2fr) Second #h(1fr) Third -/// ``` -/// -/// # Mathematical Spacing { #math-spacing } -/// In [mathematical formulas]($category/math), you can additionally use these -/// constants to add spacing between elements: `thin`, `med`, `thick`, `quad`, `wide`. -#[elem(title = "Spacing (H)", Behave)] -pub struct HElem { - /// How much spacing to insert. - #[required] - pub amount: Spacing, - - /// If `{true}`, the spacing collapses at the start or end of a paragraph. - /// Moreover, from multiple adjacent weak spacings all but the largest one - /// collapse. - /// - /// Weak spacing in markup also causes all adjacent markup spaces to be - /// removed, regardless of the amount of spacing inserted. To force a space - /// next to weak spacing, you can explicitly write `[#" "]` (for a normal - /// space) or `[~]` (for a non-breaking space). The latter can be useful to - /// create a construct that always attaches to the preceding word with one - /// non-breaking space, independently of whether a markup space existed in - /// front or not. - /// - /// ```example - /// #h(1cm, weak: true) - /// We identified a group of _weak_ - /// specimens that fail to manifest - /// in most cases. However, when - /// #h(8pt, weak: true) supported - /// #h(8pt, weak: true) on both sides, - /// they do show up. - /// - /// Further #h(0pt, weak: true) more, - /// even the smallest of them swallow - /// adjacent markup spaces. - /// ``` - #[default(false)] - pub weak: bool, -} - -impl HElem { - /// Zero-width horizontal weak spacing that eats surrounding spaces. - pub fn hole() -> Self { - Self::new(Abs::zero().into()).with_weak(true) - } -} - -impl Behave for HElem { - fn behaviour(&self) -> Behaviour { - if self.amount().is_fractional() { - Behaviour::Destructive - } else if self.weak(StyleChain::default()) { - Behaviour::Weak(1) - } else { - Behaviour::Invisible - } - } - - fn larger( - &self, - prev: &(Cow<Content>, Behaviour, StyleChain), - styles: StyleChain, - ) -> bool { - let Some(other) = prev.0.to::<Self>() else { return false }; - match (self.amount(), other.amount()) { - (Spacing::Fr(this), Spacing::Fr(other)) => this > other, - (Spacing::Rel(this), Spacing::Rel(other)) => { - this.resolve(styles) > other.resolve(prev.2) - } - _ => false, - } - } -} - -/// Inserts vertical spacing into a flow of blocks. -/// -/// The spacing can be absolute, relative, or fractional. In the last case, -/// the remaining space on the page is distributed among all fractional spacings -/// according to their relative fractions. -/// -/// # Example -/// ```example -/// #grid( -/// rows: 3cm, -/// columns: 6, -/// gutter: 1fr, -/// [A #parbreak() B], -/// [A #v(0pt) B], -/// [A #v(10pt) B], -/// [A #v(0pt, weak: true) B], -/// [A #v(40%, weak: true) B], -/// [A #v(1fr) B], -/// ) -/// ``` -#[elem(title = "Spacing (V)", Behave)] -pub struct VElem { - /// How much spacing to insert. - #[required] - pub amount: Spacing, - - /// If `{true}`, the spacing collapses at the start or end of a flow. - /// Moreover, from multiple adjacent weak spacings all but the largest one - /// collapse. Weak spacings will always collapse adjacent paragraph spacing, - /// even if the paragraph spacing is larger. - /// - /// ```example - /// The following theorem is - /// foundational to the field: - /// #v(4pt, weak: true) - /// $ x^2 + y^2 = r^2 $ - /// #v(4pt, weak: true) - /// The proof is simple: - /// ``` - #[external] - pub weak: bool, - - /// The element's weakness level, see also [`Behaviour`]. - #[internal] - #[parse(args.named("weak")?.map(|v: bool| v as usize))] - pub weakness: usize, -} - -impl VElem { - /// Normal strong spacing. - pub fn strong(amount: Spacing) -> Self { - Self::new(amount).with_weakness(0) - } - - /// User-created weak spacing. - pub fn weak(amount: Spacing) -> Self { - Self::new(amount).with_weakness(1) - } - - /// Weak spacing with list attach weakness. - pub fn list_attach(amount: Spacing) -> Self { - Self::new(amount).with_weakness(2) - } - - /// Weak spacing with BlockElem::ABOVE/BELOW weakness. - pub fn block_around(amount: Spacing) -> Self { - Self::new(amount).with_weakness(3) - } - - /// Weak spacing with BlockElem::SPACING weakness. - pub fn block_spacing(amount: Spacing) -> Self { - Self::new(amount).with_weakness(4) - } -} - -impl Behave for VElem { - fn behaviour(&self) -> Behaviour { - if self.amount().is_fractional() { - Behaviour::Destructive - } else if self.weakness(StyleChain::default()) > 0 { - Behaviour::Weak(self.weakness(StyleChain::default())) - } else { - Behaviour::Invisible - } - } - - fn larger( - &self, - prev: &(Cow<Content>, Behaviour, StyleChain), - styles: StyleChain, - ) -> bool { - let Some(other) = prev.0.to::<Self>() else { return false }; - match (self.amount(), other.amount()) { - (Spacing::Fr(this), Spacing::Fr(other)) => this > other, - (Spacing::Rel(this), Spacing::Rel(other)) => { - this.resolve(styles) > other.resolve(prev.2) - } - _ => false, - } - } -} - -cast! { - VElem, - v: Content => v.to::<Self>().cloned().ok_or("expected `v` element")?, -} - -/// Kinds of spacing. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum Spacing { - /// Spacing specified in absolute terms and relative to the parent's size. - Rel(Rel<Length>), - /// Spacing specified as a fraction of the remaining free space in the - /// parent. - Fr(Fr), -} - -impl Spacing { - /// Whether this is fractional spacing. - pub fn is_fractional(self) -> bool { - matches!(self, Self::Fr(_)) - } - - /// Whether the spacing is actually no spacing. - pub fn is_zero(&self) -> bool { - match self { - Self::Rel(rel) => rel.is_zero(), - Self::Fr(fr) => fr.is_zero(), - } - } -} - -impl From<Abs> for Spacing { - fn from(abs: Abs) -> Self { - Self::Rel(abs.into()) - } -} - -impl From<Em> for Spacing { - fn from(em: Em) -> Self { - Self::Rel(Rel::new(Ratio::zero(), em.into())) - } -} - -impl From<Length> for Spacing { - fn from(length: Length) -> Self { - Self::Rel(length.into()) - } -} - -impl From<Fr> for Spacing { - fn from(fr: Fr) -> Self { - Self::Fr(fr) - } -} - -cast! { - Spacing, - self => match self { - Self::Rel(rel) => { - if rel.rel.is_zero() { - rel.abs.into_value() - } else if rel.abs.is_zero() { - rel.rel.into_value() - } else { - rel.into_value() - } - } - Self::Fr(fr) => fr.into_value(), - }, - v: Rel<Length> => Self::Rel(v), - v: Fr => Self::Fr(v), -} diff --git a/crates/typst-library/src/layout/stack.rs b/crates/typst-library/src/layout/stack.rs deleted file mode 100644 index c12d2048..00000000 --- a/crates/typst-library/src/layout/stack.rs +++ /dev/null @@ -1,345 +0,0 @@ -use crate::layout::{AlignElem, Spacing}; -use crate::prelude::*; - -/// Arranges content and spacing horizontally or vertically. -/// -/// The stack places a list of items along an axis, with optional spacing -/// between each item. -/// -/// # Example -/// ```example -/// #stack( -/// dir: ttb, -/// rect(width: 40pt), -/// rect(width: 120pt), -/// rect(width: 90pt), -/// ) -/// ``` -#[elem(Layout)] -pub struct StackElem { - /// The direction along which the items are stacked. Possible values are: - /// - /// - `{ltr}`: Left to right. - /// - `{rtl}`: Right to left. - /// - `{ttb}`: Top to bottom. - /// - `{btt}`: Bottom to top. - /// - /// You can use the `start` and `end` methods to obtain the initial and - /// final points (respectively) of a direction, as `alignment`. You can also - /// use the `axis` method to determine whether a direction is - /// `{"horizontal"}` or `{"vertical"}`. The `inv` method returns a - /// direction's inverse direction. - /// - /// For example, `{ttb.start()}` is `top`, `{ttb.end()}` is `bottom`, - /// `{ttb.axis()}` is `{"vertical"}` and `{ttb.inv()}` is equal to `btt`. - #[default(Dir::TTB)] - pub dir: Dir, - - /// Spacing to insert between items where no explicit spacing was provided. - pub spacing: Option<Spacing>, - - /// The children to stack along the axis. - #[variadic] - pub children: Vec<StackChild>, -} - -impl Layout for StackElem { - #[tracing::instrument(name = "StackElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - let mut layouter = StackLayouter::new(self.dir(styles), regions, styles); - - // 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) => { - if let Some(kind) = deferred { - layouter.layout_spacing(kind); - } - - layouter.layout_block(vt, block, styles)?; - deferred = spacing; - } - } - } - - Ok(layouter.finish()) - } -} - -/// A child of a stack element. -#[derive(Clone, PartialEq, Hash)] -pub enum StackChild { - /// Spacing between other children. - Spacing(Spacing), - /// Arbitrary block-level content. - Block(Content), -} - -impl Debug for StackChild { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - Self::Spacing(kind) => kind.fmt(f), - Self::Block(block) => block.fmt(f), - } - } -} - -cast! { - StackChild, - self => match self { - Self::Spacing(spacing) => spacing.into_value(), - Self::Block(content) => content.into_value(), - }, - v: Spacing => Self::Spacing(v), - v: Content => Self::Block(v), -} - -/// Performs stack layout. -struct StackLayouter<'a> { - /// The stacking direction. - dir: Dir, - /// The axis of the stacking direction. - axis: Axis, - /// The regions to layout children into. - regions: Regions<'a>, - /// The inherited styles. - styles: StyleChain<'a>, - /// Whether the stack itself should expand to fill the region. - expand: Axes<bool>, - /// The initial size of the current region before we started subtracting. - initial: Size, - /// The generic size used by the frames for the current region. - used: Gen<Abs>, - /// The sum of fractions in the current region. - fr: Fr, - /// Already layouted items whose exact positions are not yet known due to - /// fractional spacing. - items: Vec<StackItem>, - /// Finished frames for previous regions. - finished: Vec<Frame>, -} - -/// A prepared item in a stack layout. -enum StackItem { - /// Absolute spacing between other items. - Absolute(Abs), - /// Fractional spacing between other items. - Fractional(Fr), - /// A frame for a layouted block. - Frame(Frame, Axes<FixedAlign>), -} - -impl<'a> StackLayouter<'a> { - /// Create a new stack layouter. - fn new(dir: Dir, mut regions: Regions<'a>, styles: StyleChain<'a>) -> Self { - let axis = dir.axis(); - let expand = regions.expand; - - // Disable expansion along the block axis for children. - regions.expand.set(axis, false); - - Self { - dir, - axis, - regions, - styles, - expand, - initial: regions.size, - used: Gen::zero(), - fr: Fr::zero(), - items: vec![], - finished: vec![], - } - } - - /// Add spacing along the spacing direction. - #[tracing::instrument(name = "StackLayouter::layout_spacing", skip_all)] - fn layout_spacing(&mut self, spacing: Spacing) { - match spacing { - Spacing::Rel(v) => { - // Resolve the spacing and limit it to the remaining space. - let resolved = v - .resolve(self.styles) - .relative_to(self.regions.base().get(self.axis)); - let remaining = self.regions.size.get_mut(self.axis); - let limited = resolved.min(*remaining); - if self.dir.axis() == Axis::Y { - *remaining -= limited; - } - self.used.main += limited; - self.items.push(StackItem::Absolute(resolved)); - } - Spacing::Fr(v) => { - self.fr += v; - self.items.push(StackItem::Fractional(v)); - } - } - } - - /// Layout an arbitrary block. - #[tracing::instrument(name = "StackLayouter::layout_block", skip_all)] - fn layout_block( - &mut self, - vt: &mut Vt, - block: &Content, - styles: StyleChain, - ) -> SourceResult<()> { - if self.regions.is_full() { - self.finish_region(); - } - - // Block-axis alignment of the `AlignElement` is respected by stacks. - let align = if let Some(align) = block.to::<AlignElem>() { - align.alignment(styles) - } else if let Some((_, local)) = block.to_styled() { - AlignElem::alignment_in(styles.chain(local)) - } else { - AlignElem::alignment_in(styles) - } - .resolve(styles); - - let fragment = block.layout(vt, styles, self.regions)?; - let len = fragment.len(); - for (i, frame) in fragment.into_iter().enumerate() { - // Grow our size, shrink the region and save the frame for later. - let size = frame.size(); - if self.dir.axis() == Axis::Y { - self.regions.size.y -= size.y; - } - - let gen = match self.axis { - Axis::X => Gen::new(size.y, size.x), - Axis::Y => Gen::new(size.x, size.y), - }; - - self.used.main += gen.main; - self.used.cross.set_max(gen.cross); - - self.items.push(StackItem::Frame(frame, align)); - - if i + 1 < len { - self.finish_region(); - } - } - - Ok(()) - } - - /// Advance to the next region. - fn finish_region(&mut self) { - // Determine the size of the stack in this region depending on whether - // the region expands. - let mut size = self - .expand - .select(self.initial, self.used.into_axes(self.axis)) - .min(self.initial); - - // Expand fully if there are fr spacings. - let full = self.initial.get(self.axis); - let remaining = full - self.used.main; - if self.fr.get() > 0.0 && full.is_finite() { - self.used.main = full; - size.set(self.axis, full); - } - - let mut output = Frame::hard(size); - let mut cursor = Abs::zero(); - let mut ruler: FixedAlign = self.dir.start().into(); - - // Place all frames. - for item in self.items.drain(..) { - match item { - StackItem::Absolute(v) => cursor += v, - StackItem::Fractional(v) => cursor += v.share(self.fr, remaining), - StackItem::Frame(frame, align) => { - if self.dir.is_positive() { - ruler = ruler.max(align.get(self.axis)); - } else { - ruler = ruler.min(align.get(self.axis)); - } - - // Align along the main axis. - let parent = size.get(self.axis); - let child = frame.size().get(self.axis); - let main = ruler.position(parent - self.used.main) - + if self.dir.is_positive() { - cursor - } else { - self.used.main - child - cursor - }; - - // Align along the cross axis. - let other = self.axis.other(); - let cross = align - .get(other) - .position(size.get(other) - frame.size().get(other)); - - let pos = Gen::new(cross, main).to_point(self.axis); - cursor += child; - output.push_frame(pos, frame); - } - } - } - - // Advance to the next region. - self.regions.next(); - self.initial = self.regions.size; - self.used = Gen::zero(); - self.fr = Fr::zero(); - self.finished.push(output); - } - - /// Finish layouting and return the resulting frames. - fn finish(mut self) -> Fragment { - self.finish_region(); - Fragment::frames(self.finished) - } -} - -/// A container with a main and cross component. -#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] -struct Gen<T> { - /// The main component. - pub cross: T, - /// The cross component. - pub main: T, -} - -impl<T> Gen<T> { - /// Create a new instance from the two components. - const fn new(cross: T, main: T) -> Self { - Self { cross, main } - } - - /// Convert to the specific representation, given the current main axis. - fn into_axes(self, main: Axis) -> Axes<T> { - match main { - Axis::X => Axes::new(self.main, self.cross), - Axis::Y => Axes::new(self.cross, self.main), - } - } -} - -impl Gen<Abs> { - /// The zero value. - fn zero() -> Self { - Self { cross: Abs::zero(), main: Abs::zero() } - } - - /// Convert to a point. - fn to_point(self, main: Axis) -> Point { - self.into_axes(main).to_point() - } -} diff --git a/crates/typst-library/src/layout/table.rs b/crates/typst-library/src/layout/table.rs deleted file mode 100644 index 0eff7a44..00000000 --- a/crates/typst-library/src/layout/table.rs +++ /dev/null @@ -1,361 +0,0 @@ -use typst::eval::{CastInfo, Reflect}; - -use crate::layout::{AlignElem, GridLayouter, TrackSizings}; -use crate::meta::Figurable; -use crate::prelude::*; - -/// A table of items. -/// -/// Tables are used to arrange content in cells. Cells can contain arbitrary -/// content, including multiple paragraphs and are specified in row-major order. -/// Because tables are just grids with configurable cell properties, refer to -/// the [grid documentation]($grid) for more information on how to size the -/// table tracks. -/// -/// To give a table a caption and make it [referenceable]($ref), put it into a -/// [figure]($figure). -/// -/// # Example -/// ```example -/// #table( -/// columns: (1fr, auto, auto), -/// inset: 10pt, -/// align: horizon, -/// [], [*Area*], [*Parameters*], -/// image("cylinder.svg"), -/// $ pi h (D^2 - d^2) / 4 $, -/// [ -/// $h$: height \ -/// $D$: outer radius \ -/// $d$: inner radius -/// ], -/// image("tetrahedron.svg"), -/// $ sqrt(2) / 12 a^3 $, -/// [$a$: edge length] -/// ) -/// ``` -#[elem(Layout, LocalName, Figurable)] -pub struct TableElem { - /// The column sizes. See the [grid documentation]($grid) for more - /// information on track sizing. - #[borrowed] - pub columns: TrackSizings, - - /// The row sizes. See the [grid documentation]($grid) for more information - /// on track sizing. - #[borrowed] - pub rows: TrackSizings, - - /// The gaps between rows & columns. See the [grid documentation]($grid) for - /// more information on gutters. - #[external] - pub gutter: TrackSizings, - - /// The gaps between columns. Takes precedence over `gutter`. See the - /// [grid documentation]($grid) for more information on gutters. - #[borrowed] - #[parse( - let gutter = args.named("gutter")?; - args.named("column-gutter")?.or_else(|| gutter.clone()) - )] - pub column_gutter: TrackSizings, - - /// The gaps between rows. Takes precedence over `gutter`. See the - /// [grid documentation]($grid) for more information on gutters. - #[parse(args.named("row-gutter")?.or_else(|| gutter.clone()))] - #[borrowed] - pub row_gutter: TrackSizings, - - /// How to fill the cells. - /// - /// This can be a color or a function that returns a color. The function is - /// passed the cells' column and row index, starting at zero. This can be - /// used to implement striped tables. - /// - /// ```example - /// #table( - /// fill: (col, _) => if calc.odd(col) { luma(240) } else { white }, - /// align: (col, row) => - /// if row == 0 { center } - /// else if col == 0 { left } - /// else { right }, - /// columns: 4, - /// [], [*Q1*], [*Q2*], [*Q3*], - /// [Revenue:], [1000 €], [2000 €], [3000 €], - /// [Expenses:], [500 €], [1000 €], [1500 €], - /// [Profit:], [500 €], [1000 €], [1500 €], - /// ) - /// ``` - #[borrowed] - pub fill: Celled<Option<Paint>>, - - /// How to align the cells' content. - /// - /// This can either be a single alignment, an array of alignments - /// (corresponding to each column) or a function that returns an alignment. - /// The function is passed the cells' column and row index, starting at zero. - /// If set to `{auto}`, the outer alignment is used. - /// - /// ```example - /// #table( - /// columns: 3, - /// align: (x, y) => (left, center, right).at(x), - /// [Hello], [Hello], [Hello], - /// [A], [B], [C], - /// ) - /// ``` - #[borrowed] - pub align: Celled<Smart<Align>>, - - /// How to [stroke]($stroke) the cells. - /// - /// Strokes can be disabled by setting this to `{none}`. - /// - /// _Note:_ Richer stroke customization for individual cells is not yet - /// implemented, but will be in the future. In the meantime, you can use the - /// third-party [tablex library](https://github.com/PgBiel/typst-tablex/). - #[resolve] - #[fold] - #[default(Some(Stroke::default()))] - pub stroke: Option<Stroke>, - - /// How much to pad the cells' content. - /// - /// ```example - /// #table( - /// inset: 10pt, - /// [Hello], - /// [World], - /// ) - /// - /// #table( - /// columns: 2, - /// inset: ( - /// x: 20pt, - /// y: 10pt, - /// ), - /// [Hello], - /// [World], - /// ) - /// ``` - #[fold] - #[default(Sides::splat(Abs::pt(5.0).into()))] - pub inset: Sides<Option<Rel<Length>>>, - - /// The contents of the table cells. - #[variadic] - pub children: Vec<Content>, -} - -impl Layout for TableElem { - #[tracing::instrument(name = "TableElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - 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 tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice()); - let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice()); - let cols = tracks.x.len().max(1); - let cells: Vec<_> = self - .children() - .iter() - .enumerate() - .map(|(i, child)| { - let mut child = child.clone().padded(inset); - - let x = i % cols; - let y = i / cols; - if let Smart::Custom(alignment) = align.resolve(vt, x, y)? { - child = child.styled(AlignElem::set_alignment(alignment)); - } - - Ok(child) - }) - .collect::<SourceResult<_>>()?; - - let fill = self.fill(styles); - let stroke = self.stroke(styles).map(Stroke::unwrap_or_default); - - // Prepare grid layout by unifying content and gutter tracks. - let layouter = - GridLayouter::new(tracks, gutter, &cells, regions, styles, self.span()); - - // Measure the columns and layout the grid row-by-row. - let mut layout = layouter.layout(vt)?; - - // Add lines and backgrounds. - for (frame, rows) in layout.fragment.iter_mut().zip(&layout.rows) { - if layout.cols.is_empty() || rows.is_empty() { - continue; - } - - // Render table lines. - if let Some(stroke) = &stroke { - let thickness = stroke.thickness; - let half = thickness / 2.0; - - // Render horizontal lines. - for offset in points(rows.iter().map(|piece| piece.height)) { - let target = Point::with_x(frame.width() + thickness); - let hline = Geometry::Line(target).stroked(stroke.clone()); - frame.prepend( - Point::new(-half, offset), - FrameItem::Shape(hline, self.span()), - ); - } - - // Render vertical lines. - for offset in points(layout.cols.iter().copied()) { - let target = Point::with_y(frame.height() + thickness); - let vline = Geometry::Line(target).stroked(stroke.clone()); - frame.prepend( - Point::new(offset, -half), - FrameItem::Shape(vline, self.span()), - ); - } - } - - // Render cell backgrounds. - let mut dx = Abs::zero(); - for (x, &col) in layout.cols.iter().enumerate() { - let mut dy = Abs::zero(); - for row in rows { - if let Some(fill) = fill.resolve(vt, x, row.y)? { - let pos = Point::new(dx, dy); - let size = Size::new(col, row.height); - let rect = Geometry::Rect(size).filled(fill); - frame.prepend(pos, FrameItem::Shape(rect, self.span())); - } - dy += row.height; - } - dx += col; - } - } - - Ok(layout.fragment) - } -} - -/// Turn an iterator of extents into an iterator of offsets before, in between, -/// and after the extents, e.g. [10mm, 5mm] -> [0mm, 10mm, 15mm]. -fn points(extents: impl IntoIterator<Item = Abs>) -> impl Iterator<Item = Abs> { - let mut offset = Abs::zero(); - std::iter::once(Abs::zero()).chain(extents).map(move |extent| { - offset += extent; - offset - }) -} - -/// A value that can be configured per cell. -#[derive(Debug, Clone, PartialEq, Hash)] -pub enum Celled<T> { - /// A bare value, the same for all cells. - Value(T), - /// A closure mapping from cell coordinates to a value. - Func(Func), - /// An array of alignment values corresponding to each column. - Array(Vec<T>), -} - -impl<T: Default + Clone + FromValue> Celled<T> { - /// Resolve the value based on the cell position. - pub fn resolve(&self, vt: &mut Vt, x: usize, y: usize) -> SourceResult<T> { - Ok(match self { - Self::Value(value) => value.clone(), - Self::Func(func) => func.call_vt(vt, [x, y])?.cast().at(func.span())?, - Self::Array(array) => x - .checked_rem(array.len()) - .and_then(|i| array.get(i)) - .cloned() - .unwrap_or_default(), - }) - } -} - -impl<T: Default> Default for Celled<T> { - fn default() -> Self { - Self::Value(T::default()) - } -} - -impl<T: Reflect> Reflect for Celled<T> { - fn input() -> CastInfo { - T::input() + Array::input() + Func::input() - } - - fn output() -> CastInfo { - T::output() + Array::output() + Func::output() - } - - fn castable(value: &Value) -> bool { - Array::castable(value) || Func::castable(value) || T::castable(value) - } -} - -impl<T: IntoValue> IntoValue for Celled<T> { - fn into_value(self) -> Value { - match self { - Self::Value(value) => value.into_value(), - Self::Func(func) => func.into_value(), - Self::Array(arr) => arr.into_value(), - } - } -} - -impl<T: FromValue> FromValue for Celled<T> { - fn from_value(value: Value) -> StrResult<Self> { - match value { - Value::Func(v) => Ok(Self::Func(v)), - Value::Array(array) => Ok(Self::Array( - array.into_iter().map(T::from_value).collect::<StrResult<_>>()?, - )), - v if T::castable(&v) => Ok(Self::Value(T::from_value(v)?)), - v => Err(Self::error(&v)), - } - } -} - -impl LocalName for TableElem { - fn local_name(lang: Lang, _: Option<Region>) -> &'static str { - match lang { - Lang::ALBANIAN => "Tabel", - Lang::ARABIC => "جدول", - Lang::BOKMÅL => "Tabell", - Lang::CHINESE => "表", - Lang::CZECH => "Tabulka", - Lang::DANISH => "Tabel", - Lang::DUTCH => "Tabel", - Lang::FILIPINO => "Talaan", - Lang::FINNISH => "Taulukko", - Lang::FRENCH => "Tableau", - Lang::GERMAN => "Tabelle", - Lang::GREEK => "Πίνακας", - Lang::HUNGARIAN => "Táblázat", - Lang::ITALIAN => "Tabella", - Lang::NYNORSK => "Tabell", - Lang::POLISH => "Tabela", - Lang::PORTUGUESE => "Tabela", - Lang::ROMANIAN => "Tabelul", - Lang::RUSSIAN => "Таблица", - Lang::SLOVENIAN => "Tabela", - Lang::SPANISH => "Tabla", - Lang::SWEDISH => "Tabell", - Lang::TURKISH => "Tablo", - Lang::UKRAINIAN => "Таблиця", - Lang::VIETNAMESE => "Bảng", - Lang::JAPANESE => "表", - Lang::ENGLISH | _ => "Table", - } - } -} - -impl Figurable for TableElem {} diff --git a/crates/typst-library/src/layout/terms.rs b/crates/typst-library/src/layout/terms.rs deleted file mode 100644 index d373768d..00000000 --- a/crates/typst-library/src/layout/terms.rs +++ /dev/null @@ -1,162 +0,0 @@ -use crate::layout::{BlockElem, HElem, ParElem, Spacing, VElem}; -use crate::prelude::*; - -/// A list of terms and their descriptions. -/// -/// Displays a sequence of terms and their descriptions vertically. When the -/// descriptions span over multiple lines, they use hanging indent to -/// communicate the visual hierarchy. -/// -/// # Example -/// ```example -/// / Ligature: A merged glyph. -/// / Kerning: A spacing adjustment -/// between two adjacent letters. -/// ``` -/// -/// # 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", Layout)] -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 - /// [leading]($par.leading) instead. This makes the term list more compact, - /// which can look better if the items are short. - /// - /// In markup mode, the value of this parameter is determined based on - /// whether items are separated with a blank line. If items directly follow - /// each other, this is set to `{true}`; if items are separated by a blank - /// line, this is set to `{false}`. - /// - /// ```example - /// / Fact: If a term list has a lot - /// of text, and maybe other inline - /// content, it should not be tight - /// anymore. - /// - /// / Tip: To make it wide, simply - /// insert a blank line between the - /// items. - /// ``` - #[default(true)] - pub tight: bool, - - /// The separator between the item and the description. - /// - /// If you want to just separate them with a certain amount of space, use - /// `{h(2cm, weak: true)}` as the separator and replace `{2cm}` with your - /// desired amount of space. - /// - /// ```example - /// #set terms(separator: [: ]) - /// - /// / Colon: A nice separator symbol. - /// ``` - #[default(HElem::new(Em::new(0.6).into()).with_weak(true).pack())] - #[borrowed] - pub separator: Content, - - /// The indentation of each item. - pub indent: Length, - - /// The hanging indent of the description. - /// - /// This is in addition to the whole item's `indent`. - /// - /// ```example - /// #set terms(hanging-indent: 0pt) - /// / Term: This term list does not - /// make use of hanging indents. - /// ``` - #[default(Em::new(2.0).into())] - pub hanging_indent: Length, - - /// The spacing between the items of a wide (non-tight) term list. - /// - /// If set to `{auto}`, uses the spacing [below blocks]($block.below). - pub spacing: Smart<Spacing>, - - /// The term list's children. - /// - /// When using the term list syntax, adjacent items are automatically - /// collected into term lists, even through constructs like for loops. - /// - /// ```example - /// #for (year, product) in ( - /// "1978": "TeX", - /// "1984": "LaTeX", - /// "2019": "Typst", - /// ) [/ #product: Born in #year.] - /// ``` - #[variadic] - pub children: Vec<TermItem>, -} - -#[scope] -impl TermsElem { - #[elem] - type TermItem; -} - -impl Layout for TermsElem { - #[tracing::instrument(name = "TermsElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - let separator = self.separator(styles); - let indent = self.indent(styles); - let hanging_indent = self.hanging_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()) - }; - - let mut seq = vec![]; - for (i, child) in self.children().iter().enumerate() { - if i > 0 { - seq.push(VElem::new(gutter).with_weakness(1).pack()); - } - if !indent.is_zero() { - seq.push(HElem::new(indent.into()).pack()); - } - seq.push(child.term().clone().strong()); - seq.push((*separator).clone()); - seq.push(child.description().clone()); - } - - Content::sequence(seq) - .styled(ParElem::set_hanging_indent(hanging_indent + indent)) - .layout(vt, styles, regions) - } -} - -/// A term list item. -#[elem(name = "item", title = "Term List Item")] -pub struct TermItem { - /// The term described by the list item. - #[required] - pub term: Content, - - /// The description of the term. - #[required] - pub description: Content, -} - -cast! { - TermItem, - array: Array => { - let mut iter = array.into_iter(); - let (term, description) = match (iter.next(), iter.next(), iter.next()) { - (Some(a), Some(b), None) => (a.cast()?, b.cast()?), - _ => bail!("array must contain exactly two entries"), - }; - Self::new(term, description) - }, - v: Content => v.to::<Self>().cloned().ok_or("expected term item or array")?, -} diff --git a/crates/typst-library/src/layout/transform.rs b/crates/typst-library/src/layout/transform.rs deleted file mode 100644 index 012a146d..00000000 --- a/crates/typst-library/src/layout/transform.rs +++ /dev/null @@ -1,187 +0,0 @@ -use typst::geom::Transform; - -use crate::prelude::*; - -/// Moves content without affecting layout. -/// -/// The `move` function allows you to move content while the layout still 'sees' -/// it at the original positions. Containers will still be sized as if the -/// content was not moved. -/// -/// # Example -/// ```example -/// #rect(inset: 0pt, move( -/// dx: 6pt, dy: 6pt, -/// rect( -/// inset: 8pt, -/// fill: white, -/// stroke: black, -/// [Abra cadabra] -/// ) -/// )) -/// ``` -#[elem(Layout)] -pub struct MoveElem { - /// The horizontal displacement of the content. - pub dx: Rel<Length>, - - /// The vertical displacement of the content. - pub dy: Rel<Length>, - - /// The content to move. - #[required] - pub body: Content, -} - -impl Layout for MoveElem { - #[tracing::instrument(name = "MoveElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - let pod = Regions::one(regions.base(), Axes::splat(false)); - let mut frame = self.body().layout(vt, 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(Fragment::frame(frame)) - } -} - -/// Rotates content without affecting layout. -/// -/// Rotates an element by a given angle. The layout will act as if the element -/// was not rotated. -/// -/// # Example -/// ```example -/// #stack( -/// dir: ltr, -/// spacing: 1fr, -/// ..range(16) -/// .map(i => rotate(24deg * i)[X]), -/// ) -/// ``` -#[elem(Layout)] -pub struct RotateElem { - /// The amount of rotation. - /// - /// ```example - /// #rotate(-1.571rad)[Space!] - /// ``` - /// - #[positional] - pub angle: Angle, - - /// The origin of the rotation. - /// - /// If, for instance, you wanted the bottom left corner of the rotated - /// element to stay aligned with the baseline, you would set it to `bottom + - /// left` instead. - /// - /// ```example - /// #set text(spacing: 8pt) - /// #let square = square.with(width: 8pt) - /// - /// #box(square()) - /// #box(rotate(30deg, origin: center, square())) - /// #box(rotate(30deg, origin: top + left, square())) - /// #box(rotate(30deg, origin: bottom + right, square())) - /// ``` - #[fold] - #[default(HAlign::Center + VAlign::Horizon)] - pub origin: Align, - - /// The content to rotate. - #[required] - pub body: Content, -} - -impl Layout for RotateElem { - #[tracing::instrument(name = "RotateElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - let pod = Regions::one(regions.base(), Axes::splat(false)); - let mut frame = self.body().layout(vt, styles, pod)?.into_frame(); - let Axes { x, y } = self - .origin(styles) - .resolve(styles) - .zip_map(frame.size(), FixedAlign::position); - let ts = Transform::translate(x, y) - .pre_concat(Transform::rotate(self.angle(styles))) - .pre_concat(Transform::translate(-x, -y)); - frame.transform(ts); - Ok(Fragment::frame(frame)) - } -} - -/// Scales content without affecting layout. -/// -/// Lets you mirror content by specifying a negative scale on a single axis. -/// -/// # Example -/// ```example -/// #set align(center) -/// #scale(x: -100%)[This is mirrored.] -/// ``` -#[elem(Layout)] -pub struct ScaleElem { - /// The horizontal scaling factor. - /// - /// The body will be mirrored horizontally if the parameter is negative. - #[parse( - let all = args.find()?; - args.named("x")?.or(all) - )] - #[default(Ratio::one())] - pub x: Ratio, - - /// The vertical scaling factor. - /// - /// The body will be mirrored vertically if the parameter is negative. - #[parse(args.named("y")?.or(all))] - #[default(Ratio::one())] - pub y: Ratio, - - /// The origin of the transformation. - /// - /// ```example - /// A#box(scale(75%)[A])A \ - /// B#box(scale(75%, origin: bottom + left)[B])B - /// ``` - #[fold] - #[default(HAlign::Center + VAlign::Horizon)] - pub origin: Align, - - /// The content to scale. - #[required] - pub body: Content, -} - -impl Layout for ScaleElem { - #[tracing::instrument(name = "ScaleElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - let pod = Regions::one(regions.base(), Axes::splat(false)); - let mut frame = self.body().layout(vt, styles, pod)?.into_frame(); - let Axes { x, y } = self - .origin(styles) - .resolve(styles) - .zip_map(frame.size(), FixedAlign::position); - let transform = Transform::translate(x, y) - .pre_concat(Transform::scale(self.x(styles), self.y(styles))) - .pre_concat(Transform::translate(-x, -y)); - frame.transform(transform); - Ok(Fragment::frame(frame)) - } -} |
