From 5b344b663a3d224134923eea0d67ebf44c069b07 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Sun, 31 Oct 2021 15:52:16 +0100 Subject: [PATCH] Reorganize modules Instead of separating functionality into layout and library, everything lives in the library now. This way, related things live side by side and there are no duplicate file names in the two directories. --- src/eval/template.rs | 6 +- src/eval/walk.rs | 3 +- src/export/pdf.rs | 2 +- src/{layout => }/frame.rs | 8 +- src/layout/constraints.rs | 24 +- src/layout/incremental.rs | 6 +- src/layout/levels.rs | 199 ++++++++++++++ src/layout/mod.rs | 227 +--------------- src/layout/text.rs | 370 ------------------------- src/lib.rs | 4 +- src/library/align.rs | 51 ++++ src/library/container.rs | 27 ++ src/{layout => library}/deco.rs | 47 +++- src/library/elements.rs | 104 ------- src/{layout => library}/grid.rs | 53 +++- src/{layout => library}/image.rs | 26 +- src/library/layout.rs | 332 ----------------------- src/library/mod.rs | 48 +++- src/{layout => library}/pad.rs | 26 +- src/library/page.rs | 76 ++++++ src/{layout => library}/par.rs | 58 +++- src/{layout => library}/shape.rs | 81 +++++- src/library/spacing.rs | 24 ++ src/{layout => library}/stack.rs | 50 +++- src/library/text.rs | 448 +++++++++++++++++++++++++------ src/library/transform.rs | 44 +++ src/library/utility.rs | 3 +- tests/typeset.rs | 3 +- 28 files changed, 1197 insertions(+), 1153 deletions(-) rename src/{layout => }/frame.rs (96%) create mode 100644 src/layout/levels.rs delete mode 100644 src/layout/text.rs create mode 100644 src/library/align.rs create mode 100644 src/library/container.rs rename src/{layout => library}/deco.rs (67%) delete mode 100644 src/library/elements.rs rename src/{layout => library}/grid.rs (90%) rename src/{layout => library}/image.rs (67%) delete mode 100644 src/library/layout.rs rename src/{layout => library}/pad.rs (78%) create mode 100644 src/library/page.rs rename src/{layout => library}/par.rs (93%) rename src/{layout => library}/shape.rs (59%) create mode 100644 src/library/spacing.rs rename src/{layout => library}/stack.rs (82%) create mode 100644 src/library/transform.rs diff --git a/src/eval/template.rs b/src/eval/template.rs index 42c93b56d..2622a1f06 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 cd9809a29..06747e409 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 1ac8149d9..5649d5527 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 similarity index 96% rename from src/layout/frame.rs rename to src/frame.rs index 82f60e22a..68aa2e9cf 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> { - Constrained { item: Rc::new(self), cts } - } } impl Debug for Frame { diff --git a/src/layout/constraints.rs b/src/layout/constraints.rs index 11f4e5c20..fdcda276d 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>; +} + +impl Constrain for Frame { + fn constrain(self, cts: Constraints) -> Constrained> { + 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 { pub cts: Constraints, } +impl Constrained { + /// 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 2f6dccd0d..a90bac1d2 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>> { vec![Constrained { diff --git a/src/layout/levels.rs b/src/layout/levels.rs new file mode 100644 index 000000000..a6b8d0503 --- /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>; +} + +/// 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> { + // 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 PageLevel for T +where + T: AsRef<[PageNode]> + Debug + ?Sized, +{ + fn layout(&self, ctx: &mut LayoutContext) -> Vec> { + 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>>; + + /// 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, + #[cfg(feature = "layout-cache")] + hash: u64, +} + +impl BlockLevel for BlockNode { + fn layout( + &self, + ctx: &mut LayoutContext, + regions: &Regions, + ) -> Vec>> { + #[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::>() + ); + 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(&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, + #[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(&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 ffbf26685..49ceccf63 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>; -} - -/// 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> { - // 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 PageLevel for T -where - T: AsRef<[PageNode]> + Debug + ?Sized, -{ - fn layout(&self, ctx: &mut LayoutContext) -> Vec> { - 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>>; - - /// 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, - #[cfg(feature = "layout-cache")] - hash: u64, -} - -impl BlockLevel for BlockNode { - fn layout( - &self, - ctx: &mut LayoutContext, - regions: &Regions, - ) -> Vec>> { - #[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::>() - ); - 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(&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, - #[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(&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 a89d7e3bb..000000000 --- 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, - ) -> 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) -> 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 { - 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, - base: usize, - text: &str, - size: Length, - variant: FontVariant, - mut families: impl Iterator + Clone, - mut first_face: Option, - 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 2ae87fc65..41b2e88b9 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 000000000..c6f96a136 --- /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 { + let first = args.find::(); + let second = args.find::(); + let body = args.find::