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::