mirror of
https://github.com/typst/typst
synced 2025-05-13 20:46:23 +08:00
420 lines
12 KiB
Rust
420 lines
12 KiB
Rust
//! The core layouting engine.
|
|
|
|
use std::io::{self, Write};
|
|
use smallvec::SmallVec;
|
|
|
|
use toddle::query::{FontClass, SharedFontLoader};
|
|
|
|
use crate::TypesetResult;
|
|
use crate::func::Command;
|
|
use crate::size::{Size, Size2D, SizeBox};
|
|
use crate::style::{LayoutStyle, TextStyle};
|
|
use crate::syntax::{Node, SyntaxTree, FuncCall};
|
|
|
|
mod actions;
|
|
mod tree;
|
|
mod flex;
|
|
mod stack;
|
|
mod text;
|
|
|
|
/// Different kinds of layouters (fully re-exported).
|
|
pub mod layouters {
|
|
pub use super::tree::layout_tree;
|
|
pub use super::flex::{FlexLayouter, FlexContext};
|
|
pub use super::stack::{StackLayouter, StackContext};
|
|
pub use super::text::{layout_text, TextContext};
|
|
}
|
|
|
|
pub use actions::{LayoutAction, LayoutActions};
|
|
pub use layouters::*;
|
|
|
|
/// A collection of layouts.
|
|
pub type MultiLayout = Vec<Layout>;
|
|
|
|
/// A sequence of layouting actions inside a box.
|
|
#[derive(Debug, Clone)]
|
|
pub struct Layout {
|
|
/// The size of the box.
|
|
pub dimensions: Size2D,
|
|
/// How to align this layout in a parent container.
|
|
pub alignment: LayoutAlignment,
|
|
/// The actions composing this layout.
|
|
pub actions: Vec<LayoutAction>,
|
|
}
|
|
|
|
impl Layout {
|
|
/// Returns a vector with all used font indices.
|
|
pub fn find_used_fonts(&self) -> Vec<usize> {
|
|
let mut fonts = Vec::new();
|
|
for action in &self.actions {
|
|
if let LayoutAction::SetFont(index, _) = action {
|
|
if !fonts.contains(index) {
|
|
fonts.push(*index);
|
|
}
|
|
}
|
|
}
|
|
fonts
|
|
}
|
|
}
|
|
|
|
/// The general context for layouting.
|
|
#[derive(Debug, Clone)]
|
|
pub struct LayoutContext<'a, 'p> {
|
|
/// The font loader to retrieve fonts from when typesetting text
|
|
/// using [`layout_text`].
|
|
pub loader: &'a SharedFontLoader<'p>,
|
|
/// The style for pages and text.
|
|
pub style: &'a LayoutStyle,
|
|
/// Whether this layouting process handles the top-level pages.
|
|
pub top_level: bool,
|
|
/// The spaces to layout in.
|
|
pub spaces: LayoutSpaces,
|
|
/// The initial axes along which content is laid out.
|
|
pub axes: LayoutAxes,
|
|
/// The alignment of the finished layout.
|
|
pub alignment: LayoutAlignment,
|
|
}
|
|
|
|
/// A possibly stack-allocated vector of layout spaces.
|
|
pub type LayoutSpaces = SmallVec<[LayoutSpace; 2]>;
|
|
|
|
/// Spacial layouting constraints.
|
|
#[derive(Debug, Copy, Clone)]
|
|
pub struct LayoutSpace {
|
|
/// The maximum size of the box to layout in.
|
|
pub dimensions: Size2D,
|
|
/// Padding that should be respected on each side.
|
|
pub padding: SizeBox,
|
|
/// Whether to expand the dimensions of the resulting layout to the full
|
|
/// dimensions of this space or to shrink them to fit the content for the
|
|
/// horizontal and vertical axis.
|
|
pub expand: LayoutExpansion,
|
|
}
|
|
|
|
impl LayoutSpace {
|
|
/// The offset from the origin to the start of content, that is,
|
|
/// `(padding.left, padding.top)`.
|
|
pub fn start(&self) -> Size2D {
|
|
Size2D::new(self.padding.left, self.padding.right)
|
|
}
|
|
|
|
/// The actually usable area (dimensions minus padding).
|
|
pub fn usable(&self) -> Size2D {
|
|
self.dimensions.unpadded(self.padding)
|
|
}
|
|
|
|
/// A layout space without padding and dimensions reduced by the padding.
|
|
pub fn usable_space(&self) -> LayoutSpace {
|
|
LayoutSpace {
|
|
dimensions: self.usable(),
|
|
padding: SizeBox::ZERO,
|
|
expand: LayoutExpansion::new(false, false),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Whether to fit to content or expand to the space's size.
|
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
|
pub struct LayoutExpansion {
|
|
pub horizontal: bool,
|
|
pub vertical: bool,
|
|
}
|
|
|
|
impl LayoutExpansion {
|
|
pub fn new(horizontal: bool, vertical: bool) -> LayoutExpansion {
|
|
LayoutExpansion { horizontal, vertical }
|
|
}
|
|
}
|
|
|
|
/// The axes along which the content is laid out.
|
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
|
pub struct LayoutAxes {
|
|
pub primary: Axis,
|
|
pub secondary: Axis,
|
|
}
|
|
|
|
impl LayoutAxes {
|
|
pub fn new(primary: Axis, secondary: Axis) -> LayoutAxes {
|
|
if primary.is_horizontal() == secondary.is_horizontal() {
|
|
panic!("LayoutAxes::new: invalid parallel axes {:?} and {:?}", primary, secondary);
|
|
}
|
|
|
|
LayoutAxes { primary, secondary }
|
|
}
|
|
|
|
/// Return the specified generic axis.
|
|
pub fn generic(&self, axis: GenericAxisKind) -> Axis {
|
|
match axis {
|
|
GenericAxisKind::Primary => self.primary,
|
|
GenericAxisKind::Secondary => self.secondary,
|
|
}
|
|
}
|
|
|
|
/// Return the specified specific axis.
|
|
pub fn specific(&self, axis: SpecificAxisKind) -> Axis {
|
|
self.generic(axis.generic(*self))
|
|
}
|
|
|
|
/// Returns the generic axis kind which is the horizontal axis.
|
|
pub fn horizontal(&self) -> GenericAxisKind {
|
|
match self.primary.is_horizontal() {
|
|
true => GenericAxisKind::Primary,
|
|
false => GenericAxisKind::Secondary,
|
|
}
|
|
}
|
|
|
|
/// Returns the generic axis kind which is the vertical axis.
|
|
pub fn vertical(&self) -> GenericAxisKind {
|
|
self.horizontal().inv()
|
|
}
|
|
|
|
/// Returns the specific axis kind which is the primary axis.
|
|
pub fn primary(&self) -> SpecificAxisKind {
|
|
match self.primary.is_horizontal() {
|
|
true => SpecificAxisKind::Horizontal,
|
|
false => SpecificAxisKind::Vertical,
|
|
}
|
|
}
|
|
|
|
/// Returns the specific axis kind which is the secondary axis.
|
|
pub fn secondary(&self) -> SpecificAxisKind {
|
|
self.primary().inv()
|
|
}
|
|
|
|
/// Returns the generic alignment corresponding to left-alignment.
|
|
pub fn left(&self) -> Alignment {
|
|
let positive = match self.primary.is_horizontal() {
|
|
true => self.primary.is_positive(),
|
|
false => self.secondary.is_positive(),
|
|
};
|
|
|
|
if positive { Alignment::Origin } else { Alignment::End }
|
|
}
|
|
|
|
/// Returns the generic alignment corresponding to right-alignment.
|
|
pub fn right(&self) -> Alignment {
|
|
self.left().inv()
|
|
}
|
|
|
|
/// Returns the generic alignment corresponding to top-alignment.
|
|
pub fn top(&self) -> Alignment {
|
|
let positive = match self.primary.is_horizontal() {
|
|
true => self.secondary.is_positive(),
|
|
false => self.primary.is_positive(),
|
|
};
|
|
|
|
if positive { Alignment::Origin } else { Alignment::End }
|
|
}
|
|
|
|
/// Returns the generic alignment corresponding to bottom-alignment.
|
|
pub fn bottom(&self) -> Alignment {
|
|
self.top().inv()
|
|
}
|
|
}
|
|
|
|
impl Default for LayoutAxes {
|
|
fn default() -> LayoutAxes {
|
|
LayoutAxes {
|
|
primary: Axis::LeftToRight,
|
|
secondary: Axis::TopToBottom,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Directions along which content is laid out.
|
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
|
pub enum Axis {
|
|
LeftToRight,
|
|
RightToLeft,
|
|
TopToBottom,
|
|
BottomToTop,
|
|
}
|
|
|
|
impl Axis {
|
|
/// Whether this is a horizontal axis.
|
|
pub fn is_horizontal(&self) -> bool {
|
|
match self {
|
|
Axis::LeftToRight | Axis::RightToLeft => true,
|
|
Axis::TopToBottom | Axis::BottomToTop => false,
|
|
}
|
|
}
|
|
|
|
/// Whether this axis points into the positive coordinate direction.
|
|
pub fn is_positive(&self) -> bool {
|
|
match self {
|
|
Axis::LeftToRight | Axis::TopToBottom => true,
|
|
Axis::RightToLeft | Axis::BottomToTop => false,
|
|
}
|
|
}
|
|
|
|
/// The inverse axis.
|
|
pub fn inv(&self) -> Axis {
|
|
match self {
|
|
Axis::LeftToRight => Axis::RightToLeft,
|
|
Axis::RightToLeft => Axis::LeftToRight,
|
|
Axis::TopToBottom => Axis::BottomToTop,
|
|
Axis::BottomToTop => Axis::TopToBottom,
|
|
}
|
|
}
|
|
|
|
/// The direction factor for this axis.
|
|
///
|
|
/// - 1 if the axis is positive.
|
|
/// - -1 if the axis is negative.
|
|
pub fn factor(&self) -> i32 {
|
|
if self.is_positive() { 1 } else { -1 }
|
|
}
|
|
}
|
|
|
|
/// The two generic kinds of layouting axes.
|
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
|
pub enum GenericAxisKind {
|
|
Primary,
|
|
Secondary,
|
|
}
|
|
|
|
impl GenericAxisKind {
|
|
/// The specific version of this axis in the given system of axes.
|
|
pub fn specific(&self, axes: LayoutAxes) -> SpecificAxisKind {
|
|
match self {
|
|
GenericAxisKind::Primary => axes.primary(),
|
|
GenericAxisKind::Secondary => axes.secondary(),
|
|
}
|
|
}
|
|
|
|
/// The other axis.
|
|
pub fn inv(&self) -> GenericAxisKind {
|
|
match self {
|
|
GenericAxisKind::Primary => GenericAxisKind::Secondary,
|
|
GenericAxisKind::Secondary => GenericAxisKind::Primary,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The two specific kinds of layouting axes.
|
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
|
pub enum SpecificAxisKind {
|
|
Horizontal,
|
|
Vertical,
|
|
}
|
|
|
|
impl SpecificAxisKind {
|
|
/// The generic version of this axis in the given system of axes.
|
|
pub fn generic(&self, axes: LayoutAxes) -> GenericAxisKind {
|
|
match self {
|
|
SpecificAxisKind::Horizontal => axes.horizontal(),
|
|
SpecificAxisKind::Vertical => axes.vertical(),
|
|
}
|
|
}
|
|
|
|
/// The other axis.
|
|
pub fn inv(&self) -> SpecificAxisKind {
|
|
match self {
|
|
SpecificAxisKind::Horizontal => SpecificAxisKind::Vertical,
|
|
SpecificAxisKind::Vertical => SpecificAxisKind::Horizontal,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The place to put a layout in a container.
|
|
#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
|
pub struct LayoutAlignment {
|
|
pub primary: Alignment,
|
|
pub secondary: Alignment,
|
|
}
|
|
|
|
impl LayoutAlignment {
|
|
pub fn new(primary: Alignment, secondary: Alignment) -> LayoutAlignment {
|
|
LayoutAlignment { primary, secondary }
|
|
}
|
|
}
|
|
|
|
/// Where to align content.
|
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
|
pub enum Alignment {
|
|
Origin,
|
|
Center,
|
|
End,
|
|
}
|
|
|
|
impl Alignment {
|
|
/// The inverse alignment.
|
|
pub fn inv(&self) -> Alignment {
|
|
match self {
|
|
Alignment::Origin => Alignment::End,
|
|
Alignment::Center => Alignment::Center,
|
|
Alignment::End => Alignment::Origin,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for Alignment {
|
|
fn default() -> Alignment {
|
|
Alignment::Origin
|
|
}
|
|
}
|
|
|
|
/// Whitespace between boxes with different interaction properties.
|
|
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
|
pub enum SpacingKind {
|
|
/// A hard space consumes surrounding soft spaces and is always layouted.
|
|
Hard,
|
|
/// A soft space consumes surrounding soft spaces with higher value.
|
|
Soft(u32),
|
|
}
|
|
|
|
/// The standard spacing kind used for paragraph spacing.
|
|
const PARAGRAPH_KIND: SpacingKind = SpacingKind::Soft(1);
|
|
|
|
/// The standard spacing kind used for normal spaces between boxes.
|
|
const SPACE_KIND: SpacingKind = SpacingKind::Soft(2);
|
|
|
|
/// The last appeared spacing.
|
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
|
enum LastSpacing {
|
|
Hard,
|
|
Soft(Size, u32),
|
|
None,
|
|
}
|
|
|
|
impl LastSpacing {
|
|
/// The size of the soft space if this is a soft space or zero otherwise.
|
|
fn soft_or_zero(&self) -> Size {
|
|
match self {
|
|
LastSpacing::Soft(space, _) => *space,
|
|
_ => Size::ZERO,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Layout components that can be serialized.
|
|
pub trait Serialize {
|
|
/// Serialize the data structure into an output writable.
|
|
fn serialize<W: Write>(&self, f: &mut W) -> io::Result<()>;
|
|
}
|
|
|
|
impl Serialize for Layout {
|
|
fn serialize<W: Write>(&self, f: &mut W) -> io::Result<()> {
|
|
writeln!(f, "{:.4} {:.4}", self.dimensions.x.to_pt(), self.dimensions.y.to_pt())?;
|
|
writeln!(f, "{}", self.actions.len())?;
|
|
for action in &self.actions {
|
|
action.serialize(f)?;
|
|
writeln!(f)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl Serialize for MultiLayout {
|
|
fn serialize<W: Write>(&self, f: &mut W) -> io::Result<()> {
|
|
writeln!(f, "{}", self.len())?;
|
|
for layout in self {
|
|
layout.serialize(f)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// The result type for layouting.
|
|
pub type LayoutResult<T> = TypesetResult<T>;
|