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