summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/eval/template.rs6
-rw-r--r--src/eval/walk.rs3
-rw-r--r--src/export/pdf.rs2
-rw-r--r--src/frame.rs (renamed from src/layout/frame.rs)8
-rw-r--r--src/layout/constraints.rs24
-rw-r--r--src/layout/incremental.rs6
-rw-r--r--src/layout/levels.rs199
-rw-r--r--src/layout/mod.rs227
-rw-r--r--src/layout/text.rs370
-rw-r--r--src/lib.rs4
-rw-r--r--src/library/align.rs51
-rw-r--r--src/library/container.rs27
-rw-r--r--src/library/deco.rs (renamed from src/layout/deco.rs)47
-rw-r--r--src/library/elements.rs104
-rw-r--r--src/library/grid.rs (renamed from src/layout/grid.rs)53
-rw-r--r--src/library/image.rs (renamed from src/layout/image.rs)26
-rw-r--r--src/library/layout.rs332
-rw-r--r--src/library/mod.rs48
-rw-r--r--src/library/pad.rs (renamed from src/layout/pad.rs)26
-rw-r--r--src/library/page.rs76
-rw-r--r--src/library/par.rs (renamed from src/layout/par.rs)58
-rw-r--r--src/library/shape.rs (renamed from src/layout/shape.rs)81
-rw-r--r--src/library/spacing.rs24
-rw-r--r--src/library/stack.rs (renamed from src/layout/stack.rs)50
-rw-r--r--src/library/text.rs420
-rw-r--r--src/library/transform.rs44
-rw-r--r--src/library/utility.rs3
27 files changed, 1181 insertions, 1138 deletions
diff --git a/src/eval/template.rs b/src/eval/template.rs
index 42c93b56..2622a1f0 100644
--- a/src/eval/template.rs
+++ b/src/eval/template.rs
@@ -8,9 +8,9 @@ use std::rc::Rc;
use super::Str;
use crate::diag::StrResult;
use crate::geom::{Align, Dir, GenAxis, Length, Linear, Sides, Size};
-use crate::layout::{
- BlockLevel, BlockNode, Decoration, InlineLevel, InlineNode, PadNode, PageNode,
- ParChild, ParNode, Spacing, StackChild, StackNode,
+use crate::layout::{BlockLevel, BlockNode, InlineLevel, InlineNode, PageNode};
+use crate::library::{
+ Decoration, PadNode, ParChild, ParNode, Spacing, StackChild, StackNode,
};
use crate::style::Style;
use crate::util::EcoString;
diff --git a/src/eval/walk.rs b/src/eval/walk.rs
index cd9809a2..06747e40 100644
--- a/src/eval/walk.rs
+++ b/src/eval/walk.rs
@@ -3,7 +3,8 @@ use std::rc::Rc;
use super::{Eval, EvalContext, Str, Template, Value};
use crate::diag::TypResult;
use crate::geom::Align;
-use crate::layout::{BlockLevel, ParChild, ParNode, Spacing, StackChild, StackNode};
+use crate::layout::BlockLevel;
+use crate::library::{ParChild, ParNode, Spacing, StackChild, StackNode};
use crate::syntax::*;
use crate::util::BoolExt;
diff --git a/src/export/pdf.rs b/src/export/pdf.rs
index 1ac8149d..5649d552 100644
--- a/src/export/pdf.rs
+++ b/src/export/pdf.rs
@@ -14,9 +14,9 @@ use ttf_parser::{name_id, GlyphId, Tag};
use super::subset;
use crate::font::{find_name, FaceId, FontStore};
+use crate::frame::{Element, Frame, Geometry};
use crate::geom::{self, Color, Em, Length, Paint, Size};
use crate::image::{Image, ImageId, ImageStore};
-use crate::layout::{Element, Frame, Geometry};
use crate::Context;
/// Export a collection of frames into a PDF document.
diff --git a/src/layout/frame.rs b/src/frame.rs
index 82f60e22..68aa2e9c 100644
--- a/src/layout/frame.rs
+++ b/src/frame.rs
@@ -1,9 +1,10 @@
+//! Finished layouts.
+
use std::fmt::{self, Debug, Formatter};
use std::rc::Rc;
use serde::{Deserialize, Serialize};
-use super::{Constrained, Constraints};
use crate::font::FaceId;
use crate::geom::{Em, Length, Paint, Path, Point, Size};
use crate::image::ImageId;
@@ -58,11 +59,6 @@ impl Frame {
pub fn elements(&self) -> Elements {
Elements { stack: vec![(0, Point::zero(), self)] }
}
-
- /// Wrap the frame with constraints.
- pub fn constrain(self, cts: Constraints) -> Constrained<Rc<Self>> {
- Constrained { item: Rc::new(self), cts }
- }
}
impl Debug for Frame {
diff --git a/src/layout/constraints.rs b/src/layout/constraints.rs
index 11f4e5c2..fdcda276 100644
--- a/src/layout/constraints.rs
+++ b/src/layout/constraints.rs
@@ -1,4 +1,19 @@
-use super::*;
+use std::rc::Rc;
+
+use crate::frame::Frame;
+use crate::geom::{Length, Size, Spec};
+
+/// Constrain a frame with constraints.
+pub trait Constrain {
+ /// Reference-count the frame and wrap it with constraints.
+ fn constrain(self, cts: Constraints) -> Constrained<Rc<Frame>>;
+}
+
+impl Constrain for Frame {
+ fn constrain(self, cts: Constraints) -> Constrained<Rc<Frame>> {
+ Constrained::new(Rc::new(self), cts)
+ }
+}
/// Carries an item that is only valid in certain regions and the constraints
/// that describe these regions.
@@ -10,6 +25,13 @@ pub struct Constrained<T> {
pub cts: Constraints,
}
+impl<T> Constrained<T> {
+ /// Constrain an item with constraints.
+ pub fn new(item: T, cts: Constraints) -> Self {
+ Self { item, cts }
+ }
+}
+
/// Describe regions that match them.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub struct Constraints {
diff --git a/src/layout/incremental.rs b/src/layout/incremental.rs
index 2f6dccd0..a90bac1d 100644
--- a/src/layout/incremental.rs
+++ b/src/layout/incremental.rs
@@ -1,10 +1,12 @@
use std::cmp::Reverse;
use std::collections::HashMap;
+use std::rc::Rc;
use decorum::N32;
use itertools::Itertools;
-use super::*;
+use super::{Constrained, Regions};
+use crate::frame::Frame;
const TEMP_LEN: usize = 5;
const TEMP_LAST: usize = TEMP_LEN - 1;
@@ -396,6 +398,8 @@ impl PatternProperties {
#[cfg(test)]
mod tests {
use super::*;
+ use crate::geom::{Size, Spec};
+ use crate::layout::Constraints;
fn empty_frames() -> Vec<Constrained<Rc<Frame>>> {
vec![Constrained {
diff --git a/src/layout/levels.rs b/src/layout/levels.rs
new file mode 100644
index 00000000..a6b8d050
--- /dev/null
+++ b/src/layout/levels.rs
@@ -0,0 +1,199 @@
+use std::fmt::{self, Debug, Formatter};
+use std::hash::{Hash, Hasher};
+use std::rc::Rc;
+
+use super::*;
+use crate::geom::{Length, Size};
+
+/// Page-level nodes directly produce frames representing pages.
+///
+/// Such nodes create their own regions instead of being supplied with them from
+/// some parent.
+pub trait PageLevel: Debug {
+ /// Layout the node, producing one frame per page.
+ fn layout(&self, ctx: &mut LayoutContext) -> Vec<Rc<Frame>>;
+}
+
+/// Layouts its children onto one or multiple pages.
+#[derive(Debug)]
+pub struct PageNode {
+ /// The size of the page.
+ pub size: Size,
+ /// The node that produces the actual pages.
+ pub child: BlockNode,
+}
+
+impl PageLevel for PageNode {
+ fn layout(&self, ctx: &mut LayoutContext) -> Vec<Rc<Frame>> {
+ // When one of the lengths is infinite the page fits its content along
+ // that axis.
+ let expand = self.size.to_spec().map(Length::is_finite);
+ let regions = Regions::repeat(self.size, self.size, expand);
+ self.child.layout(ctx, &regions).into_iter().map(|c| c.item).collect()
+ }
+}
+
+impl<T> PageLevel for T
+where
+ T: AsRef<[PageNode]> + Debug + ?Sized,
+{
+ fn layout(&self, ctx: &mut LayoutContext) -> Vec<Rc<Frame>> {
+ self.as_ref().iter().flat_map(|node| node.layout(ctx)).collect()
+ }
+}
+
+/// Block-level nodes can be layouted into a sequence of regions.
+///
+/// They return one frame per used region alongside constraints that define
+/// whether the result is reusable in other regions.
+pub trait BlockLevel: Debug {
+ /// Layout the node into the given regions, producing constrained frames.
+ fn layout(
+ &self,
+ ctx: &mut LayoutContext,
+ regions: &Regions,
+ ) -> Vec<Constrained<Rc<Frame>>>;
+
+ /// Convert to a packed block-level node.
+ fn pack(self) -> BlockNode
+ where
+ Self: Sized + Hash + 'static,
+ {
+ BlockNode {
+ #[cfg(feature = "layout-cache")]
+ hash: hash_node(&self),
+ node: Rc::new(self),
+ }
+ }
+}
+
+/// A packed [block-level](BlockLevel) layouting node with precomputed hash.
+#[derive(Clone)]
+pub struct BlockNode {
+ node: Rc<dyn BlockLevel>,
+ #[cfg(feature = "layout-cache")]
+ hash: u64,
+}
+
+impl BlockLevel for BlockNode {
+ fn layout(
+ &self,
+ ctx: &mut LayoutContext,
+ regions: &Regions,
+ ) -> Vec<Constrained<Rc<Frame>>> {
+ #[cfg(not(feature = "layout-cache"))]
+ return self.node.layout(ctx, regions);
+
+ #[cfg(feature = "layout-cache")]
+ ctx.layouts.get(self.hash, regions).unwrap_or_else(|| {
+ ctx.level += 1;
+ let frames = self.node.layout(ctx, regions);
+ ctx.level -= 1;
+
+ let entry = FramesEntry::new(frames.clone(), ctx.level);
+
+ #[cfg(debug_assertions)]
+ if !entry.check(regions) {
+ eprintln!("node: {:#?}", self.node);
+ eprintln!("regions: {:#?}", regions);
+ eprintln!(
+ "constraints: {:#?}",
+ frames.iter().map(|c| c.cts).collect::<Vec<_>>()
+ );
+ panic!("constraints did not match regions they were created for");
+ }
+
+ ctx.layouts.insert(self.hash, entry);
+ frames
+ })
+ }
+
+ fn pack(self) -> BlockNode
+ where
+ Self: Sized + Hash + 'static,
+ {
+ self
+ }
+}
+
+impl Hash for BlockNode {
+ fn hash<H: Hasher>(&self, _state: &mut H) {
+ #[cfg(feature = "layout-cache")]
+ _state.write_u64(self.hash);
+ #[cfg(not(feature = "layout-cache"))]
+ unimplemented!()
+ }
+}
+
+impl Debug for BlockNode {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ self.node.fmt(f)
+ }
+}
+
+/// Inline-level nodes are layouted as part of paragraph layout.
+///
+/// They only know the width and not the height of the paragraph's region and
+/// return only a single frame.
+pub trait InlineLevel: Debug {
+ /// Layout the node into a frame.
+ fn layout(&self, ctx: &mut LayoutContext, space: Length, base: Size) -> Frame;
+
+ /// Convert to a packed inline-level node.
+ fn pack(self) -> InlineNode
+ where
+ Self: Sized + Hash + 'static,
+ {
+ InlineNode {
+ #[cfg(feature = "layout-cache")]
+ hash: hash_node(&self),
+ node: Rc::new(self),
+ }
+ }
+}
+
+/// A packed [inline-level](InlineLevel) layouting node with precomputed hash.
+#[derive(Clone)]
+pub struct InlineNode {
+ node: Rc<dyn InlineLevel>,
+ #[cfg(feature = "layout-cache")]
+ hash: u64,
+}
+
+impl InlineLevel for InlineNode {
+ fn layout(&self, ctx: &mut LayoutContext, space: Length, base: Size) -> Frame {
+ self.node.layout(ctx, space, base)
+ }
+
+ fn pack(self) -> InlineNode
+ where
+ Self: Sized + Hash + 'static,
+ {
+ self
+ }
+}
+
+impl Hash for InlineNode {
+ fn hash<H: Hasher>(&self, _state: &mut H) {
+ #[cfg(feature = "layout-cache")]
+ _state.write_u64(self.hash);
+ #[cfg(not(feature = "layout-cache"))]
+ unimplemented!()
+ }
+}
+
+impl Debug for InlineNode {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ self.node.fmt(f)
+ }
+}
+
+/// Hash a node alongside its type id.
+#[cfg(feature = "layout-cache")]
+fn hash_node(node: &(impl Hash + 'static)) -> u64 {
+ use std::any::Any;
+ let mut state = fxhash::FxHasher64::default();
+ node.type_id().hash(&mut state);
+ node.hash(&mut state);
+ state.finish()
+}
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index ffbf2668..49ceccf6 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -1,41 +1,22 @@
//! Layouting.
mod constraints;
-mod deco;
-mod frame;
-mod grid;
-mod image;
#[cfg(feature = "layout-cache")]
mod incremental;
-mod pad;
-mod par;
+mod levels;
mod regions;
-mod shape;
-mod stack;
-mod text;
-pub use self::image::*;
pub use constraints::*;
-pub use deco::*;
-pub use frame::*;
-pub use grid::*;
#[cfg(feature = "layout-cache")]
pub use incremental::*;
-pub use pad::*;
-pub use par::*;
+pub use levels::*;
pub use regions::*;
-pub use shape::*;
-pub use stack::*;
-pub use text::*;
-use std::fmt::{self, Debug, Formatter};
-use std::hash::{Hash, Hasher};
use std::rc::Rc;
use crate::font::FontStore;
-use crate::geom::*;
+use crate::frame::Frame;
use crate::image::ImageStore;
-use crate::util::OptionExt;
use crate::Context;
/// Layout a page-level node into a collection of frames.
@@ -74,205 +55,3 @@ impl<'a> LayoutContext<'a> {
}
}
}
-
-/// Page-level nodes directly produce frames representing pages.
-///
-/// Such nodes create their own regions instead of being supplied with them from
-/// some parent.
-pub trait PageLevel: Debug {
- /// Layout the node, producing one frame per page.
- fn layout(&self, ctx: &mut LayoutContext) -> Vec<Rc<Frame>>;
-}
-
-/// Layouts its children onto one or multiple pages.
-#[derive(Debug)]
-pub struct PageNode {
- /// The size of the page.
- pub size: Size,
- /// The node that produces the actual pages.
- pub child: BlockNode,
-}
-
-impl PageLevel for PageNode {
- fn layout(&self, ctx: &mut LayoutContext) -> Vec<Rc<Frame>> {
- // When one of the lengths is infinite the page fits its content along
- // that axis.
- let expand = self.size.to_spec().map(Length::is_finite);
- let regions = Regions::repeat(self.size, self.size, expand);
- self.child.layout(ctx, &regions).into_iter().map(|c| c.item).collect()
- }
-}
-
-impl<T> PageLevel for T
-where
- T: AsRef<[PageNode]> + Debug + ?Sized,
-{
- fn layout(&self, ctx: &mut LayoutContext) -> Vec<Rc<Frame>> {
- self.as_ref().iter().flat_map(|node| node.layout(ctx)).collect()
- }
-}
-
-/// Block-level nodes can be layouted into a sequence of regions.
-///
-/// They return one frame per used region alongside constraints that define
-/// whether the result is reusable in other regions.
-pub trait BlockLevel: Debug {
- /// Layout the node into the given regions, producing constrained frames.
- fn layout(
- &self,
- ctx: &mut LayoutContext,
- regions: &Regions,
- ) -> Vec<Constrained<Rc<Frame>>>;
-
- /// Convert to a packed block-level node.
- fn pack(self) -> BlockNode
- where
- Self: Sized + Hash + 'static,
- {
- BlockNode {
- #[cfg(feature = "layout-cache")]
- hash: hash_node(&self),
- node: Rc::new(self),
- }
- }
-}
-
-/// A packed [block-level](BlockLevel) layouting node with precomputed hash.
-#[derive(Clone)]
-pub struct BlockNode {
- node: Rc<dyn BlockLevel>,
- #[cfg(feature = "layout-cache")]
- hash: u64,
-}
-
-impl BlockLevel for BlockNode {
- fn layout(
- &self,
- ctx: &mut LayoutContext,
- regions: &Regions,
- ) -> Vec<Constrained<Rc<Frame>>> {
- #[cfg(not(feature = "layout-cache"))]
- return self.node.layout(ctx, regions);
-
- #[cfg(feature = "layout-cache")]
- ctx.layouts.get(self.hash, regions).unwrap_or_else(|| {
- ctx.level += 1;
- let frames = self.node.layout(ctx, regions);
- ctx.level -= 1;
-
- let entry = FramesEntry::new(frames.clone(), ctx.level);
-
- #[cfg(debug_assertions)]
- if !entry.check(regions) {
- eprintln!("node: {:#?}", self.node);
- eprintln!("regions: {:#?}", regions);
- eprintln!(
- "constraints: {:#?}",
- frames.iter().map(|c| c.cts).collect::<Vec<_>>()
- );
- panic!("constraints did not match regions they were created for");
- }
-
- ctx.layouts.insert(self.hash, entry);
- frames
- })
- }
-
- fn pack(self) -> BlockNode
- where
- Self: Sized + Hash + 'static,
- {
- self
- }
-}
-
-impl Hash for BlockNode {
- fn hash<H: Hasher>(&self, _state: &mut H) {
- #[cfg(feature = "layout-cache")]
- _state.write_u64(self.hash);
- #[cfg(not(feature = "layout-cache"))]
- unimplemented!()
- }
-}
-
-impl Debug for BlockNode {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- self.node.fmt(f)
- }
-}
-
-/// Inline-level nodes are layouted as part of paragraph layout.
-///
-/// They only know the width and not the height of the paragraph's region and
-/// return only a single frame.
-pub trait InlineLevel: Debug {
- /// Layout the node into a frame.
- fn layout(&self, ctx: &mut LayoutContext, space: Length, base: Size) -> Frame;
-
- /// Convert to a packed inline-level node.
- fn pack(self) -> InlineNode
- where
- Self: Sized + Hash + 'static,
- {
- InlineNode {
- #[cfg(feature = "layout-cache")]
- hash: hash_node(&self),
- node: Rc::new(self),
- }
- }
-}
-
-/// A packed [inline-level](InlineLevel) layouting node with precomputed hash.
-#[derive(Clone)]
-pub struct InlineNode {
- node: Rc<dyn InlineLevel>,
- #[cfg(feature = "layout-cache")]
- hash: u64,
-}
-
-impl InlineLevel for InlineNode {
- fn layout(&self, ctx: &mut LayoutContext, space: Length, base: Size) -> Frame {
- self.node.layout(ctx, space, base)
- }
-
- fn pack(self) -> InlineNode
- where
- Self: Sized + Hash + 'static,
- {
- self
- }
-}
-
-impl Hash for InlineNode {
- fn hash<H: Hasher>(&self, _state: &mut H) {
- #[cfg(feature = "layout-cache")]
- _state.write_u64(self.hash);
- #[cfg(not(feature = "layout-cache"))]
- unimplemented!()
- }
-}
-
-impl Debug for InlineNode {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- self.node.fmt(f)
- }
-}
-
-/// Hash a node alongside its type id.
-#[cfg(feature = "layout-cache")]
-fn hash_node(node: &(impl Hash + 'static)) -> u64 {
- use std::any::Any;
- let mut state = fxhash::FxHasher64::default();
- node.type_id().hash(&mut state);
- node.hash(&mut state);
- state.finish()
-}
-
-/// Kinds of spacing.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum Spacing {
- /// A length stated in absolute values and/or relative to the parent's size.
- Linear(Linear),
- /// A length that is the fraction of the remaining free space in the parent.
- Fractional(Fractional),
-}
diff --git a/src/layout/text.rs b/src/layout/text.rs
deleted file mode 100644
index a89d7e3b..00000000
--- a/src/layout/text.rs
+++ /dev/null
@@ -1,370 +0,0 @@
-use std::borrow::Cow;
-use std::ops::Range;
-
-use rustybuzz::UnicodeBuffer;
-
-use super::*;
-use crate::font::{Face, FaceId, FontVariant};
-use crate::geom::{Dir, Em, Length, Point, Size};
-use crate::style::TextStyle;
-use crate::util::SliceExt;
-
-/// Shape text into [`ShapedText`].
-pub fn shape<'a>(
- ctx: &mut LayoutContext,
- text: &'a str,
- style: &'a TextStyle,
- dir: Dir,
-) -> ShapedText<'a> {
- let mut glyphs = vec![];
- if !text.is_empty() {
- shape_segment(
- ctx,
- &mut glyphs,
- 0,
- text,
- style.size,
- style.variant(),
- style.families(),
- None,
- dir,
- );
- }
-
- let (size, baseline) = measure(ctx, &glyphs, style);
- ShapedText {
- text,
- dir,
- style,
- size,
- baseline,
- glyphs: Cow::Owned(glyphs),
- }
-}
-
-/// The result of shaping text.
-///
-/// This type contains owned or borrowed shaped text runs, which can be
-/// measured, used to reshape substrings more quickly and converted into a
-/// frame.
-#[derive(Debug, Clone)]
-pub struct ShapedText<'a> {
- /// The text that was shaped.
- pub text: &'a str,
- /// The text direction.
- pub dir: Dir,
- /// The properties used for font selection.
- pub style: &'a TextStyle,
- /// The font size.
- pub size: Size,
- /// The baseline from the top of the frame.
- pub baseline: Length,
- /// The shaped glyphs.
- pub glyphs: Cow<'a, [ShapedGlyph]>,
-}
-
-/// A single glyph resulting from shaping.
-#[derive(Debug, Copy, Clone)]
-pub struct ShapedGlyph {
- /// The font face the glyph is contained in.
- pub face_id: FaceId,
- /// The glyph's index in the face.
- pub glyph_id: u16,
- /// The advance width of the glyph.
- pub x_advance: Em,
- /// The horizontal offset of the glyph.
- pub x_offset: Em,
- /// The start index of the glyph in the source text.
- pub text_index: usize,
- /// Whether splitting the shaping result before this glyph would yield the
- /// same results as shaping the parts to both sides of `text_index`
- /// separately.
- pub safe_to_break: bool,
-}
-
-impl<'a> ShapedText<'a> {
- /// Build the shaped text's frame.
- pub fn build(&self) -> Frame {
- let mut frame = Frame::new(self.size, self.baseline);
- let mut offset = Length::zero();
-
- for (face_id, group) in self.glyphs.as_ref().group_by_key(|g| g.face_id) {
- let pos = Point::new(offset, self.baseline);
-
- let mut text = Text {
- face_id,
- size: self.style.size,
- width: Length::zero(),
- fill: self.style.fill,
- glyphs: vec![],
- };
-
- for glyph in group {
- text.glyphs.push(Glyph {
- id: glyph.glyph_id,
- x_advance: glyph.x_advance,
- x_offset: glyph.x_offset,
- });
- text.width += glyph.x_advance.to_length(text.size);
- }
-
- offset += text.width;
- frame.push(pos, Element::Text(text));
- }
-
- frame
- }
-
- /// Reshape a range of the shaped text, reusing information from this
- /// shaping process if possible.
- pub fn reshape(
- &'a self,
- ctx: &mut LayoutContext,
- text_range: Range<usize>,
- ) -> ShapedText<'a> {
- if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) {
- let (size, baseline) = measure(ctx, glyphs, self.style);
- Self {
- text: &self.text[text_range],
- dir: self.dir,
- style: self.style,
- size,
- baseline,
- glyphs: Cow::Borrowed(glyphs),
- }
- } else {
- shape(ctx, &self.text[text_range], self.style, self.dir)
- }
- }
-
- /// Find the subslice of glyphs that represent the given text range if both
- /// sides are safe to break.
- fn slice_safe_to_break(&self, text_range: Range<usize>) -> Option<&[ShapedGlyph]> {
- let Range { mut start, mut end } = text_range;
- if !self.dir.is_positive() {
- std::mem::swap(&mut start, &mut end);
- }
-
- let left = self.find_safe_to_break(start, Side::Left)?;
- let right = self.find_safe_to_break(end, Side::Right)?;
- Some(&self.glyphs[left .. right])
- }
-
- /// Find the glyph offset matching the text index that is most towards the
- /// given side and safe-to-break.
- fn find_safe_to_break(&self, text_index: usize, towards: Side) -> Option<usize> {
- let ltr = self.dir.is_positive();
-
- // Handle edge cases.
- let len = self.glyphs.len();
- if text_index == 0 {
- return Some(if ltr { 0 } else { len });
- } else if text_index == self.text.len() {
- return Some(if ltr { len } else { 0 });
- }
-
- // Find any glyph with the text index.
- let mut idx = self
- .glyphs
- .binary_search_by(|g| {
- let ordering = g.text_index.cmp(&text_index);
- if ltr { ordering } else { ordering.reverse() }
- })
- .ok()?;
-
- let next = match towards {
- Side::Left => usize::checked_sub,
- Side::Right => usize::checked_add,
- };
-
- // Search for the outermost glyph with the text index.
- while let Some(next) = next(idx, 1) {
- if self.glyphs.get(next).map_or(true, |g| g.text_index != text_index) {
- break;
- }
- idx = next;
- }
-
- // RTL needs offset one because the left side of the range should be
- // exclusive and the right side inclusive, contrary to the normal
- // behaviour of ranges.
- if !ltr {
- idx += 1;
- }
-
- self.glyphs[idx].safe_to_break.then(|| idx)
- }
-}
-
-/// A visual side.
-enum Side {
- Left,
- Right,
-}
-
-/// Shape text with font fallback using the `families` iterator.
-fn shape_segment<'a>(
- ctx: &mut LayoutContext,
- glyphs: &mut Vec<ShapedGlyph>,
- base: usize,
- text: &str,
- size: Length,
- variant: FontVariant,
- mut families: impl Iterator<Item = &'a str> + Clone,
- mut first_face: Option<FaceId>,
- dir: Dir,
-) {
- // Select the font family.
- let (face_id, fallback) = loop {
- // Try to load the next available font family.
- match families.next() {
- Some(family) => {
- if let Some(id) = ctx.fonts.select(family, variant) {
- break (id, true);
- }
- }
- // We're out of families, so we don't do any more fallback and just
- // shape the tofus with the first face we originally used.
- None => match first_face {
- Some(id) => break (id, false),
- None => return,
- },
- }
- };
-
- // Remember the id if this the first available face since we use that one to
- // shape tofus.
- first_face.get_or_insert(face_id);
-
- // Fill the buffer with our text.
- let mut buffer = UnicodeBuffer::new();
- buffer.push_str(text);
- buffer.set_direction(match dir {
- Dir::LTR => rustybuzz::Direction::LeftToRight,
- Dir::RTL => rustybuzz::Direction::RightToLeft,
- _ => unimplemented!(),
- });
-
- // Shape!
- let mut face = ctx.fonts.get(face_id);
- let buffer = rustybuzz::shape(face.ttf(), &[], buffer);
- let infos = buffer.glyph_infos();
- let pos = buffer.glyph_positions();
-
- // Collect the shaped glyphs, doing fallback and shaping parts again with
- // the next font if necessary.
- let mut i = 0;
- while i < infos.len() {
- let info = &infos[i];
- let cluster = info.cluster as usize;
-
- if info.glyph_id != 0 || !fallback {
- // Add the glyph to the shaped output.
- // TODO: Don't ignore y_advance and y_offset.
- glyphs.push(ShapedGlyph {
- face_id,
- glyph_id: info.glyph_id as u16,
- x_advance: face.to_em(pos[i].x_advance),
- x_offset: face.to_em(pos[i].x_offset),
- text_index: base + cluster,
- safe_to_break: !info.unsafe_to_break(),
- });
- } else {
- // Determine the source text range for the tofu sequence.
- let range = {
- // First, search for the end of the tofu sequence.
- let k = i;
- while infos.get(i + 1).map_or(false, |info| info.glyph_id == 0) {
- i += 1;
- }
-
- // Then, determine the start and end text index.
- //
- // Examples:
- // Everything is shown in visual order. Tofus are written as "_".
- // We want to find out that the tofus span the text `2..6`.
- // Note that the clusters are longer than 1 char.
- //
- // Left-to-right:
- // Text: h a l i h a l l o
- // Glyphs: A _ _ C E
- // Clusters: 0 2 4 6 8
- // k=1 i=2
- //
- // Right-to-left:
- // Text: O L L A H I L A H
- // Glyphs: E C _ _ A
- // Clusters: 8 6 4 2 0
- // k=2 i=3
-
- let ltr = dir.is_positive();
- let first = if ltr { k } else { i };
- let start = infos[first].cluster as usize;
-
- let last = if ltr { i.checked_add(1) } else { k.checked_sub(1) };
- let end = last
- .and_then(|last| infos.get(last))
- .map_or(text.len(), |info| info.cluster as usize);
-
- start .. end
- };
-
- // Recursively shape the tofu sequence with the next family.
- shape_segment(
- ctx,
- glyphs,
- base + range.start,
- &text[range],
- size,
- variant,
- families.clone(),
- first_face,
- dir,
- );
-
- face = ctx.fonts.get(face_id);
- }
-
- i += 1;
- }
-}
-
-/// Measure the size and baseline of a run of shaped glyphs with the given
-/// properties.
-fn measure(
- ctx: &mut LayoutContext,
- glyphs: &[ShapedGlyph],
- style: &TextStyle,
-) -> (Size, Length) {
- let mut width = Length::zero();
- let mut top = Length::zero();
- let mut bottom = Length::zero();
-
- // Expand top and bottom by reading the face's vertical metrics.
- let mut expand = |face: &Face| {
- top.set_max(face.vertical_metric(style.top_edge, style.size));
- bottom.set_max(-face.vertical_metric(style.bottom_edge, style.size));
- };
-
- if glyphs.is_empty() {
- // When there are no glyphs, we just use the vertical metrics of the
- // first available font.
- for family in style.families() {
- if let Some(face_id) = ctx.fonts.select(family, style.variant) {
- expand(ctx.fonts.get(face_id));
- break;
- }
- }
- } else {
- for (face_id, group) in glyphs.group_by_key(|g| g.face_id) {
- let face = ctx.fonts.get(face_id);
- expand(face);
-
- for glyph in group {
- width += glyph.x_advance.to_length(style.size);
- }
- }
- }
-
- (Size::new(width, top + bottom), top)
-}
diff --git a/src/lib.rs b/src/lib.rs
index 2ae87fc6..41b2e88b 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -33,6 +33,7 @@ pub mod diag;
pub mod eval;
pub mod export;
pub mod font;
+pub mod frame;
pub mod geom;
pub mod image;
pub mod layout;
@@ -49,10 +50,11 @@ use std::rc::Rc;
use crate::diag::TypResult;
use crate::eval::{Module, Scope};
use crate::font::FontStore;
+use crate::frame::Frame;
use crate::image::ImageStore;
+use crate::layout::PageNode;
#[cfg(feature = "layout-cache")]
use crate::layout::{EvictionPolicy, LayoutCache};
-use crate::layout::{Frame, PageNode};
use crate::loading::Loader;
use crate::source::{SourceId, SourceStore};
use crate::style::Style;
diff --git a/src/library/align.rs b/src/library/align.rs
new file mode 100644
index 00000000..c6f96a13
--- /dev/null
+++ b/src/library/align.rs
@@ -0,0 +1,51 @@
+use super::prelude::*;
+
+/// `align`: Configure the alignment along the layouting axes.
+pub fn align(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ let first = args.find::<Align>();
+ let second = args.find::<Align>();
+ let body = args.find::<Template>();
+
+ let mut horizontal = args.named("horizontal")?;
+ let mut vertical = args.named("vertical")?;
+
+ for value in first.into_iter().chain(second) {
+ match value.axis() {
+ Some(SpecAxis::Horizontal) | None if horizontal.is_none() => {
+ horizontal = Some(value);
+ }
+ Some(SpecAxis::Vertical) | None if vertical.is_none() => {
+ vertical = Some(value);
+ }
+ _ => {}
+ }
+ }
+
+ let realign = |template: &mut Template| {
+ template.modify(move |style| {
+ if let Some(horizontal) = horizontal {
+ style.aligns.inline = horizontal;
+ }
+
+ if let Some(vertical) = vertical {
+ style.aligns.block = vertical;
+ }
+ });
+
+ if vertical.is_some() {
+ template.parbreak();
+ }
+ };
+
+ Ok(if let Some(body) = body {
+ let mut template = Template::new();
+ template.save();
+ realign(&mut template);
+ template += body;
+ template.restore();
+ Value::Template(template)
+ } else {
+ realign(&mut ctx.template);
+ Value::None
+ })
+}
diff --git a/src/library/container.rs b/src/library/container.rs
new file mode 100644
index 00000000..b2591cce
--- /dev/null
+++ b/src/library/container.rs
@@ -0,0 +1,27 @@
+use super::prelude::*;
+use super::{ShapeKind, ShapeNode};
+
+/// `box`: Place content in a rectangular box.
+pub fn box_(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ let width = args.named("width")?;
+ let height = args.named("height")?;
+ let fill = args.named("fill")?;
+ let body: Template = args.find().unwrap_or_default();
+ Ok(Value::Template(Template::from_inline(move |style| {
+ ShapeNode {
+ shape: ShapeKind::Rect,
+ width,
+ height,
+ fill: fill.map(Paint::Color),
+ child: Some(body.to_stack(style).pack()),
+ }
+ })))
+}
+
+/// `block`: Place content in a block.
+pub fn block(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ let body: Template = args.expect("body")?;
+ Ok(Value::Template(Template::from_block(move |style| {
+ body.to_stack(style)
+ })))
+}
diff --git a/src/layout/deco.rs b/src/library/deco.rs
index 669bf404..18ca2bb1 100644
--- a/src/layout/deco.rs
+++ b/src/library/deco.rs
@@ -1,6 +1,51 @@
-use super::*;
+use super::prelude::*;
use crate::util::EcoString;
+/// `strike`: Typeset striken-through text.
+pub fn strike(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ line_impl(args, LineKind::Strikethrough)
+}
+
+/// `underline`: Typeset underlined text.
+pub fn underline(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ line_impl(args, LineKind::Underline)
+}
+
+/// `overline`: Typeset text with an overline.
+pub fn overline(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ line_impl(args, LineKind::Overline)
+}
+
+fn line_impl(args: &mut Args, kind: LineKind) -> TypResult<Value> {
+ let stroke = args.named("stroke")?.or_else(|| args.find());
+ let thickness = args.named::<Linear>("thickness")?.or_else(|| args.find());
+ let offset = args.named("offset")?;
+ let extent = args.named("extent")?.unwrap_or_default();
+ let body: Template = args.expect("body")?;
+
+ Ok(Value::Template(body.decorate(Decoration::Line(
+ LineDecoration {
+ kind,
+ stroke: stroke.map(Paint::Color),
+ thickness,
+ offset,
+ extent,
+ },
+ ))))
+}
+
+/// `link`: Typeset text as a link.
+pub fn link(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ let url = args.expect::<Str>("url")?;
+ let body = args.find().unwrap_or_else(|| {
+ let mut template = Template::new();
+ template.text(url.trim_start_matches("mailto:").trim_start_matches("tel:"));
+ template
+ });
+
+ Ok(Value::Template(body.decorate(Decoration::Link(url.into()))))
+}
+
/// A decoration for a frame.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum Decoration {
diff --git a/src/library/elements.rs b/src/library/elements.rs
deleted file mode 100644
index 01efb405..00000000
--- a/src/library/elements.rs
+++ /dev/null
@@ -1,104 +0,0 @@
-use std::io;
-
-use super::*;
-use crate::diag::Error;
-use crate::layout::{ImageNode, ShapeKind, ShapeNode};
-
-/// `image`: An image.
-pub fn image(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let path = args.expect::<Spanned<Str>>("path to image file")?;
- let width = args.named("width")?;
- let height = args.named("height")?;
-
- let full = ctx.make_path(&path.v);
- let id = ctx.images.load(&full).map_err(|err| {
- Error::boxed(path.span, match err.kind() {
- io::ErrorKind::NotFound => "file not found".into(),
- _ => format!("failed to load image ({})", err),
- })
- })?;
-
- Ok(Value::Template(Template::from_inline(move |_| ImageNode {
- id,
- width,
- height,
- })))
-}
-
-/// `rect`: A rectangle with optional content.
-pub fn rect(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let width = args.named("width")?;
- let height = args.named("height")?;
- let fill = args.named("fill")?;
- let body = args.find();
- Ok(shape_impl(ShapeKind::Rect, width, height, fill, body))
-}
-
-/// `square`: A square with optional content.
-pub fn square(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let size = args.named::<Length>("size")?.map(Linear::from);
- let width = match size {
- None => args.named("width")?,
- size => size,
- };
- let height = match size {
- None => args.named("height")?,
- size => size,
- };
- let fill = args.named("fill")?;
- let body = args.find();
- Ok(shape_impl(ShapeKind::Square, width, height, fill, body))
-}
-
-/// `ellipse`: An ellipse with optional content.
-pub fn ellipse(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let width = args.named("width")?;
- let height = args.named("height")?;
- let fill = args.named("fill")?;
- let body = args.find();
- Ok(shape_impl(ShapeKind::Ellipse, width, height, fill, body))
-}
-
-/// `circle`: A circle with optional content.
-pub fn circle(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let diameter = args.named("radius")?.map(|r: Length| 2.0 * Linear::from(r));
- let width = match diameter {
- None => args.named("width")?,
- diameter => diameter,
- };
- let height = match diameter {
- None => args.named("height")?,
- diameter => diameter,
- };
- let fill = args.named("fill")?;
- let body = args.find();
- Ok(shape_impl(ShapeKind::Circle, width, height, fill, body))
-}
-
-fn shape_impl(
- shape: ShapeKind,
- mut width: Option<Linear>,
- mut height: Option<Linear>,
- fill: Option<Color>,
- body: Option<Template>,
-) -> Value {
- // Set default shape size if there's no body.
- if body.is_none() {
- let v = Length::pt(30.0).into();
- height.get_or_insert(v);
- width.get_or_insert(match shape {
- ShapeKind::Square | ShapeKind::Circle => v,
- ShapeKind::Rect | ShapeKind::Ellipse => 1.5 * v,
- });
- }
-
- Value::Template(Template::from_inline(move |style| ShapeNode {
- shape,
- width,
- height,
- fill: Some(Paint::Color(
- fill.unwrap_or(Color::Rgba(RgbaColor::new(175, 175, 175, 255))),
- )),
- child: body.as_ref().map(|template| template.to_stack(style).pack()),
- }))
-}
diff --git a/src/layout/grid.rs b/src/library/grid.rs
index 57986b48..af486496 100644
--- a/src/layout/grid.rs
+++ b/src/library/grid.rs
@@ -1,4 +1,55 @@
-use super::*;
+use super::prelude::*;
+
+/// `grid`: Arrange children into a grid.
+pub fn grid(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ castable! {
+ Vec<TrackSizing>: "integer or (auto, linear, fractional, or array thereof)",
+ Value::Auto => vec![TrackSizing::Auto],
+ Value::Length(v) => vec![TrackSizing::Linear(v.into())],
+ Value::Relative(v) => vec![TrackSizing::Linear(v.into())],
+ Value::Linear(v) => vec![TrackSizing::Linear(v)],
+ Value::Fractional(v) => vec![TrackSizing::Fractional(v)],
+ Value::Int(count) => vec![TrackSizing::Auto; count.max(0) as usize],
+ Value::Array(values) => values
+ .into_iter()
+ .filter_map(|v| v.cast().ok())
+ .collect(),
+ }
+
+ castable! {
+ TrackSizing: "auto, linear, or fractional",
+ Value::Auto => Self::Auto,
+ Value::Length(v) => Self::Linear(v.into()),
+ Value::Relative(v) => Self::Linear(v.into()),
+ Value::Linear(v) => Self::Linear(v),
+ Value::Fractional(v) => Self::Fractional(v),
+ }
+
+ let columns = args.named("columns")?.unwrap_or_default();
+ let rows = args.named("rows")?.unwrap_or_default();
+ let tracks = Spec::new(columns, rows);
+
+ let base_gutter: Vec<TrackSizing> = args.named("gutter")?.unwrap_or_default();
+ let column_gutter = args.named("column-gutter")?;
+ let row_gutter = args.named("row-gutter")?;
+ let gutter = Spec::new(
+ column_gutter.unwrap_or_else(|| base_gutter.clone()),
+ row_gutter.unwrap_or(base_gutter),
+ );
+
+ let children: Vec<Template> = args.all().collect();
+
+ Ok(Value::Template(Template::from_block(move |style| {
+ GridNode {
+ tracks: tracks.clone(),
+ gutter: gutter.clone(),
+ children: children
+ .iter()
+ .map(|child| child.to_stack(&style).pack())
+ .collect(),
+ }
+ })))
+}
/// A node that arranges its children in a grid.
#[derive(Debug, Hash)]
diff --git a/src/layout/image.rs b/src/library/image.rs
index b410895b..c2273502 100644
--- a/src/layout/image.rs
+++ b/src/library/image.rs
@@ -1,6 +1,30 @@
-use super::*;
+use std::io;
+
+use super::prelude::*;
+use crate::diag::Error;
use crate::image::ImageId;
+/// `image`: An image.
+pub fn image(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ let path = args.expect::<Spanned<Str>>("path to image file")?;
+ let width = args.named("width")?;
+ let height = args.named("height")?;
+
+ let full = ctx.make_path(&path.v);
+ let id = ctx.images.load(&full).map_err(|err| {
+ Error::boxed(path.span, match err.kind() {
+ io::ErrorKind::NotFound => "file not found".into(),
+ _ => format!("failed to load image ({})", err),
+ })
+ })?;
+
+ Ok(Value::Template(Template::from_inline(move |_| ImageNode {
+ id,
+ width,
+ height,
+ })))
+}
+
/// An image node.
#[derive(Debug, Hash)]
pub struct ImageNode {
diff --git a/src/library/layout.rs b/src/library/layout.rs
deleted file mode 100644
index e5650664..00000000
--- a/src/library/layout.rs
+++ /dev/null
@@ -1,332 +0,0 @@
-use super::*;
-use crate::layout::{
- GridNode, PadNode, ShapeKind, ShapeNode, StackChild, StackNode, TrackSizing,
-};
-use crate::style::{Paper, PaperClass};
-
-/// `page`: Configure pages.
-pub fn page(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let paper = match args.named::<Spanned<Str>>("paper")?.or_else(|| args.find()) {
- Some(name) => match Paper::from_name(&name.v) {
- None => bail!(name.span, "invalid paper name"),
- paper => paper,
- },
- None => None,
- };
-
- let width = args.named("width")?;
- let height = args.named("height")?;
- let margins = args.named("margins")?;
- let left = args.named("left")?;
- let top = args.named("top")?;
- let right = args.named("right")?;
- let bottom = args.named("bottom")?;
- let flip = args.named("flip")?;
-
- ctx.template.modify(move |style| {
- let page = style.page_mut();
-
- if let Some(paper) = paper {
- page.class = paper.class();
- page.size = paper.size();
- }
-
- if let Some(width) = width {
- page.class = PaperClass::Custom;
- page.size.w = width;
- }
-
- if let Some(height) = height {
- page.class = PaperClass::Custom;
- page.size.h = height;
- }
-
- if let Some(margins) = margins {
- page.margins = Sides::splat(Some(margins));
- }
-
- if let Some(left) = left {
- page.margins.left = Some(left);
- }
-
- if let Some(top) = top {
- page.margins.top = Some(top);
- }
-
- if let Some(right) = right {
- page.margins.right = Some(right);
- }
-
- if let Some(bottom) = bottom {
- page.margins.bottom = Some(bottom);
- }
-
- if flip.unwrap_or(false) {
- std::mem::swap(&mut page.size.w, &mut page.size.h);
- }
- });
-
- ctx.template.pagebreak(false);
-
- Ok(Value::None)
-}
-
-/// `pagebreak`: Start a new page.
-pub fn pagebreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> {
- let mut template = Template::new();
- template.pagebreak(true);
- Ok(Value::Template(template))
-}
-
-/// `align`: Configure the alignment along the layouting axes.
-pub fn align(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let first = args.find::<Align>();
- let second = args.find::<Align>();
- let body = args.find::<Template>();
-
- let mut horizontal = args.named("horizontal")?;
- let mut vertical = args.named("vertical")?;
-
- for value in first.into_iter().chain(second) {
- match value.axis() {
- Some(SpecAxis::Horizontal) | None if horizontal.is_none() => {
- horizontal = Some(value);
- }
- Some(SpecAxis::Vertical) | None if vertical.is_none() => {
- vertical = Some(value);
- }
- _ => {}
- }
- }
-
- let realign = |template: &mut Template| {
- template.modify(move |style| {
- if let Some(horizontal) = horizontal {
- style.aligns.inline = horizontal;
- }
-
- if let Some(vertical) = vertical {
- style.aligns.block = vertical;
- }
- });
-
- if vertical.is_some() {
- template.parbreak();
- }
- };
-
- Ok(if let Some(body) = body {
- let mut template = Template::new();
- template.save();
- realign(&mut template);
- template += body;
- template.restore();
- Value::Template(template)
- } else {
- realign(&mut ctx.template);
- Value::None
- })
-}
-
-/// `h`: Horizontal spacing.
-pub fn h(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let mut template = Template::new();
- template.spacing(GenAxis::Inline, args.expect("spacing")?);
- Ok(Value::Template(template))
-}
-
-/// `v`: Vertical spacing.
-pub fn v(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let mut template = Template::new();
- template.spacing(GenAxis::Block, args.expect("spacing")?);
- Ok(Value::Template(template))
-}
-
-/// `box`: Place content in a rectangular box.
-pub fn box_(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let width = args.named("width")?;
- let height = args.named("height")?;
- let fill = args.named("fill")?;
- let body: Template = args.find().unwrap_or_default();
- Ok(Value::Template(Template::from_inline(move |style| {
- ShapeNode {
- shape: ShapeKind::Rect,
- width,
- height,
- fill: fill.map(Paint::Color),
- child: Some(body.to_stack(style).pack()),
- }
- })))
-}
-
-/// `block`: Place content in a block.
-pub fn block(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let body: Template = args.expect("body")?;
- Ok(Value::Template(Template::from_block(move |style| {
- body.to_stack(style)
- })))
-}
-
-/// `pad`: Pad content at the sides.
-pub fn pad(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let all = args.find();
- let left = args.named("left")?;
- let top = args.named("top")?;
- let right = args.named("right")?;
- let bottom = args.named("bottom")?;
- let body: Template = args.expect("body")?;
-
- let padding = Sides::new(
- left.or(all).unwrap_or_default(),
- top.or(all).unwrap_or_default(),
- right.or(all).unwrap_or_default(),
- bottom.or(all).unwrap_or_default(),
- );
-
- Ok(Value::Template(Template::from_block(move |style| {
- PadNode {
- padding,
- child: body.to_stack(&style).pack(),
- }
- })))
-}
-
-/// `move`: Move content without affecting layout.
-pub fn move_(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- #[derive(Debug, Hash)]
- struct MoveNode {
- offset: Spec<Option<Linear>>,
- child: ShapeNode,
- }
-
- impl InlineLevel for MoveNode {
- fn layout(&self, ctx: &mut LayoutContext, space: Length, base: Size) -> Frame {
- let offset = Point::new(
- self.offset.x.map(|x| x.resolve(base.w)).unwrap_or_default(),
- self.offset.y.map(|y| y.resolve(base.h)).unwrap_or_default(),
- );
-
- let mut frame = self.child.layout(ctx, space, base);
- for (point, _) in &mut frame.children {
- *point += offset;
- }
-
- frame
- }
- }
-
- let x = args.named("x")?;
- let y = args.named("y")?;
- let body: Template = args.expect("body")?;
-
- Ok(Value::Template(Template::from_inline(move |style| {
- MoveNode {
- offset: Spec::new(x, y),
- child: ShapeNode {
- shape: ShapeKind::Rect,
- width: None,
- height: None,
- fill: None,
- child: Some(body.to_stack(style).pack()),
- },
- }
- })))
-}
-
-/// `stack`: Stack children along an axis.
-pub fn stack(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- enum Child {
- Spacing(Spacing),
- Any(Template),
- }
-
- castable! {
- Child: "linear, fractional or template",
- Value::Length(v) => Self::Spacing(Spacing::Linear(v.into())),
- Value::Relative(v) => Self::Spacing(Spacing::Linear(v.into())),
- Value::Linear(v) => Self::Spacing(Spacing::Linear(v)),
- Value::Fractional(v) => Self::Spacing(Spacing::Fractional(v)),
- Value::Template(v) => Self::Any(v),
- }
-
- let dir = args.named("dir")?.unwrap_or(Dir::TTB);
- let spacing = args.named("spacing")?;
- let list: Vec<Child> = args.all().collect();
-
- Ok(Value::Template(Template::from_block(move |style| {
- let mut children = vec![];
- let mut delayed = None;
-
- // Build the list of stack children.
- for child in &list {
- match child {
- Child::Spacing(v) => {
- children.push(StackChild::Spacing(*v));
- delayed = None;
- }
- Child::Any(template) => {
- if let Some(v) = delayed {
- children.push(StackChild::Spacing(v));
- }
-
- let node = template.to_stack(style).pack();
- children.push(StackChild::Node(node, style.aligns.block));
- delayed = spacing;
- }
- }
- }
-
- StackNode { dir, children }
- })))
-}
-
-/// `grid`: Arrange children into a grid.
-pub fn grid(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- castable! {
- Vec<TrackSizing>: "integer or (auto, linear, fractional, or array thereof)",
- Value::Auto => vec![TrackSizing::Auto],
- Value::Length(v) => vec![TrackSizing::Linear(v.into())],
- Value::Relative(v) => vec![TrackSizing::Linear(v.into())],
- Value::Linear(v) => vec![TrackSizing::Linear(v)],
- Value::Fractional(v) => vec![TrackSizing::Fractional(v)],
- Value::Int(count) => vec![TrackSizing::Auto; count.max(0) as usize],
- Value::Array(values) => values
- .into_iter()
- .filter_map(|v| v.cast().ok())
- .collect(),
- }
-
- castable! {
- TrackSizing: "auto, linear, or fractional",
- Value::Auto => Self::Auto,
- Value::Length(v) => Self::Linear(v.into()),
- Value::Relative(v) => Self::Linear(v.into()),
- Value::Linear(v) => Self::Linear(v),
- Value::Fractional(v) => Self::Fractional(v),
- }
-
- let columns = args.named("columns")?.unwrap_or_default();
- let rows = args.named("rows")?.unwrap_or_default();
- let tracks = Spec::new(columns, rows);
-
- let base_gutter: Vec<TrackSizing> = args.named("gutter")?.unwrap_or_default();
- let column_gutter = args.named("column-gutter")?;
- let row_gutter = args.named("row-gutter")?;
- let gutter = Spec::new(
- column_gutter.unwrap_or_else(|| base_gutter.clone()),
- row_gutter.unwrap_or(base_gutter),
- );
-
- let children: Vec<Template> = args.all().collect();
-
- Ok(Value::Template(Template::from_block(move |style| {
- GridNode {
- tracks: tracks.clone(),
- gutter: gutter.clone(),
- children: children
- .iter()
- .map(|child| child.to_stack(&style).pack())
- .collect(),
- }
- })))
-}
diff --git a/src/library/mod.rs b/src/library/mod.rs
index 6d3de11d..9a1dc2e9 100644
--- a/src/library/mod.rs
+++ b/src/library/mod.rs
@@ -3,26 +3,54 @@
//! Call [`new`] to obtain a [`Scope`] containing all standard library
//! definitions.
-mod elements;
-mod layout;
+mod align;
+mod container;
+mod deco;
+mod grid;
+mod image;
+mod pad;
+mod page;
+mod par;
+mod shape;
+mod spacing;
+mod stack;
mod text;
+mod transform;
mod utility;
-pub use elements::*;
-pub use layout::*;
+/// Helpful imports for creating library functionality.
+mod prelude {
+ pub use std::rc::Rc;
+
+ pub use crate::diag::{At, TypResult};
+ pub use crate::eval::{Args, EvalContext, Str, Template, Value};
+ pub use crate::frame::*;
+ pub use crate::geom::*;
+ pub use crate::layout::*;
+ pub use crate::syntax::{Span, Spanned};
+ pub use crate::util::OptionExt;
+}
+
+pub use self::image::*;
+pub use align::*;
+pub use container::*;
+pub use deco::*;
+pub use grid::*;
+pub use pad::*;
+pub use page::*;
+pub use par::*;
+pub use shape::*;
+pub use spacing::*;
+pub use stack::*;
pub use text::*;
+pub use transform::*;
pub use utility::*;
use std::convert::TryFrom;
-use std::rc::Rc;
-use crate::diag::{At, TypResult};
-use crate::eval::{Args, Array, EvalContext, Scope, Str, Template, Value};
+use crate::eval::{Scope, Value};
use crate::font::{FontFamily, FontStretch, FontStyle, FontWeight, VerticalFontMetric};
use crate::geom::*;
-use crate::layout::{BlockLevel, Frame, InlineLevel, LayoutContext, Spacing};
-use crate::style::Style;
-use crate::syntax::{Span, Spanned};
/// Construct a scope containing all standard library definitions.
pub fn new() -> Scope {
diff --git a/src/layout/pad.rs b/src/library/pad.rs
index 52766dfa..6457f603 100644
--- a/src/layout/pad.rs
+++ b/src/library/pad.rs
@@ -1,4 +1,28 @@
-use super::*;
+use super::prelude::*;
+
+/// `pad`: Pad content at the sides.
+pub fn pad(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ let all = args.find();
+ let left = args.named("left")?;
+ let top = args.named("top")?;
+ let right = args.named("right")?;
+ let bottom = args.named("bottom")?;
+ let body: Template = args.expect("body")?;
+
+ let padding = Sides::new(
+ left.or(all).unwrap_or_default(),
+ top.or(all).unwrap_or_default(),
+ right.or(all).unwrap_or_default(),
+ bottom.or(all).unwrap_or_default(),
+ );
+
+ Ok(Value::Template(Template::from_block(move |style| {
+ PadNode {
+ padding,
+ child: body.to_stack(&style).pack(),
+ }
+ })))
+}
/// A node that adds padding to its child.
#[derive(Debug, Hash)]
diff --git a/src/library/page.rs b/src/library/page.rs
new file mode 100644
index 00000000..7e1bd05b
--- /dev/null
+++ b/src/library/page.rs
@@ -0,0 +1,76 @@
+use super::prelude::*;
+use crate::style::{Paper, PaperClass};
+
+/// `page`: Configure pages.
+pub fn page(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ let paper = match args.named::<Spanned<Str>>("paper")?.or_else(|| args.find()) {
+ Some(name) => match Paper::from_name(&name.v) {
+ None => bail!(name.span, "invalid paper name"),
+ paper => paper,
+ },
+ None => None,
+ };
+
+ let width = args.named("width")?;
+ let height = args.named("height")?;
+ let margins = args.named("margins")?;
+ let left = args.named("left")?;
+ let top = args.named("top")?;
+ let right = args.named("right")?;
+ let bottom = args.named("bottom")?;
+ let flip = args.named("flip")?;
+
+ ctx.template.modify(move |style| {
+ let page = style.page_mut();
+
+ if let Some(paper) = paper {
+ page.class = paper.class();
+ page.size = paper.size();
+ }
+
+ if let Some(width) = width {
+ page.class = PaperClass::Custom;
+ page.size.w = width;
+ }
+
+ if let Some(height) = height {
+ page.class = PaperClass::Custom;
+ page.size.h = height;
+ }
+
+ if let Some(margins) = margins {
+ page.margins = Sides::splat(Some(margins));
+ }
+
+ if let Some(left) = left {
+ page.margins.left = Some(left);
+ }
+
+ if let Some(top) = top {
+ page.margins.top = Some(top);
+ }
+
+ if let Some(right) = right {
+ page.margins.right = Some(right);
+ }
+
+ if let Some(bottom) = bottom {
+ page.margins.bottom = Some(bottom);
+ }
+
+ if flip.unwrap_or(false) {
+ std::mem::swap(&mut page.size.w, &mut page.size.h);
+ }
+ });
+
+ ctx.template.pagebreak(false);
+
+ Ok(Value::None)
+}
+
+/// `pagebreak`: Start a new page.
+pub fn pagebreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> {
+ let mut template = Template::new();
+ template.pagebreak(true);
+ Ok(Value::Template(template))
+}
diff --git a/src/layout/par.rs b/src/library/par.rs
index a645eb07..3330eedf 100644
--- a/src/layout/par.rs
+++ b/src/library/par.rs
@@ -5,11 +5,63 @@ use itertools::Either;
use unicode_bidi::{BidiInfo, Level};
use xi_unicode::LineBreakIterator;
-use super::*;
+use super::prelude::*;
+use super::{shape, Decoration, ShapedText, Spacing};
use crate::style::TextStyle;
use crate::util::{EcoString, RangeExt, SliceExt};
-type Range = std::ops::Range<usize>;
+/// `par`: Configure paragraphs.
+pub fn par(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ let spacing = args.named("spacing")?;
+ let leading = args.named("leading")?;
+
+ ctx.template.modify(move |style| {
+ let par = style.par_mut();
+
+ if let Some(spacing) = spacing {
+ par.spacing = spacing;
+ }
+
+ if let Some(leading) = leading {
+ par.leading = leading;
+ }
+ });
+
+ ctx.template.parbreak();
+
+ Ok(Value::None)
+}
+
+/// `lang`: Configure the language.
+pub fn lang(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ let iso = args.find::<Str>();
+ let dir = if let Some(dir) = args.named::<Spanned<Dir>>("dir")? {
+ if dir.v.axis() == SpecAxis::Horizontal {
+ Some(dir.v)
+ } else {
+ bail!(dir.span, "must be horizontal");
+ }
+ } else {
+ iso.as_deref().map(lang_dir)
+ };
+
+ if let Some(dir) = dir {
+ ctx.template.modify(move |style| style.dir = dir);
+ }
+
+ ctx.template.parbreak();
+
+ Ok(Value::None)
+}
+
+/// The default direction for the language identified by the given `iso` code.
+fn lang_dir(iso: &str) -> Dir {
+ match iso.to_ascii_lowercase().as_str() {
+ "ar" | "he" | "fa" | "ur" | "ps" | "yi" => Dir::RTL,
+ "en" | "fr" | "de" => Dir::LTR,
+ _ => Dir::LTR,
+ }
+}
/// A node that arranges its children into a paragraph.
#[derive(Debug, Hash)]
@@ -104,6 +156,8 @@ impl Debug for ParChild {
}
}
+type Range = std::ops::Range<usize>;
+
/// A paragraph representation in which children are already layouted and text
/// is separated into shapable runs.
struct ParLayouter<'a> {
diff --git a/src/layout/shape.rs b/src/library/shape.rs
index ed70dd95..1eba2285 100644
--- a/src/layout/shape.rs
+++ b/src/library/shape.rs
@@ -1,8 +1,87 @@
use std::f64::consts::SQRT_2;
-use super::*;
+use super::prelude::*;
+use super::PadNode;
use crate::util::RcExt;
+/// `rect`: A rectangle with optional content.
+pub fn rect(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ let width = args.named("width")?;
+ let height = args.named("height")?;
+ let fill = args.named("fill")?;
+ let body = args.find();
+ Ok(shape_impl(ShapeKind::Rect, width, height, fill, body))
+}
+
+/// `square`: A square with optional content.
+pub fn square(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ let size = args.named::<Length>("size")?.map(Linear::from);
+ let width = match size {
+ None => args.named("width")?,
+ size => size,
+ };
+ let height = match size {
+ None => args.named("height")?,
+ size => size,
+ };
+ let fill = args.named("fill")?;
+ let body = args.find();
+ Ok(shape_impl(ShapeKind::Square, width, height, fill, body))
+}
+
+/// `ellipse`: An ellipse with optional content.
+pub fn ellipse(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ let width = args.named("width")?;
+ let height = args.named("height")?;
+ let fill = args.named("fill")?;
+ let body = args.find();
+ Ok(shape_impl(ShapeKind::Ellipse, width, height, fill, body))
+}
+
+/// `circle`: A circle with optional content.
+pub fn circle(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ let diameter = args.named("radius")?.map(|r: Length| 2.0 * Linear::from(r));
+ let width = match diameter {
+ None => args.named("width")?,
+ diameter => diameter,
+ };
+ let height = match diameter {
+ None => args.named("height")?,
+ diameter => diameter,
+ };
+ let fill = args.named("fill")?;
+ let body = args.find();
+ Ok(shape_impl(ShapeKind::Circle, width, height, fill, body))
+}
+
+fn shape_impl(
+ shape: ShapeKind,
+ mut width: Option<Linear>,
+ mut height: Option<Linear>,
+ fill: Option<Color>,
+ body: Option<Template>,
+) -> Value {
+ // Set default shape size if there's no body.
+ if body.is_none() {
+ let v = Length::pt(30.0).into();
+ height.get_or_insert(v);
+ width.get_or_insert(match shape {
+ ShapeKind::Square | ShapeKind::Circle => v,
+ ShapeKind::Rect | ShapeKind::Ellipse => 1.5 * v,
+ });
+ }
+
+ Value::Template(Template::from_inline(move |style| ShapeNode {
+ shape,
+ width,
+ height,
+ fill: Some(Paint::Color(
+ fill.unwrap_or(Color::Rgba(RgbaColor::new(175, 175, 175, 255))),
+ )),
+ child: body.as_ref().map(|template| template.to_stack(style).pack()),
+ }))
+}
+
/// Places its child into a sizable and fillable shape.
#[derive(Debug, Hash)]
pub struct ShapeNode {
diff --git a/src/library/spacing.rs b/src/library/spacing.rs
new file mode 100644
index 00000000..8ffd5e7a
--- /dev/null
+++ b/src/library/spacing.rs
@@ -0,0 +1,24 @@
+use super::prelude::*;
+
+/// `h`: Horizontal spacing.
+pub fn h(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ let mut template = Template::new();
+ template.spacing(GenAxis::Inline, args.expect("spacing")?);
+ Ok(Value::Template(template))
+}
+
+/// `v`: Vertical spacing.
+pub fn v(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ let mut template = Template::new();
+ template.spacing(GenAxis::Block, args.expect("spacing")?);
+ Ok(Value::Template(template))
+}
+
+/// Kinds of spacing.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum Spacing {
+ /// A length stated in absolute values and/or relative to the parent's size.
+ Linear(Linear),
+ /// A length that is the fraction of the remaining free space in the parent.
+ Fractional(Fractional),
+}
diff --git a/src/layout/stack.rs b/src/library/stack.rs
index 0fb3e3eb..80ed507b 100644
--- a/src/layout/stack.rs
+++ b/src/library/stack.rs
@@ -1,6 +1,54 @@
use std::fmt::{self, Debug, Formatter};
-use super::*;
+use super::prelude::*;
+use super::Spacing;
+
+/// `stack`: Stack children along an axis.
+pub fn stack(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ enum Child {
+ Spacing(Spacing),
+ Any(Template),
+ }
+
+ castable! {
+ Child: "linear, fractional or template",
+ Value::Length(v) => Self::Spacing(Spacing::Linear(v.into())),
+ Value::Relative(v) => Self::Spacing(Spacing::Linear(v.into())),
+ Value::Linear(v) => Self::Spacing(Spacing::Linear(v)),
+ Value::Fractional(v) => Self::Spacing(Spacing::Fractional(v)),
+ Value::Template(v) => Self::Any(v),
+ }
+
+ let dir = args.named("dir")?.unwrap_or(Dir::TTB);
+ let spacing = args.named("spacing")?;
+ let list: Vec<Child> = args.all().collect();
+
+ Ok(Value::Template(Template::from_block(move |style| {
+ let mut children = vec![];
+ let mut delayed = None;
+
+ // Build the list of stack children.
+ for child in &list {
+ match child {
+ Child::Spacing(v) => {
+ children.push(StackChild::Spacing(*v));
+ delayed = None;
+ }
+ Child::Any(template) => {
+ if let Some(v) = delayed {
+ children.push(StackChild::Spacing(v));
+ }
+
+ let node = template.to_stack(style).pack();
+ children.push(StackChild::Node(node, style.aligns.block));
+ delayed = spacing;
+ }
+ }
+ }
+
+ StackNode { dir, children }
+ })))
+}
/// A node that stacks its children.
#[derive(Debug, Hash)]
diff --git a/src/library/text.rs b/src/library/text.rs
index e4d52396..b33a002d 100644
--- a/src/library/text.rs
+++ b/src/library/text.rs
@@ -1,5 +1,13 @@
-use super::*;
-use crate::layout::{Decoration, LineDecoration, LineKind};
+use std::borrow::Cow;
+use std::ops::Range;
+
+use rustybuzz::UnicodeBuffer;
+
+use super::prelude::*;
+use crate::font::{Face, FaceId, FontFamily, FontVariant};
+use crate::geom::{Dir, Em, Length, Point, Size};
+use crate::style::{Style, TextStyle};
+use crate::util::SliceExt;
/// `font`: Configure the font.
pub fn font(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
@@ -108,100 +116,362 @@ pub fn font(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
})
}
-/// `par`: Configure paragraphs.
-pub fn par(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let spacing = args.named("spacing")?;
- let leading = args.named("leading")?;
+/// Shape text into [`ShapedText`].
+pub fn shape<'a>(
+ ctx: &mut LayoutContext,
+ text: &'a str,
+ style: &'a TextStyle,
+ dir: Dir,
+) -> ShapedText<'a> {
+ let mut glyphs = vec![];
+ if !text.is_empty() {
+ shape_segment(
+ ctx,
+ &mut glyphs,
+ 0,
+ text,
+ style.size,
+ style.variant(),
+ style.families(),
+ None,
+ dir,
+ );
+ }
- ctx.template.modify(move |style| {
- let par = style.par_mut();
+ let (size, baseline) = measure(ctx, &glyphs, style);
+ ShapedText {
+ text,
+ dir,
+ style,
+ size,
+ baseline,
+ glyphs: Cow::Owned(glyphs),
+ }
+}
- if let Some(spacing) = spacing {
- par.spacing = spacing;
- }
+/// The result of shaping text.
+///
+/// This type contains owned or borrowed shaped text runs, which can be
+/// measured, used to reshape substrings more quickly and converted into a
+/// frame.
+#[derive(Debug, Clone)]
+pub struct ShapedText<'a> {
+ /// The text that was shaped.
+ pub text: &'a str,
+ /// The text direction.
+ pub dir: Dir,
+ /// The properties used for font selection.
+ pub style: &'a TextStyle,
+ /// The font size.
+ pub size: Size,
+ /// The baseline from the top of the frame.
+ pub baseline: Length,
+ /// The shaped glyphs.
+ pub glyphs: Cow<'a, [ShapedGlyph]>,
+}
- if let Some(leading) = leading {
- par.leading = leading;
- }
- });
+/// A single glyph resulting from shaping.
+#[derive(Debug, Copy, Clone)]
+pub struct ShapedGlyph {
+ /// The font face the glyph is contained in.
+ pub face_id: FaceId,
+ /// The glyph's index in the face.
+ pub glyph_id: u16,
+ /// The advance width of the glyph.
+ pub x_advance: Em,
+ /// The horizontal offset of the glyph.
+ pub x_offset: Em,
+ /// The start index of the glyph in the source text.
+ pub text_index: usize,
+ /// Whether splitting the shaping result before this glyph would yield the
+ /// same results as shaping the parts to both sides of `text_index`
+ /// separately.
+ pub safe_to_break: bool,
+}
- ctx.template.parbreak();
+impl<'a> ShapedText<'a> {
+ /// Build the shaped text's frame.
+ pub fn build(&self) -> Frame {
+ let mut frame = Frame::new(self.size, self.baseline);
+ let mut offset = Length::zero();
+
+ for (face_id, group) in self.glyphs.as_ref().group_by_key(|g| g.face_id) {
+ let pos = Point::new(offset, self.baseline);
+
+ let mut text = Text {
+ face_id,
+ size: self.style.size,
+ width: Length::zero(),
+ fill: self.style.fill,
+ glyphs: vec![],
+ };
+
+ for glyph in group {
+ text.glyphs.push(Glyph {
+ id: glyph.glyph_id,
+ x_advance: glyph.x_advance,
+ x_offset: glyph.x_offset,
+ });
+ text.width += glyph.x_advance.to_length(text.size);
+ }
+
+ offset += text.width;
+ frame.push(pos, Element::Text(text));
+ }
- Ok(Value::None)
-}
+ frame
+ }
-/// `lang`: Configure the language.
-pub fn lang(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let iso = args.find::<Str>();
- let dir = if let Some(dir) = args.named::<Spanned<Dir>>("dir")? {
- if dir.v.axis() == SpecAxis::Horizontal {
- Some(dir.v)
+ /// Reshape a range of the shaped text, reusing information from this
+ /// shaping process if possible.
+ pub fn reshape(
+ &'a self,
+ ctx: &mut LayoutContext,
+ text_range: Range<usize>,
+ ) -> ShapedText<'a> {
+ if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) {
+ let (size, baseline) = measure(ctx, glyphs, self.style);
+ Self {
+ text: &self.text[text_range],
+ dir: self.dir,
+ style: self.style,
+ size,
+ baseline,
+ glyphs: Cow::Borrowed(glyphs),
+ }
} else {
- bail!(dir.span, "must be horizontal");
+ shape(ctx, &self.text[text_range], self.style, self.dir)
+ }
+ }
+
+ /// Find the subslice of glyphs that represent the given text range if both
+ /// sides are safe to break.
+ fn slice_safe_to_break(&self, text_range: Range<usize>) -> Option<&[ShapedGlyph]> {
+ let Range { mut start, mut end } = text_range;
+ if !self.dir.is_positive() {
+ std::mem::swap(&mut start, &mut end);
}
- } else {
- iso.as_deref().map(lang_dir)
- };
- if let Some(dir) = dir {
- ctx.template.modify(move |style| style.dir = dir);
+ let left = self.find_safe_to_break(start, Side::Left)?;
+ let right = self.find_safe_to_break(end, Side::Right)?;
+ Some(&self.glyphs[left .. right])
}
- ctx.template.parbreak();
+ /// Find the glyph offset matching the text index that is most towards the
+ /// given side and safe-to-break.
+ fn find_safe_to_break(&self, text_index: usize, towards: Side) -> Option<usize> {
+ let ltr = self.dir.is_positive();
+
+ // Handle edge cases.
+ let len = self.glyphs.len();
+ if text_index == 0 {
+ return Some(if ltr { 0 } else { len });
+ } else if text_index == self.text.len() {
+ return Some(if ltr { len } else { 0 });
+ }
- Ok(Value::None)
-}
+ // Find any glyph with the text index.
+ let mut idx = self
+ .glyphs
+ .binary_search_by(|g| {
+ let ordering = g.text_index.cmp(&text_index);
+ if ltr { ordering } else { ordering.reverse() }
+ })
+ .ok()?;
+
+ let next = match towards {
+ Side::Left => usize::checked_sub,
+ Side::Right => usize::checked_add,
+ };
+
+ // Search for the outermost glyph with the text index.
+ while let Some(next) = next(idx, 1) {
+ if self.glyphs.get(next).map_or(true, |g| g.text_index != text_index) {
+ break;
+ }
+ idx = next;
+ }
+
+ // RTL needs offset one because the left side of the range should be
+ // exclusive and the right side inclusive, contrary to the normal
+ // behaviour of ranges.
+ if !ltr {
+ idx += 1;
+ }
-/// The default direction for the language identified by the given `iso` code.
-fn lang_dir(iso: &str) -> Dir {
- match iso.to_ascii_lowercase().as_str() {
- "ar" | "he" | "fa" | "ur" | "ps" | "yi" => Dir::RTL,
- "en" | "fr" | "de" => Dir::LTR,
- _ => Dir::LTR,
+ self.glyphs[idx].safe_to_break.then(|| idx)
}
}
-/// `strike`: Typeset striken-through text.
-pub fn strike(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- line_impl(args, LineKind::Strikethrough)
+/// A visual side.
+enum Side {
+ Left,
+ Right,
}
-/// `underline`: Typeset underlined text.
-pub fn underline(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- line_impl(args, LineKind::Underline)
-}
+/// Shape text with font fallback using the `families` iterator.
+fn shape_segment<'a>(
+ ctx: &mut LayoutContext,
+ glyphs: &mut Vec<ShapedGlyph>,
+ base: usize,
+ text: &str,
+ size: Length,
+ variant: FontVariant,
+ mut families: impl Iterator<Item = &'a str> + Clone,
+ mut first_face: Option<FaceId>,
+ dir: Dir,
+) {
+ // Select the font family.
+ let (face_id, fallback) = loop {
+ // Try to load the next available font family.
+ match families.next() {
+ Some(family) => {
+ if let Some(id) = ctx.fonts.select(family, variant) {
+ break (id, true);
+ }
+ }
+ // We're out of families, so we don't do any more fallback and just
+ // shape the tofus with the first face we originally used.
+ None => match first_face {
+ Some(id) => break (id, false),
+ None => return,
+ },
+ }
+ };
-/// `overline`: Typeset text with an overline.
-pub fn overline(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- line_impl(args, LineKind::Overline)
-}
+ // Remember the id if this the first available face since we use that one to
+ // shape tofus.
+ first_face.get_or_insert(face_id);
+
+ // Fill the buffer with our text.
+ let mut buffer = UnicodeBuffer::new();
+ buffer.push_str(text);
+ buffer.set_direction(match dir {
+ Dir::LTR => rustybuzz::Direction::LeftToRight,
+ Dir::RTL => rustybuzz::Direction::RightToLeft,
+ _ => unimplemented!(),
+ });
+
+ // Shape!
+ let mut face = ctx.fonts.get(face_id);
+ let buffer = rustybuzz::shape(face.ttf(), &[], buffer);
+ let infos = buffer.glyph_infos();
+ let pos = buffer.glyph_positions();
+
+ // Collect the shaped glyphs, doing fallback and shaping parts again with
+ // the next font if necessary.
+ let mut i = 0;
+ while i < infos.len() {
+ let info = &infos[i];
+ let cluster = info.cluster as usize;
+
+ if info.glyph_id != 0 || !fallback {
+ // Add the glyph to the shaped output.
+ // TODO: Don't ignore y_advance and y_offset.
+ glyphs.push(ShapedGlyph {
+ face_id,
+ glyph_id: info.glyph_id as u16,
+ x_advance: face.to_em(pos[i].x_advance),
+ x_offset: face.to_em(pos[i].x_offset),
+ text_index: base + cluster,
+ safe_to_break: !info.unsafe_to_break(),
+ });
+ } else {
+ // Determine the source text range for the tofu sequence.
+ let range = {
+ // First, search for the end of the tofu sequence.
+ let k = i;
+ while infos.get(i + 1).map_or(false, |info| info.glyph_id == 0) {
+ i += 1;
+ }
+
+ // Then, determine the start and end text index.
+ //
+ // Examples:
+ // Everything is shown in visual order. Tofus are written as "_".
+ // We want to find out that the tofus span the text `2..6`.
+ // Note that the clusters are longer than 1 char.
+ //
+ // Left-to-right:
+ // Text: h a l i h a l l o
+ // Glyphs: A _ _ C E
+ // Clusters: 0 2 4 6 8
+ // k=1 i=2
+ //
+ // Right-to-left:
+ // Text: O L L A H I L A H
+ // Glyphs: E C _ _ A
+ // Clusters: 8 6 4 2 0
+ // k=2 i=3
+
+ let ltr = dir.is_positive();
+ let first = if ltr { k } else { i };
+ let start = infos[first].cluster as usize;
+
+ let last = if ltr { i.checked_add(1) } else { k.checked_sub(1) };
+ let end = last
+ .and_then(|last| infos.get(last))
+ .map_or(text.len(), |info| info.cluster as usize);
+
+ start .. end
+ };
+
+ // Recursively shape the tofu sequence with the next family.
+ shape_segment(
+ ctx,
+ glyphs,
+ base + range.start,
+ &text[range],
+ size,
+ variant,
+ families.clone(),
+ first_face,
+ dir,
+ );
+
+ face = ctx.fonts.get(face_id);
+ }
-fn line_impl(args: &mut Args, kind: LineKind) -> TypResult<Value> {
- let stroke = args.named("stroke")?.or_else(|| args.find());
- let thickness = args.named::<Linear>("thickness")?.or_else(|| args.find());
- let offset = args.named("offset")?;
- let extent = args.named("extent")?.unwrap_or_default();
- let body: Template = args.expect("body")?;
-
- Ok(Value::Template(body.decorate(Decoration::Line(
- LineDecoration {
- kind,
- stroke: stroke.map(Paint::Color),
- thickness,
- offset,
- extent,
- },
- ))))
+ i += 1;
+ }
}
-/// `link`: Typeset text as a link.
-pub fn link(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
- let url = args.expect::<Str>("url")?;
- let body = args.find().unwrap_or_else(|| {
- let mut template = Template::new();
- template.text(url.trim_start_matches("mailto:").trim_start_matches("tel:"));
- template
- });
+/// Measure the size and baseline of a run of shaped glyphs with the given
+/// properties.
+fn measure(
+ ctx: &mut LayoutContext,
+ glyphs: &[ShapedGlyph],
+ style: &TextStyle,
+) -> (Size, Length) {
+ let mut width = Length::zero();
+ let mut top = Length::zero();
+ let mut bottom = Length::zero();
+
+ // Expand top and bottom by reading the face's vertical metrics.
+ let mut expand = |face: &Face| {
+ top.set_max(face.vertical_metric(style.top_edge, style.size));
+ bottom.set_max(-face.vertical_metric(style.bottom_edge, style.size));
+ };
+
+ if glyphs.is_empty() {
+ // When there are no glyphs, we just use the vertical metrics of the
+ // first available font.
+ for family in style.families() {
+ if let Some(face_id) = ctx.fonts.select(family, style.variant) {
+ expand(ctx.fonts.get(face_id));
+ break;
+ }
+ }
+ } else {
+ for (face_id, group) in glyphs.group_by_key(|g| g.face_id) {
+ let face = ctx.fonts.get(face_id);
+ expand(face);
+
+ for glyph in group {
+ width += glyph.x_advance.to_length(style.size);
+ }
+ }
+ }
- Ok(Value::Template(body.decorate(Decoration::Link(url.into()))))
+ (Size::new(width, top + bottom), top)
}
diff --git a/src/library/transform.rs b/src/library/transform.rs
new file mode 100644
index 00000000..846a7262
--- /dev/null
+++ b/src/library/transform.rs
@@ -0,0 +1,44 @@
+use super::prelude::*;
+use super::{ShapeKind, ShapeNode};
+
+/// `move`: Move content without affecting layout.
+pub fn move_(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
+ let x = args.named("x")?;
+ let y = args.named("y")?;
+ let body: Template = args.expect("body")?;
+
+ Ok(Value::Template(Template::from_inline(move |style| {
+ MoveNode {
+ offset: Spec::new(x, y),
+ child: ShapeNode {
+ shape: ShapeKind::Rect,
+ width: None,
+ height: None,
+ fill: None,
+ child: Some(body.to_stack(style).pack()),
+ },
+ }
+ })))
+}
+
+#[derive(Debug, Hash)]
+struct MoveNode {
+ offset: Spec<Option<Linear>>,
+ child: ShapeNode,
+}
+
+impl InlineLevel for MoveNode {
+ fn layout(&self, ctx: &mut LayoutContext, space: Length, base: Size) -> Frame {
+ let offset = Point::new(
+ self.offset.x.map(|x| x.resolve(base.w)).unwrap_or_default(),
+ self.offset.y.map(|y| y.resolve(base.h)).unwrap_or_default(),
+ );
+
+ let mut frame = self.child.layout(ctx, space, base);
+ for (point, _) in &mut frame.children {
+ *point += offset;
+ }
+
+ frame
+ }
+}
diff --git a/src/library/utility.rs b/src/library/utility.rs
index 15f2bcf5..05b7639b 100644
--- a/src/library/utility.rs
+++ b/src/library/utility.rs
@@ -1,7 +1,8 @@
use std::cmp::Ordering;
use std::str::FromStr;
-use super::*;
+use super::prelude::*;
+use crate::eval::Array;
/// `assert`: Ensure that a condition is fulfilled.
pub fn assert(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {