summaryrefslogtreecommitdiff
path: root/crates/typst-library/src/layout/frame.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/typst-library/src/layout/frame.rs')
-rw-r--r--crates/typst-library/src/layout/frame.rs606
1 files changed, 606 insertions, 0 deletions
diff --git a/crates/typst-library/src/layout/frame.rs b/crates/typst-library/src/layout/frame.rs
new file mode 100644
index 00000000..204584fc
--- /dev/null
+++ b/crates/typst-library/src/layout/frame.rs
@@ -0,0 +1,606 @@
+//! Finished documents.
+
+use std::fmt::{self, Debug, Formatter};
+use std::num::NonZeroUsize;
+use std::sync::Arc;
+
+use smallvec::SmallVec;
+use typst_syntax::Span;
+use typst_utils::{LazyHash, Numeric};
+
+use crate::foundations::{cast, dict, Dict, Label, StyleChain, Value};
+use crate::introspection::{Location, Tag};
+use crate::layout::{
+ Abs, Axes, FixedAlignment, HideElem, Length, Point, Size, Transform,
+};
+use crate::model::{Destination, LinkElem};
+use crate::text::TextItem;
+use crate::visualize::{Color, FixedStroke, Geometry, Image, Paint, Path, Shape};
+
+/// A finished layout with items at fixed positions.
+#[derive(Default, Clone, Hash)]
+pub struct Frame {
+ /// The size of the frame.
+ size: Size,
+ /// The baseline of the frame measured from the top. If this is `None`, the
+ /// frame's implicit baseline is at the bottom.
+ baseline: Option<Abs>,
+ /// The items composing this layout.
+ items: Arc<LazyHash<Vec<(Point, FrameItem)>>>,
+ /// The hardness of this frame.
+ ///
+ /// Determines whether it is a boundary for gradient drawing.
+ kind: FrameKind,
+}
+
+/// Constructor, accessors and setters.
+impl Frame {
+ /// Create a new, empty frame.
+ ///
+ /// Panics the size is not finite.
+ #[track_caller]
+ pub fn new(size: Size, kind: FrameKind) -> Self {
+ assert!(size.is_finite());
+ Self {
+ size,
+ baseline: None,
+ items: Arc::new(LazyHash::new(vec![])),
+ kind,
+ }
+ }
+
+ /// Create a new, empty soft frame.
+ ///
+ /// Panics if the size is not finite.
+ #[track_caller]
+ pub fn soft(size: Size) -> Self {
+ Self::new(size, FrameKind::Soft)
+ }
+
+ /// Create a new, empty hard frame.
+ ///
+ /// Panics if the size is not finite.
+ #[track_caller]
+ pub fn hard(size: Size) -> Self {
+ Self::new(size, FrameKind::Hard)
+ }
+
+ /// Sets the frame's hardness.
+ pub fn set_kind(&mut self, kind: FrameKind) {
+ 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
+ }
+
+ /// Whether the frame contains no items.
+ pub fn is_empty(&self) -> bool {
+ self.items.is_empty()
+ }
+
+ /// The size of the frame.
+ pub fn size(&self) -> Size {
+ self.size
+ }
+
+ /// The size of the frame, mutably.
+ pub fn size_mut(&mut self) -> &mut Size {
+ &mut self.size
+ }
+
+ /// Set the size of the frame.
+ pub fn set_size(&mut self, size: Size) {
+ self.size = size;
+ }
+
+ /// The width of the frame.
+ pub fn width(&self) -> Abs {
+ self.size.x
+ }
+
+ /// The height of the frame.
+ pub fn height(&self) -> Abs {
+ self.size.y
+ }
+
+ /// The vertical position of the frame's baseline.
+ pub fn baseline(&self) -> Abs {
+ self.baseline.unwrap_or(self.size.y)
+ }
+
+ /// Whether the frame has a non-default baseline.
+ pub fn has_baseline(&self) -> bool {
+ self.baseline.is_some()
+ }
+
+ /// Set the frame's baseline from the top.
+ pub fn set_baseline(&mut self, baseline: Abs) {
+ self.baseline = Some(baseline);
+ }
+
+ /// The distance from the baseline to the top of the frame.
+ ///
+ /// This is the same as `baseline()`, but more in line with the terminology
+ /// used in math layout.
+ pub fn ascent(&self) -> Abs {
+ self.baseline()
+ }
+
+ /// The distance from the baseline to the bottom of the frame.
+ pub fn descent(&self) -> Abs {
+ self.size.y - self.baseline()
+ }
+
+ /// An iterator over the items inside this frame alongside their positions
+ /// relative to the top-left of the frame.
+ pub fn items(&self) -> std::slice::Iter<'_, (Point, FrameItem)> {
+ self.items.iter()
+ }
+}
+
+/// Insert items and subframes.
+impl Frame {
+ /// The layer the next item will be added on. This corresponds to the number
+ /// of items in the frame.
+ pub fn layer(&self) -> usize {
+ self.items.len()
+ }
+
+ /// Add an item at a position in the foreground.
+ pub fn push(&mut self, pos: Point, item: FrameItem) {
+ Arc::make_mut(&mut self.items).push((pos, item));
+ }
+
+ /// Add multiple items at a position in the foreground.
+ ///
+ /// The first item in the iterator will be the one that is most in the
+ /// background.
+ pub fn push_multiple<I>(&mut self, items: I)
+ where
+ I: IntoIterator<Item = (Point, FrameItem)>,
+ {
+ Arc::make_mut(&mut self.items).extend(items);
+ }
+
+ /// Add a frame at a position in the foreground.
+ ///
+ /// Automatically decides whether to inline the frame or to include it as a
+ /// group based on the number of items in it.
+ pub fn push_frame(&mut self, pos: Point, frame: Frame) {
+ if self.should_inline(&frame) {
+ self.inline(self.layer(), pos, frame);
+ } else {
+ self.push(pos, FrameItem::Group(GroupItem::new(frame)));
+ }
+ }
+
+ /// Insert an item at the given layer in the frame.
+ ///
+ /// This panics if the layer is greater than the number of layers present.
+ #[track_caller]
+ pub fn insert(&mut self, layer: usize, pos: Point, item: FrameItem) {
+ Arc::make_mut(&mut self.items).insert(layer, (pos, item));
+ }
+
+ /// Add an item at a position in the background.
+ pub fn prepend(&mut self, pos: Point, item: FrameItem) {
+ self.insert(0, pos, item);
+ }
+
+ /// Add multiple items at a position in the background.
+ ///
+ /// The first item in the iterator will be the one that is most in the
+ /// background.
+ pub fn prepend_multiple<I>(&mut self, items: I)
+ where
+ I: IntoIterator<Item = (Point, FrameItem)>,
+ {
+ Arc::make_mut(&mut self.items).splice(0..0, items);
+ }
+
+ /// Add a frame at a position in the background.
+ pub fn prepend_frame(&mut self, pos: Point, frame: Frame) {
+ if self.should_inline(&frame) {
+ self.inline(0, pos, frame);
+ } else {
+ self.prepend(pos, FrameItem::Group(GroupItem::new(frame)));
+ }
+ }
+
+ /// Whether the given frame should be inlined.
+ fn should_inline(&self, frame: &Frame) -> bool {
+ // We do not inline big frames and hard frames.
+ frame.kind().is_soft() && (self.items.is_empty() || frame.items.len() <= 5)
+ }
+
+ /// 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;
+ return;
+ }
+
+ // Try to transfer the items without adjusting the position.
+ // Also try to reuse the items if the Arc isn't shared.
+ let range = layer..layer;
+ if pos.is_zero() {
+ let sink = Arc::make_mut(&mut self.items);
+ match Arc::try_unwrap(frame.items) {
+ Ok(items) => {
+ sink.splice(range, items.into_inner());
+ }
+ Err(arc) => {
+ sink.splice(range, arc.iter().cloned());
+ }
+ }
+ return;
+ }
+
+ // We have to adjust the item positions.
+ // But still try to reuse the items if the Arc isn't shared.
+ let sink = Arc::make_mut(&mut self.items);
+ match Arc::try_unwrap(frame.items) {
+ Ok(items) => {
+ sink.splice(
+ range,
+ items.into_inner().into_iter().map(|(p, e)| (p + pos, e)),
+ );
+ }
+ Err(arc) => {
+ sink.splice(range, arc.iter().cloned().map(|(p, e)| (p + pos, e)));
+ }
+ }
+ }
+}
+
+/// Modify the frame.
+impl Frame {
+ /// Remove all items from the frame.
+ pub fn clear(&mut self) {
+ if Arc::strong_count(&self.items) == 1 {
+ Arc::make_mut(&mut self.items).clear();
+ } else {
+ self.items = Arc::new(LazyHash::new(vec![]));
+ }
+ }
+
+ /// Adjust the frame's size, translate the original content by an offset
+ /// computed according to the given alignments, and return the amount of
+ /// offset.
+ pub fn resize(&mut self, target: Size, align: Axes<FixedAlignment>) -> Point {
+ if self.size == target {
+ return Point::zero();
+ }
+ let offset =
+ align.zip_map(target - self.size, FixedAlignment::position).to_point();
+ self.size = target;
+ self.translate(offset);
+ offset
+ }
+
+ /// Move the baseline and contents of the frame by an offset.
+ pub fn translate(&mut self, offset: Point) {
+ if !offset.is_zero() {
+ if let Some(baseline) = &mut self.baseline {
+ *baseline += offset.y;
+ }
+ for (point, _) in Arc::make_mut(&mut self.items).iter_mut() {
+ *point += offset;
+ }
+ }
+ }
+
+ /// Apply late-stage properties from the style chain to this frame. This
+ /// includes:
+ /// - `HideElem::hidden`
+ /// - `LinkElem::dests`
+ ///
+ /// This must be called on all frames produced by elements
+ /// that manually handle styles (because their children can have varying
+ /// styles). This currently includes flow, par, and equation.
+ ///
+ /// Other elements don't manually need to handle it because their parents
+ /// that result from realization will take care of it and the styles can
+ /// only apply to them as a whole, not part of it (because they don't manage
+ /// styles).
+ pub fn post_processed(mut self, styles: StyleChain) -> Self {
+ self.post_process(styles);
+ self
+ }
+
+ /// Post process in place.
+ pub fn post_process(&mut self, styles: StyleChain) {
+ if !self.is_empty() {
+ self.post_process_raw(
+ LinkElem::dests_in(styles),
+ HideElem::hidden_in(styles),
+ );
+ }
+ }
+
+ /// Apply raw late-stage properties from the raw data.
+ pub fn post_process_raw(&mut self, dests: SmallVec<[Destination; 1]>, hide: bool) {
+ if !self.is_empty() {
+ let size = self.size;
+ self.push_multiple(
+ dests
+ .into_iter()
+ .map(|dest| (Point::zero(), FrameItem::Link(dest, size))),
+ );
+ if hide {
+ self.hide();
+ }
+ }
+ }
+
+ /// Hide all content in the frame, but keep metadata.
+ pub fn hide(&mut self) {
+ Arc::make_mut(&mut self.items).retain_mut(|(_, item)| match item {
+ FrameItem::Group(group) => {
+ group.frame.hide();
+ !group.frame.is_empty()
+ }
+ FrameItem::Tag(_) => true,
+ _ => false,
+ });
+ }
+
+ /// Add a background fill.
+ pub fn fill(&mut self, fill: impl Into<Paint>) {
+ self.prepend(
+ Point::zero(),
+ FrameItem::Shape(Geometry::Rect(self.size()).filled(fill), Span::detached()),
+ );
+ }
+
+ /// Arbitrarily transform the contents of the frame.
+ pub fn transform(&mut self, transform: Transform) {
+ if !self.is_empty() {
+ self.group(|g| g.transform = transform);
+ }
+ }
+
+ /// Clip the contents of a frame to a clip path.
+ ///
+ /// The clip path can be the size of the frame in the case of a
+ /// rectangular frame. In the case of a frame with rounded corner,
+ /// this should be a path that matches the frame's outline.
+ pub fn clip(&mut self, clip_path: Path) {
+ if !self.is_empty() {
+ self.group(|g| g.clip_path = Some(clip_path));
+ }
+ }
+
+ /// Add a label to the frame.
+ pub fn label(&mut self, label: Label) {
+ self.group(|g| g.label = Some(label));
+ }
+
+ /// Set a parent for the frame. As a result, all elements in the frame
+ /// become logically ordered immediately after the given location.
+ pub fn set_parent(&mut self, parent: Location) {
+ if !self.is_empty() {
+ self.group(|g| g.parent = Some(parent));
+ }
+ }
+
+ /// Wrap the frame's contents in a group and modify that group with `f`.
+ fn group<F>(&mut self, f: F)
+ where
+ F: FnOnce(&mut GroupItem),
+ {
+ let mut wrapper = Frame::soft(self.size);
+ wrapper.baseline = self.baseline;
+ let mut group = GroupItem::new(std::mem::take(self));
+ f(&mut group);
+ wrapper.push(Point::zero(), FrameItem::Group(group));
+ *self = wrapper;
+ }
+}
+
+/// Tools for debugging.
+impl Frame {
+ /// Add a full size aqua background and a red baseline for debugging.
+ pub fn mark_box(mut self) -> Self {
+ self.mark_box_in_place();
+ self
+ }
+
+ /// Debug in place. Add a full size aqua background and a red baseline for debugging.
+ pub fn mark_box_in_place(&mut self) {
+ self.insert(
+ 0,
+ Point::zero(),
+ FrameItem::Shape(
+ Geometry::Rect(self.size).filled(Color::TEAL.with_alpha(0.5)),
+ Span::detached(),
+ ),
+ );
+ self.insert(
+ 1,
+ Point::with_y(self.baseline()),
+ FrameItem::Shape(
+ Geometry::Line(Point::with_x(self.size.x))
+ .stroked(FixedStroke::from_pair(Color::RED, Abs::pt(1.0))),
+ Span::detached(),
+ ),
+ );
+ }
+
+ /// Add a green marker at a position for debugging.
+ pub fn mark_point(&mut self, pos: Point) {
+ let radius = Abs::pt(2.0);
+ self.push(
+ pos - Point::splat(radius),
+ FrameItem::Shape(
+ Geometry::Path(Path::ellipse(Size::splat(2.0 * radius)))
+ .filled(Color::GREEN),
+ Span::detached(),
+ ),
+ );
+ }
+
+ /// Add a green marker line at a position for debugging.
+ pub fn mark_line(&mut self, y: Abs) {
+ self.push(
+ Point::with_y(y),
+ FrameItem::Shape(
+ Geometry::Line(Point::with_x(self.size.x))
+ .stroked(FixedStroke::from_pair(Color::GREEN, Abs::pt(1.0))),
+ Span::detached(),
+ ),
+ );
+ }
+}
+
+impl Debug for Frame {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ f.write_str("Frame ")?;
+ f.debug_list()
+ .entries(self.items.iter().map(|(_, item)| item))
+ .finish()
+ }
+}
+
+/// The hardness of a frame.
+///
+/// This corresponds to whether or not the frame is considered to be the
+/// innermost parent of its contents. This is used to determine the coordinate
+/// reference system for gradients.
+#[derive(Default, Clone, Copy, PartialEq, Eq, Hash, Debug)]
+pub enum FrameKind {
+ /// A container which follows its parent's size.
+ ///
+ /// Soft frames are the default since they do not impact the layout of
+ /// a gradient set on one of its children.
+ #[default]
+ Soft,
+ /// A container which uses its own size.
+ ///
+ /// This is used for pages, blocks, and boxes.
+ Hard,
+}
+
+impl FrameKind {
+ /// Returns `true` if the frame is soft.
+ pub fn is_soft(self) -> bool {
+ matches!(self, Self::Soft)
+ }
+
+ /// Returns `true` if the frame is hard.
+ pub fn is_hard(self) -> bool {
+ matches!(self, Self::Hard)
+ }
+}
+
+/// The building block frames are composed of.
+#[derive(Clone, Hash)]
+pub enum FrameItem {
+ /// A subframe with optional transformation and clipping.
+ Group(GroupItem),
+ /// A run of shaped text.
+ Text(TextItem),
+ /// A geometric shape with optional fill and stroke.
+ Shape(Shape, Span),
+ /// An image and its size.
+ Image(Image, Size, Span),
+ /// An internal or external link to a destination.
+ Link(Destination, Size),
+ /// An introspectable element that produced something within this frame
+ /// alongside its key.
+ Tag(Tag),
+}
+
+impl Debug for FrameItem {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ match self {
+ Self::Group(group) => group.fmt(f),
+ Self::Text(text) => write!(f, "{text:?}"),
+ Self::Shape(shape, _) => write!(f, "{shape:?}"),
+ Self::Image(image, _, _) => write!(f, "{image:?}"),
+ Self::Link(dest, _) => write!(f, "Link({dest:?})"),
+ Self::Tag(tag) => write!(f, "{tag:?}"),
+ }
+ }
+}
+
+/// A subframe with optional transformation and clipping.
+#[derive(Clone, Hash)]
+pub struct GroupItem {
+ /// The group's frame.
+ pub frame: Frame,
+ /// A transformation to apply to the group.
+ pub transform: Transform,
+ /// Whether the frame should be a clipping boundary.
+ pub clip_path: Option<Path>,
+ /// The group's label.
+ pub label: Option<Label>,
+ /// The group's logical parent. All elements in this group are logically
+ /// ordered immediately after the parent's start location.
+ pub parent: Option<Location>,
+}
+
+impl GroupItem {
+ /// Create a new group with default settings.
+ pub fn new(frame: Frame) -> Self {
+ Self {
+ frame,
+ transform: Transform::identity(),
+ clip_path: None,
+ label: None,
+ parent: None,
+ }
+ }
+}
+
+impl Debug for GroupItem {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ f.write_str("Group ")?;
+ self.frame.fmt(f)
+ }
+}
+
+/// A physical position in a document.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct Position {
+ /// The page, starting at 1.
+ pub page: NonZeroUsize,
+ /// The exact coordinates on the page (from the top left, as usual).
+ pub point: Point,
+}
+
+cast! {
+ Position,
+ self => Value::Dict(self.into()),
+ mut dict: Dict => {
+ let page = dict.take("page")?.cast()?;
+ let x: Length = dict.take("x")?.cast()?;
+ let y: Length = dict.take("y")?.cast()?;
+ dict.finish(&["page", "x", "y"])?;
+ Self { page, point: Point::new(x.abs, y.abs) }
+ },
+}
+
+impl From<Position> for Dict {
+ fn from(pos: Position) -> Self {
+ dict! {
+ "page" => pos.page,
+ "x" => pos.point.x,
+ "y" => pos.point.y,
+ }
+ }
+}