diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/eval/template.rs | 6 | ||||
| -rw-r--r-- | src/eval/walk.rs | 3 | ||||
| -rw-r--r-- | src/export/pdf.rs | 2 | ||||
| -rw-r--r-- | src/frame.rs (renamed from src/layout/frame.rs) | 8 | ||||
| -rw-r--r-- | src/layout/constraints.rs | 24 | ||||
| -rw-r--r-- | src/layout/incremental.rs | 6 | ||||
| -rw-r--r-- | src/layout/levels.rs | 199 | ||||
| -rw-r--r-- | src/layout/mod.rs | 227 | ||||
| -rw-r--r-- | src/layout/text.rs | 370 | ||||
| -rw-r--r-- | src/lib.rs | 4 | ||||
| -rw-r--r-- | src/library/align.rs | 51 | ||||
| -rw-r--r-- | src/library/container.rs | 27 | ||||
| -rw-r--r-- | src/library/deco.rs (renamed from src/layout/deco.rs) | 47 | ||||
| -rw-r--r-- | src/library/elements.rs | 104 | ||||
| -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.rs | 332 | ||||
| -rw-r--r-- | src/library/mod.rs | 48 | ||||
| -rw-r--r-- | src/library/pad.rs (renamed from src/layout/pad.rs) | 26 | ||||
| -rw-r--r-- | src/library/page.rs | 76 | ||||
| -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.rs | 24 | ||||
| -rw-r--r-- | src/library/stack.rs (renamed from src/layout/stack.rs) | 50 | ||||
| -rw-r--r-- | src/library/text.rs | 420 | ||||
| -rw-r--r-- | src/library/transform.rs | 44 | ||||
| -rw-r--r-- | src/library/utility.rs | 3 |
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, ®ions).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, ®ions).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) -} @@ -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> { |
