diff options
| author | Laurenz <laurmaedje@gmail.com> | 2021-12-07 16:36:39 +0100 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2021-12-07 16:36:39 +0100 |
| commit | 40b87d4066fe85cb3fde6cf84cd60d748273ae25 (patch) | |
| tree | 792b2e5edd8e72649d9fdcac24dc07620bf0f15c /src/library | |
| parent | 26bdc1f0f6fe8113d7fcfb4d5aca46aa5238ccd8 (diff) | |
Set Rules Episode II: Attack of the properties
Diffstat (limited to 'src/library')
| -rw-r--r-- | src/library/align.rs | 28 | ||||
| -rw-r--r-- | src/library/deco.rs | 8 | ||||
| -rw-r--r-- | src/library/grid.rs | 7 | ||||
| -rw-r--r-- | src/library/mod.rs | 12 | ||||
| -rw-r--r-- | src/library/page.rs | 382 | ||||
| -rw-r--r-- | src/library/par.rs | 66 | ||||
| -rw-r--r-- | src/library/placed.rs | 15 | ||||
| -rw-r--r-- | src/library/text.rs | 908 | ||||
| -rw-r--r-- | src/library/transform.rs | 1 |
9 files changed, 929 insertions, 498 deletions
diff --git a/src/library/align.rs b/src/library/align.rs index e2e7addb..76db7fc4 100644 --- a/src/library/align.rs +++ b/src/library/align.rs @@ -2,18 +2,6 @@ use super::prelude::*; /// `align`: Configure the alignment along the layouting axes. pub fn align(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { - castable! { - Spec<Option<Align>>, - Expected: "1d or 2d alignment", - @align: Align => { - let mut aligns = Spec::default(); - aligns.set(align.axis(), Some(*align)); - aligns - }, - @aligns: Spec<Align> => aligns.map(Some), - - } - let aligns = args.expect::<Spec<_>>("alignment")?; let body = args.expect::<Node>("body")?; @@ -62,3 +50,19 @@ impl Layout for AlignNode { frames } } + +dynamic! { + Align: "alignment", +} + +castable! { + Spec<Option<Align>>, + Expected: "1d or 2d alignment", + @align: Align => { + let mut aligns = Spec::default(); + aligns.set(align.axis(), Some(*align)); + aligns + }, + @aligns: Spec<Align> => aligns.map(Some), + +} diff --git a/src/library/deco.rs b/src/library/deco.rs index 3576e8fe..b1ca030a 100644 --- a/src/library/deco.rs +++ b/src/library/deco.rs @@ -31,7 +31,11 @@ fn line_impl(args: &mut Args, kind: LineKind) -> TypResult<Value> { pub fn link(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { let url = args.expect::<EcoString>("url")?; let body = args.find().unwrap_or_else(|| { - Node::Text(url.trim_start_matches("mailto:").trim_start_matches("tel:").into()) + let mut text = url.as_str(); + for prefix in ["mailto:", "tel:"] { + text = text.trim_start_matches(prefix); + } + Node::Text(text.into()) }); Ok(Value::Node(body.decorate(Decoration::Link(url)))) } @@ -120,7 +124,7 @@ impl LineDecoration { let extent = self.extent.resolve(text.size); let subpos = Point::new(pos.x - extent, pos.y + offset); - let target = Point::new(text.width + 2.0 * extent, Length::zero()); + let target = Point::new(text.width() + 2.0 * extent, Length::zero()); let shape = Shape::stroked(Geometry::Line(target), stroke); frame.push(subpos, Element::Shape(shape)); } diff --git a/src/library/grid.rs b/src/library/grid.rs index ba4ce11f..e82f09ef 100644 --- a/src/library/grid.rs +++ b/src/library/grid.rs @@ -39,8 +39,11 @@ pub fn grid(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { row_gutter.unwrap_or(base_gutter), ); - let children = args.all().map(Node::into_block).collect(); - Ok(Value::block(GridNode { tracks, gutter, children })) + Ok(Value::block(GridNode { + tracks, + gutter, + children: args.all().map(Node::into_block).collect(), + })) } /// A node that arranges its children in a grid. diff --git a/src/library/mod.rs b/src/library/mod.rs index e1988635..7f7f95e3 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -26,7 +26,7 @@ mod prelude { pub use std::rc::Rc; pub use crate::diag::{At, TypResult}; - pub use crate::eval::{Args, EvalContext, Node, Smart, Value}; + pub use crate::eval::{Args, EvalContext, Node, Property, Smart, Styles, Value}; pub use crate::frame::*; pub use crate::geom::*; pub use crate::layout::*; @@ -54,7 +54,6 @@ pub use utility::*; use crate::eval::{Scope, Value}; use crate::geom::*; -use crate::style::FontFamily; /// Construct a scope containing all standard library definitions. pub fn new() -> Scope { @@ -139,15 +138,6 @@ dynamic! { Dir: "direction", } -dynamic! { - Align: "alignment", -} - -dynamic! { - FontFamily: "font family", - Value::Str(string) => Self::Named(string.to_lowercase()), -} - castable! { Paint, Expected: "color", diff --git a/src/library/page.rs b/src/library/page.rs index 9def5400..a4ad84f6 100644 --- a/src/library/page.rs +++ b/src/library/page.rs @@ -1,70 +1,43 @@ +#![allow(unused)] + +use std::fmt::{self, Display, Formatter}; +use std::str::FromStr; + use super::prelude::*; use super::PadNode; -use crate::style::{Paper, PaperClass}; /// `page`: Configure pages. pub fn page(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> { castable! { Paper, Expected: "string", - Value::Str(string) => Paper::from_name(&string).ok_or("unknown paper")?, - } - - let paper = args.named::<Paper>("paper")?.or_else(|| args.find()); - let width = args.named::<Smart<_>>("width")?; - let height = args.named::<Smart<_>>("height")?; - let flip = args.named("flip")?; - 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 fill = args.named("fill")?; - - let page = ctx.style.page_mut(); - - if let Some(paper) = paper { - page.class = paper.class(); - page.size = paper.size(); + Value::Str(string) => Paper::from_str(&string).map_err(|e| e.to_string())?, } - if let Some(width) = width { - page.class = PaperClass::Custom; - page.size.x = width.unwrap_or(Length::inf()); + if let Some(paper) = args.named::<Paper>("paper")?.or_else(|| args.find()) { + ctx.styles.set(PageNode::CLASS, paper.class()); + ctx.styles.set(PageNode::WIDTH, Smart::Custom(paper.width())); + ctx.styles.set(PageNode::HEIGHT, Smart::Custom(paper.height())); } - if let Some(height) = height { - page.class = PaperClass::Custom; - page.size.y = height.unwrap_or(Length::inf()); + if let Some(width) = args.named("width")? { + ctx.styles.set(PageNode::CLASS, PaperClass::Custom); + ctx.styles.set(PageNode::WIDTH, width); } - if flip.unwrap_or(false) { - std::mem::swap(&mut page.size.x, &mut page.size.y); + if let Some(height) = args.named("height")? { + ctx.styles.set(PageNode::CLASS, PaperClass::Custom); + ctx.styles.set(PageNode::HEIGHT, height); } - if let Some(margins) = margins { - page.margins = Sides::splat(margins); - } - - if let Some(left) = left { - page.margins.left = left; - } - - if let Some(top) = top { - page.margins.top = top; - } - - if let Some(right) = right { - page.margins.right = right; - } - - if let Some(bottom) = bottom { - page.margins.bottom = bottom; - } + let margins = args.named("margins")?; - if let Some(fill) = fill { - page.fill = fill; - } + set!(ctx, PageNode::FLIPPED => args.named("flipped")?); + set!(ctx, PageNode::LEFT => args.named("left")?.or(margins)); + set!(ctx, PageNode::TOP => args.named("top")?.or(margins)); + set!(ctx, PageNode::RIGHT => args.named("right")?.or(margins)); + set!(ctx, PageNode::BOTTOM => args.named("bottom")?.or(margins)); + set!(ctx, PageNode::FILL => args.named("fill")?); Ok(Value::None) } @@ -74,29 +47,69 @@ pub fn pagebreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> { Ok(Value::Node(Node::Pagebreak)) } -/// Layouts its children onto one or multiple pages. +/// Layouts its child onto one or multiple pages. #[derive(Debug, Hash)] pub struct PageNode(pub PackedNode); +properties! { + PageNode, + + /// The unflipped width of the page. + WIDTH: Smart<Length> = Smart::Custom(Paper::default().width()), + /// The unflipped height of the page. + HEIGHT: Smart<Length> = Smart::Custom(Paper::default().height()), + /// The class of paper. Defines the default margins. + CLASS: PaperClass = Paper::default().class(), + /// Whether the page is flipped into landscape orientation. + FLIPPED: bool = false, + /// The left margin. + LEFT: Smart<Linear> = Smart::Auto, + /// The right margin. + RIGHT: Smart<Linear> = Smart::Auto, + /// The top margin. + TOP: Smart<Linear> = Smart::Auto, + /// The bottom margin. + BOTTOM: Smart<Linear> = Smart::Auto, + /// The page's background color. + FILL: Option<Paint> = None, +} + impl PageNode { /// Layout the page run into a sequence of frames, one per page. pub fn layout(&self, ctx: &mut LayoutContext) -> Vec<Rc<Frame>> { - // TODO(set): Get style from styles. - let style = crate::style::PageStyle::default(); + // TODO(set): Take styles as parameter. + let styles = Styles::new(); // When one of the lengths is infinite the page fits its content along // that axis. - let expand = style.size.map(Length::is_finite); - let regions = Regions::repeat(style.size, style.size, expand); + let width = styles.get(Self::WIDTH).unwrap_or(Length::inf()); + let height = styles.get(Self::HEIGHT).unwrap_or(Length::inf()); + let mut size = Size::new(width, height); + if styles.get(Self::FLIPPED) { + std::mem::swap(&mut size.x, &mut size.y); + } - // Layout the child. - let padding = style.margins(); + // Determine the margins. + let class = styles.get(Self::CLASS); + let default = class.default_margins(); + let padding = Sides { + left: styles.get(Self::LEFT).unwrap_or(default.left), + right: styles.get(Self::RIGHT).unwrap_or(default.right), + top: styles.get(Self::TOP).unwrap_or(default.top), + bottom: styles.get(Self::BOTTOM).unwrap_or(default.bottom), + }; + + // Pad the child. let padded = PadNode { child: self.0.clone(), padding }.pack(); + + // Layout the child. + let expand = size.map(Length::is_finite); + let regions = Regions::repeat(size, size, expand); let mut frames: Vec<_> = padded.layout(ctx, ®ions).into_iter().map(|c| c.item).collect(); // Add background fill if requested. - if let Some(fill) = style.fill { + if let Some(fill) = styles.get(Self::FILL) { for frame in &mut frames { let shape = Shape::filled(Geometry::Rect(frame.size), fill); Rc::make_mut(frame).prepend(Point::zero(), Element::Shape(shape)); @@ -106,3 +119,256 @@ impl PageNode { frames } } + +/// Specification of a paper. +#[derive(Debug, Copy, Clone)] +pub struct Paper { + /// The broad class this paper belongs to. + class: PaperClass, + /// The width of the paper in millimeters. + width: f64, + /// The height of the paper in millimeters. + height: f64, +} + +impl Paper { + /// The class of the paper. + pub fn class(self) -> PaperClass { + self.class + } + + /// The width of the paper. + pub fn width(self) -> Length { + Length::mm(self.width) + } + + /// The height of the paper. + pub fn height(self) -> Length { + Length::mm(self.height) + } +} + +impl Default for Paper { + fn default() -> Self { + Paper::A4 + } +} + +/// Defines default margins for a class of related papers. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum PaperClass { + Custom, + Base, + US, + Newspaper, + Book, +} + +impl PaperClass { + /// The default margins for this page class. + fn default_margins(self) -> Sides<Linear> { + let f = |r| Relative::new(r).into(); + let s = |l, t, r, b| Sides::new(f(l), f(t), f(r), f(b)); + match self { + Self::Custom => s(0.1190, 0.0842, 0.1190, 0.0842), + Self::Base => s(0.1190, 0.0842, 0.1190, 0.0842), + Self::US => s(0.1760, 0.1092, 0.1760, 0.0910), + Self::Newspaper => s(0.0455, 0.0587, 0.0455, 0.0294), + Self::Book => s(0.1200, 0.0852, 0.1500, 0.0965), + } + } +} + +/// Defines paper constants and a paper parsing implementation. +macro_rules! papers { + ($(($var:ident: $class:ident, $width:expr, $height: expr, $($pats:tt)*))*) => { + /// Predefined papers. + /// + /// Each paper is parsable from its name in kebab-case. + impl Paper { + $(pub const $var: Self = Self { + class: PaperClass::$class, + width: $width, + height: $height, + };)* + } + + impl FromStr for Paper { + type Err = ParsePaperError; + + fn from_str(name: &str) -> Result<Self, Self::Err> { + match name.to_lowercase().as_str() { + $($($pats)* => Ok(Self::$var),)* + _ => Err(ParsePaperError), + } + } + } + + /// The error when parsing a [`Paper`] from a string fails. + #[derive(Debug, Copy, Clone, Eq, PartialEq)] + pub struct ParsePaperError; + + impl Display for ParsePaperError { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad("invalid paper name") + } + } + + impl std::error::Error for ParsePaperError {} + }; +} + +// All paper sizes in mm. +// +// Resources: +// - https://papersizes.io/ +// - https://en.wikipedia.org/wiki/Paper_size +// - https://www.theedkins.co.uk/jo/units/oldunits/print.htm +// - https://vintagepaper.co/blogs/news/traditional-paper-sizes +papers! { + // ---------------------------------------------------------------------- // + // ISO 216 A Series + (A0: Base, 841.0, 1189.0, "a0") + (A1: Base, 594.0, 841.0, "a1") + (A2: Base, 420.0, 594.0, "a2") + (A3: Base, 297.0, 420.0, "a3") + (A4: Base, 210.0, 297.0, "a4") + (A5: Base, 148.0, 210.0, "a5") + (A6: Book, 105.0, 148.0, "a6") + (A7: Base, 74.0, 105.0, "a7") + (A8: Base, 52.0, 74.0, "a8") + (A9: Base, 37.0, 52.0, "a9") + (A10: Base, 26.0, 37.0, "a10") + (A11: Base, 18.0, 26.0, "a11") + + // ISO 216 B Series + (ISO_B1: Base, 707.0, 1000.0, "iso-b1") + (ISO_B2: Base, 500.0, 707.0, "iso-b2") + (ISO_B3: Base, 353.0, 500.0, "iso-b3") + (ISO_B4: Base, 250.0, 353.0, "iso-b4") + (ISO_B5: Book, 176.0, 250.0, "iso-b5") + (ISO_B6: Book, 125.0, 176.0, "iso-b6") + (ISO_B7: Base, 88.0, 125.0, "iso-b7") + (ISO_B8: Base, 62.0, 88.0, "iso-b8") + + // ISO 216 C Series + (ISO_C3: Base, 324.0, 458.0, "iso-c3") + (ISO_C4: Base, 229.0, 324.0, "iso-c4") + (ISO_C5: Base, 162.0, 229.0, "iso-c5") + (ISO_C6: Base, 114.0, 162.0, "iso-c6") + (ISO_C7: Base, 81.0, 114.0, "iso-c7") + (ISO_C8: Base, 57.0, 81.0, "iso-c8") + + // DIN D Series (extension to ISO) + (DIN_D3: Base, 272.0, 385.0, "din-d3") + (DIN_D4: Base, 192.0, 272.0, "din-d4") + (DIN_D5: Base, 136.0, 192.0, "din-d5") + (DIN_D6: Base, 96.0, 136.0, "din-d6") + (DIN_D7: Base, 68.0, 96.0, "din-d7") + (DIN_D8: Base, 48.0, 68.0, "din-d8") + + // SIS (used in academia) + (SIS_G5: Base, 169.0, 239.0, "sis-g5") + (SIS_E5: Base, 115.0, 220.0, "sis-e5") + + // ANSI Extensions + (ANSI_A: Base, 216.0, 279.0, "ansi-a") + (ANSI_B: Base, 279.0, 432.0, "ansi-b") + (ANSI_C: Base, 432.0, 559.0, "ansi-c") + (ANSI_D: Base, 559.0, 864.0, "ansi-d") + (ANSI_E: Base, 864.0, 1118.0, "ansi-e") + + // ANSI Architectural Paper + (ARCH_A: Base, 229.0, 305.0, "arch-a") + (ARCH_B: Base, 305.0, 457.0, "arch-b") + (ARCH_C: Base, 457.0, 610.0, "arch-c") + (ARCH_D: Base, 610.0, 914.0, "arch-d") + (ARCH_E1: Base, 762.0, 1067.0, "arch-e1") + (ARCH_E: Base, 914.0, 1219.0, "arch-e") + + // JIS B Series + (JIS_B0: Base, 1030.0, 1456.0, "jis-b0") + (JIS_B1: Base, 728.0, 1030.0, "jis-b1") + (JIS_B2: Base, 515.0, 728.0, "jis-b2") + (JIS_B3: Base, 364.0, 515.0, "jis-b3") + (JIS_B4: Base, 257.0, 364.0, "jis-b4") + (JIS_B5: Base, 182.0, 257.0, "jis-b5") + (JIS_B6: Base, 128.0, 182.0, "jis-b6") + (JIS_B7: Base, 91.0, 128.0, "jis-b7") + (JIS_B8: Base, 64.0, 91.0, "jis-b8") + (JIS_B9: Base, 45.0, 64.0, "jis-b9") + (JIS_B10: Base, 32.0, 45.0, "jis-b10") + (JIS_B11: Base, 22.0, 32.0, "jis-b11") + + // SAC D Series + (SAC_D0: Base, 764.0, 1064.0, "sac-d0") + (SAC_D1: Base, 532.0, 760.0, "sac-d1") + (SAC_D2: Base, 380.0, 528.0, "sac-d2") + (SAC_D3: Base, 264.0, 376.0, "sac-d3") + (SAC_D4: Base, 188.0, 260.0, "sac-d4") + (SAC_D5: Base, 130.0, 184.0, "sac-d5") + (SAC_D6: Base, 92.0, 126.0, "sac-d6") + + // ISO 7810 ID + (ISO_ID_1: Base, 85.6, 53.98, "iso-id-1") + (ISO_ID_2: Base, 74.0, 105.0, "iso-id-2") + (ISO_ID_3: Base, 88.0, 125.0, "iso-id-3") + + // ---------------------------------------------------------------------- // + // Asia + (ASIA_F4: Base, 210.0, 330.0, "asia-f4") + + // Japan + (JP_SHIROKU_BAN_4: Base, 264.0, 379.0, "jp-shiroku-ban-4") + (JP_SHIROKU_BAN_5: Base, 189.0, 262.0, "jp-shiroku-ban-5") + (JP_SHIROKU_BAN_6: Base, 127.0, 188.0, "jp-shiroku-ban-6") + (JP_KIKU_4: Base, 227.0, 306.0, "jp-kiku-4") + (JP_KIKU_5: Base, 151.0, 227.0, "jp-kiku-5") + (JP_BUSINESS_CARD: Base, 91.0, 55.0, "jp-business-card") + + // China + (CN_BUSINESS_CARD: Base, 90.0, 54.0, "cn-business-card") + + // Europe + (EU_BUSINESS_CARD: Base, 85.0, 55.0, "eu-business-card") + + // French Traditional (AFNOR) + (FR_TELLIERE: Base, 340.0, 440.0, "fr-tellière") + (FR_COURONNE_ECRITURE: Base, 360.0, 460.0, "fr-couronne-écriture") + (FR_COURONNE_EDITION: Base, 370.0, 470.0, "fr-couronne-édition") + (FR_RAISIN: Base, 500.0, 650.0, "fr-raisin") + (FR_CARRE: Base, 450.0, 560.0, "fr-carré") + (FR_JESUS: Base, 560.0, 760.0, "fr-jésus") + + // United Kingdom Imperial + (UK_BRIEF: Base, 406.4, 342.9, "uk-brief") + (UK_DRAFT: Base, 254.0, 406.4, "uk-draft") + (UK_FOOLSCAP: Base, 203.2, 330.2, "uk-foolscap") + (UK_QUARTO: Base, 203.2, 254.0, "uk-quarto") + (UK_CROWN: Base, 508.0, 381.0, "uk-crown") + (UK_BOOK_A: Book, 111.0, 178.0, "uk-book-a") + (UK_BOOK_B: Book, 129.0, 198.0, "uk-book-b") + + // Unites States + (US_LETTER: US, 215.9, 279.4, "us-letter") + (US_LEGAL: US, 215.9, 355.6, "us-legal") + (US_TABLOID: US, 279.4, 431.8, "us-tabloid") + (US_EXECUTIVE: US, 184.15, 266.7, "us-executive") + (US_FOOLSCAP_FOLIO: US, 215.9, 342.9, "us-foolscap-folio") + (US_STATEMENT: US, 139.7, 215.9, "us-statement") + (US_LEDGER: US, 431.8, 279.4, "us-ledger") + (US_OFICIO: US, 215.9, 340.36, "us-oficio") + (US_GOV_LETTER: US, 203.2, 266.7, "us-gov-letter") + (US_GOV_LEGAL: US, 215.9, 330.2, "us-gov-legal") + (US_BUSINESS_CARD: Base, 88.9, 50.8, "us-business-card") + (US_DIGEST: Book, 139.7, 215.9, "us-digest") + (US_TRADE: Book, 152.4, 228.6, "us-trade") + + // ---------------------------------------------------------------------- // + // Other + (NEWSPAPER_COMPACT: Newspaper, 280.0, 430.0, "newspaper-compact") + (NEWSPAPER_BERLINER: Newspaper, 315.0, 470.0, "newspaper-berliner") + (NEWSPAPER_BROADSHEET: Newspaper, 381.0, 578.0, "newspaper-broadsheet") + (PRESENTATION_16_9: Base, 297.0, 167.0625, "presentation-16-9") + (PRESENTATION_4_3: Base, 280.0, 210.0, "presentation-4-3") +} diff --git a/src/library/par.rs b/src/library/par.rs index 6da1ee95..21760225 100644 --- a/src/library/par.rs +++ b/src/library/par.rs @@ -6,8 +6,7 @@ use unicode_bidi::{BidiInfo, Level}; use xi_unicode::LineBreakIterator; use super::prelude::*; -use super::{shape, Decoration, ShapedText, Spacing}; -use crate::style::TextStyle; +use super::{shape, Decoration, ShapedText, Spacing, TextNode}; use crate::util::{EcoString, RangeExt, RcExt, SliceExt}; /// `par`: Configure paragraphs. @@ -38,24 +37,14 @@ pub fn par(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> { align = Some(v); } - let par = ctx.style.par_mut(); - - if let Some(dir) = dir { - par.dir = dir; - par.align = if dir == Dir::LTR { Align::Left } else { Align::Right }; + if let (Some(dir), None) = (dir, align) { + align = Some(if dir == Dir::LTR { Align::Left } else { Align::Right }); } - if let Some(align) = align { - par.align = align; - } - - if let Some(leading) = leading { - par.leading = leading; - } - - if let Some(spacing) = spacing { - par.spacing = spacing; - } + set!(ctx, ParNode::DIR => dir); + set!(ctx, ParNode::ALIGN => align); + set!(ctx, ParNode::LEADING => leading); + set!(ctx, ParNode::SPACING => spacing); Ok(Value::None) } @@ -64,24 +53,39 @@ pub fn par(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> { #[derive(Debug, Hash)] pub struct ParNode(pub Vec<ParChild>); +properties! { + ParNode, + + /// The direction for text and inline objects. + DIR: Dir = Dir::LTR, + /// How to align text and inline objects in their line. + ALIGN: Align = Align::Left, + // TODO(set): Make relative to font size. + /// The spacing between lines (dependent on scaled font size). + LEADING: Length = Length::pt(6.5), + /// The spacing between paragraphs (dependent on scaled font size). + SPACING: Length = Length::pt(12.0), +} + impl Layout for ParNode { fn layout( &self, ctx: &mut LayoutContext, regions: &Regions, ) -> Vec<Constrained<Rc<Frame>>> { + // TODO(set): Take styles as parameter. + let styles = Styles::new(); + // Collect all text into one string used for BiDi analysis. let text = self.collect_text(); // Find out the BiDi embedding levels. - // TODO(set): Get dir from styles. - let bidi = BidiInfo::new(&text, Level::from_dir(Dir::LTR)); + let default_level = Level::from_dir(styles.get(Self::DIR)); + let bidi = BidiInfo::new(&text, default_level); // Prepare paragraph layout by building a representation on which we can // do line breaking without layouting each and every line from scratch. - // TODO(set): Get text style from styles. - let style = crate::style::TextStyle::default(); - let layouter = ParLayouter::new(self, ctx, regions, bidi, &style); + let layouter = ParLayouter::new(self, ctx, regions, bidi, &styles); // Find suitable linebreaks. layouter.layout(ctx, regions.clone()) @@ -115,7 +119,7 @@ impl ParNode { fn strings(&self) -> impl Iterator<Item = &str> { self.0.iter().map(|child| match child { ParChild::Spacing(_) => " ", - ParChild::Text(ref piece, ..) => piece, + ParChild::Text(ref piece, ..) => &piece.0, ParChild::Node(..) => "\u{FFFC}", ParChild::Decorate(_) | ParChild::Undecorate => "", }) @@ -128,7 +132,8 @@ pub enum ParChild { /// Spacing between other nodes. Spacing(Spacing), /// A run of text and how to align it in its line. - Text(EcoString), + // TODO(set): A single text run may also have its own style. + Text(TextNode), /// Any child node and how to align it in its line. Node(PackedNode), /// A decoration that applies until a matching `Undecorate`. @@ -188,7 +193,7 @@ impl<'a> ParLayouter<'a> { ctx: &mut LayoutContext, regions: &Regions, bidi: BidiInfo<'a>, - style: &'a TextStyle, + styles: &'a Styles, ) -> Self { let mut items = vec![]; let mut ranges = vec![]; @@ -215,7 +220,7 @@ impl<'a> ParLayouter<'a> { cursor += group.len(); let subrange = start .. cursor; let text = &bidi.text[subrange.clone()]; - let shaped = shape(ctx, text, style, level.dir()); + let shaped = shape(ctx, text, styles, level.dir()); items.push(ParItem::Text(shaped)); ranges.push(subrange); } @@ -243,9 +248,8 @@ impl<'a> ParLayouter<'a> { } Self { - // TODO(set): Get alignment and leading from styles. - align: Align::Left, - leading: Length::pt(6.0), + align: styles.get(ParNode::ALIGN), + leading: styles.get(ParNode::LEADING), bidi, items, ranges, @@ -540,7 +544,7 @@ impl<'a> LineLayout<'a> { // Compute the reordered ranges in visual order (left to right). self.par.bidi.visual_runs(para, self.line.clone()) } else { - <_>::default() + (vec![], vec![]) }; runs.into_iter() diff --git a/src/library/placed.rs b/src/library/placed.rs index ce76a969..3e2e1b26 100644 --- a/src/library/placed.rs +++ b/src/library/placed.rs @@ -7,24 +7,21 @@ pub fn place(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { let tx = args.named("dx")?.unwrap_or_default(); let ty = args.named("dy")?.unwrap_or_default(); let body: Node = args.expect("body")?; - Ok(Value::block(PlacedNode { - child: body.into_block().moved(Point::new(tx, ty)).aligned(aligns), - })) + Ok(Value::block(PlacedNode( + body.into_block().moved(Point::new(tx, ty)).aligned(aligns), + ))) } /// A node that places its child absolutely. #[derive(Debug, Hash)] -pub struct PlacedNode { - /// The node to be placed. - pub child: PackedNode, -} +pub struct PlacedNode(pub PackedNode); impl PlacedNode { /// Whether this node wants to be placed relative to its its parent's base /// origin. instead of relative to the parent's current flow/cursor /// position. pub fn out_of_flow(&self) -> bool { - self.child + self.0 .downcast::<AlignNode>() .map_or(false, |node| node.aligns.y.is_some()) } @@ -46,7 +43,7 @@ impl Layout for PlacedNode { Regions::one(regions.base, regions.base, expand) }; - let mut frames = self.child.layout(ctx, &pod); + let mut frames = self.0.layout(ctx, &pod); let Constrained { item: frame, cts } = &mut frames[0]; // If expansion is off, zero all sizes so that we don't take up any diff --git a/src/library/text.rs b/src/library/text.rs index d96f7666..01218087 100644 --- a/src/library/text.rs +++ b/src/library/text.rs @@ -1,5 +1,6 @@ use std::borrow::Cow; use std::convert::TryInto; +use std::fmt::{self, Debug, Formatter}; use std::ops::Range; use rustybuzz::{Feature, UnicodeBuffer}; @@ -11,219 +12,326 @@ use crate::font::{ VerticalFontMetric, }; use crate::geom::{Dir, Em, Length, Point, Size}; -use crate::style::{ - FontFamily, FontFeatures, NumberPosition, NumberType, NumberWidth, StylisticSet, - TextStyle, -}; use crate::util::{EcoString, SliceExt}; /// `font`: Configure the font. pub fn font(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> { - castable! { - Vec<FontFamily>, - Expected: "string, generic family or array thereof", - Value::Str(string) => vec![FontFamily::Named(string.to_lowercase())], - Value::Array(values) => { - values.into_iter().filter_map(|v| v.cast().ok()).collect() - }, - @family: FontFamily => vec![family.clone()], - } + let list = args.named("family")?.or_else(|| { + let families: Vec<_> = args.all().collect(); + (!families.is_empty()).then(|| families) + }); - castable! { - Vec<EcoString>, - Expected: "string or array of strings", - Value::Str(string) => vec![string.to_lowercase()], - Value::Array(values) => values - .into_iter() - .filter_map(|v| v.cast().ok()) - .map(|string: EcoString| string.to_lowercase()) - .collect(), - } + set!(ctx, TextNode::FAMILY_LIST => list); + set!(ctx, TextNode::SERIF_LIST => args.named("serif")?); + set!(ctx, TextNode::SANS_SERIF_LIST => args.named("sans-serif")?); + set!(ctx, TextNode::MONOSPACE_LIST => args.named("monospace")?); + set!(ctx, TextNode::FALLBACK => args.named("fallback")?); + set!(ctx, TextNode::STYLE => args.named("style")?); + set!(ctx, TextNode::WEIGHT => args.named("weight")?); + set!(ctx, TextNode::STRETCH => args.named("stretch")?); + set!(ctx, TextNode::FILL => args.named("fill")?.or_else(|| args.find())); + set!(ctx, TextNode::SIZE => args.named("size")?.or_else(|| args.find())); + set!(ctx, TextNode::TRACKING => args.named("tracking")?.map(Em::new)); + set!(ctx, TextNode::TOP_EDGE => args.named("top-edge")?); + set!(ctx, TextNode::BOTTOM_EDGE => args.named("bottom-edge")?); + set!(ctx, TextNode::KERNING => args.named("kerning")?); + set!(ctx, TextNode::SMALLCAPS => args.named("smallcaps")?); + set!(ctx, TextNode::ALTERNATES => args.named("alternates")?); + set!(ctx, TextNode::STYLISTIC_SET => args.named("stylistic-set")?); + set!(ctx, TextNode::LIGATURES => args.named("ligatures")?); + set!(ctx, TextNode::DISCRETIONARY_LIGATURES => args.named("discretionary-ligatures")?); + set!(ctx, TextNode::HISTORICAL_LIGATURES => args.named("historical-ligatures")?); + set!(ctx, TextNode::NUMBER_TYPE => args.named("number-type")?); + set!(ctx, TextNode::NUMBER_WIDTH => args.named("number-width")?); + set!(ctx, TextNode::NUMBER_POSITION => args.named("number-position")?); + set!(ctx, TextNode::SLASHED_ZERO => args.named("slashed-zero")?); + set!(ctx, TextNode::FRACTIONS => args.named("fractions")?); + set!(ctx, TextNode::FEATURES => args.named("features")?); - castable! { - FontStyle, - Expected: "string", - Value::Str(string) => match string.as_str() { - "normal" => Self::Normal, - "italic" => Self::Italic, - "oblique" => Self::Oblique, - _ => Err(r#"expected "normal", "italic" or "oblique""#)?, - }, - } + Ok(Value::None) +} - castable! { - FontWeight, - Expected: "integer or string", - Value::Int(v) => v.try_into().map_or(Self::BLACK, Self::from_number), - Value::Str(string) => match string.as_str() { - "thin" => Self::THIN, - "extralight" => Self::EXTRALIGHT, - "light" => Self::LIGHT, - "regular" => Self::REGULAR, - "medium" => Self::MEDIUM, - "semibold" => Self::SEMIBOLD, - "bold" => Self::BOLD, - "extrabold" => Self::EXTRABOLD, - "black" => Self::BLACK, - _ => Err("unknown font weight")?, - }, - } +/// A single run of text with the same style. +#[derive(Debug, Hash)] +pub struct TextNode(pub EcoString); + +properties! { + TextNode, + + /// A prioritized sequence of font families. + FAMILY_LIST: Vec<FontFamily> = vec![FontFamily::SansSerif], + /// The serif font family/families. + SERIF_LIST: Vec<String> = vec!["ibm plex serif".into()], + /// The sans-serif font family/families. + SANS_SERIF_LIST: Vec<String> = vec!["ibm plex sans".into()], + /// The monospace font family/families. + MONOSPACE_LIST: Vec<String> = vec!["ibm plex mono".into()], + /// Whether to allow font fallback when the primary font list contains no + /// match. + FALLBACK: bool = true, + + /// How the font is styled. + STYLE: FontStyle = FontStyle::Normal, + /// The boldness / thickness of the font's glyphs. + WEIGHT: FontWeight = FontWeight::REGULAR, + /// The width of the glyphs. + STRETCH: FontStretch = FontStretch::NORMAL, + /// Whether the font weight should be increased by 300. + STRONG: bool = false, + /// Whether the the font style should be inverted. + EMPH: bool = false, + /// Whether a monospace font should be preferred. + MONOSPACE: bool = false, + /// The glyph fill color. + FILL: Paint = RgbaColor::BLACK.into(), + + /// The size of the glyphs. + // TODO(set): Resolve relative to outer font size. + SIZE: Length = Length::pt(11.0), + /// The amount of space that should be added between characters. + TRACKING: Em = Em::zero(), + /// The top end of the text bounding box. + TOP_EDGE: VerticalFontMetric = VerticalFontMetric::CapHeight, + /// The bottom end of the text bounding box. + BOTTOM_EDGE: VerticalFontMetric = VerticalFontMetric::Baseline, + + /// Whether to apply kerning ("kern"). + KERNING: bool = true, + /// Whether small capital glyphs should be used. ("smcp") + SMALLCAPS: bool = false, + /// Whether to apply stylistic alternates. ("salt") + ALTERNATES: bool = false, + /// Which stylistic set to apply. ("ss01" - "ss20") + STYLISTIC_SET: Option<StylisticSet> = None, + /// Whether standard ligatures are active. ("liga", "clig") + LIGATURES: bool = true, + /// Whether ligatures that should be used sparingly are active. ("dlig") + DISCRETIONARY_LIGATURES: bool = false, + /// Whether historical ligatures are active. ("hlig") + HISTORICAL_LIGATURES: bool = false, + /// Which kind of numbers / figures to select. + NUMBER_TYPE: Smart<NumberType> = Smart::Auto, + /// The width of numbers / figures. + NUMBER_WIDTH: Smart<NumberWidth> = Smart::Auto, + /// How to position numbers. + NUMBER_POSITION: NumberPosition = NumberPosition::Normal, + /// Whether to have a slash through the zero glyph. ("zero") + SLASHED_ZERO: bool = false, + /// Whether to convert fractions. ("frac") + FRACTIONS: bool = false, + /// Raw OpenType features to apply. + FEATURES: Vec<(Tag, u32)> = vec![], +} - castable! { - FontStretch, - Expected: "relative", - Value::Relative(v) => Self::from_ratio(v.get() as f32), - } +/// A generic or named font family. +#[derive(Clone, Eq, PartialEq, Hash)] +pub enum FontFamily { + /// A family that has "serifs", small strokes attached to letters. + Serif, + /// A family in which glyphs do not have "serifs", small attached strokes. + SansSerif, + /// A family in which (almost) all glyphs are of equal width. + Monospace, + /// A specific family with a name. + Named(String), +} - castable! { - VerticalFontMetric, - Expected: "linear or string", - Value::Length(v) => Self::Linear(v.into()), - Value::Relative(v) => Self::Linear(v.into()), - Value::Linear(v) => Self::Linear(v), - Value::Str(string) => match string.as_str() { - "ascender" => Self::Ascender, - "cap-height" => Self::CapHeight, - "x-height" => Self::XHeight, - "baseline" => Self::Baseline, - "descender" => Self::Descender, - _ => Err("unknown font metric")?, - }, +impl Debug for FontFamily { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad(match self { + Self::Serif => "serif", + Self::SansSerif => "sans-serif", + Self::Monospace => "monospace", + Self::Named(s) => s, + }) } +} - castable! { - StylisticSet, - Expected: "integer", - Value::Int(v) => match v { - 1 ..= 20 => Self::new(v as u8), - _ => Err("must be between 1 and 20")?, - }, - } +dynamic! { + FontFamily: "font family", + Value::Str(string) => Self::Named(string.to_lowercase().into()), +} - castable! { - NumberType, - Expected: "string", - Value::Str(string) => match string.as_str() { - "lining" => Self::Lining, - "old-style" => Self::OldStyle, - _ => Err(r#"expected "lining" or "old-style""#)?, - }, - } +castable! { + Vec<FontFamily>, + Expected: "string, generic family or array thereof", + Value::Str(string) => vec![FontFamily::Named(string.to_lowercase().into())], + Value::Array(values) => { + values.into_iter().filter_map(|v| v.cast().ok()).collect() + }, + @family: FontFamily => vec![family.clone()], +} - castable! { - NumberWidth, - Expected: "string", - Value::Str(string) => match string.as_str() { - "proportional" => Self::Proportional, - "tabular" => Self::Tabular, - _ => Err(r#"expected "proportional" or "tabular""#)?, - }, - } +castable! { + Vec<String>, + Expected: "string or array of strings", + Value::Str(string) => vec![string.to_lowercase().into()], + Value::Array(values) => values + .into_iter() + .filter_map(|v| v.cast().ok()) + .map(|string: EcoString| string.to_lowercase().into()) + .collect(), +} - castable! { - NumberPosition, - Expected: "string", - Value::Str(string) => match string.as_str() { - "normal" => Self::Normal, - "subscript" => Self::Subscript, - "superscript" => Self::Superscript, - _ => Err(r#"expected "normal", "subscript" or "superscript""#)?, - }, +castable! { + FontStyle, + Expected: "string", + Value::Str(string) => match string.as_str() { + "normal" => Self::Normal, + "italic" => Self::Italic, + "oblique" => Self::Oblique, + _ => Err(r#"expected "normal", "italic" or "oblique""#)?, + }, +} + +castable! { + FontWeight, + Expected: "integer or string", + Value::Int(v) => v.try_into().map_or(Self::BLACK, Self::from_number), + Value::Str(string) => match string.as_str() { + "thin" => Self::THIN, + "extralight" => Self::EXTRALIGHT, + "light" => Self::LIGHT, + "regular" => Self::REGULAR, + "medium" => Self::MEDIUM, + "semibold" => Self::SEMIBOLD, + "bold" => Self::BOLD, + "extrabold" => Self::EXTRABOLD, + "black" => Self::BLACK, + _ => Err("unknown font weight")?, + }, +} + +castable! { + FontStretch, + Expected: "relative", + Value::Relative(v) => Self::from_ratio(v.get() as f32), +} + +castable! { + VerticalFontMetric, + Expected: "linear or string", + Value::Length(v) => Self::Linear(v.into()), + Value::Relative(v) => Self::Linear(v.into()), + Value::Linear(v) => Self::Linear(v), + Value::Str(string) => match string.as_str() { + "ascender" => Self::Ascender, + "cap-height" => Self::CapHeight, + "x-height" => Self::XHeight, + "baseline" => Self::Baseline, + "descender" => Self::Descender, + _ => Err("unknown font metric")?, + }, +} + +/// A stylistic set in a font face. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct StylisticSet(u8); + +impl StylisticSet { + /// Creates a new set, clamping to 1-20. + pub fn new(index: u8) -> Self { + Self(index.clamp(1, 20)) } - castable! { - Vec<(Tag, u32)>, - Expected: "array of strings or dictionary mapping tags to integers", - Value::Array(values) => values - .into_iter() - .filter_map(|v| v.cast().ok()) - .map(|string: EcoString| (Tag::from_bytes_lossy(string.as_bytes()), 1)) - .collect(), - Value::Dict(values) => values - .into_iter() - .filter_map(|(k, v)| { - let tag = Tag::from_bytes_lossy(k.as_bytes()); - let num = v.cast::<i64>().ok()?.try_into().ok()?; - Some((tag, num)) - }) - .collect(), + /// Get the value, guaranteed to be 1-20. + pub fn get(self) -> u8 { + self.0 } +} - let list = args.named("family")?.or_else(|| { - let families: Vec<_> = args.all().collect(); - (!families.is_empty()).then(|| families) - }); +castable! { + StylisticSet, + Expected: "integer", + Value::Int(v) => match v { + 1 ..= 20 => Self::new(v as u8), + _ => Err("must be between 1 and 20")?, + }, +} - let serif = args.named("serif")?; - let sans_serif = args.named("sans-serif")?; - let monospace = args.named("monospace")?; - let fallback = args.named("fallback")?; - let style = args.named("style")?; - let weight = args.named("weight")?; - let stretch = args.named("stretch")?; - let size = args.named::<Linear>("size")?.or_else(|| args.find()); - let tracking = args.named("tracking")?.map(Em::new); - let top_edge = args.named("top-edge")?; - let bottom_edge = args.named("bottom-edge")?; - let fill = args.named("fill")?.or_else(|| args.find()); - let kerning = args.named("kerning")?; - let smallcaps = args.named("smallcaps")?; - let alternates = args.named("alternates")?; - let stylistic_set = args.named("stylistic-set")?; - let ligatures = args.named("ligatures")?; - let discretionary_ligatures = args.named("discretionary-ligatures")?; - let historical_ligatures = args.named("historical-ligatures")?; - let number_type = args.named("number-type")?; - let number_width = args.named("number-width")?; - let number_position = args.named("number-position")?; - let slashed_zero = args.named("slashed-zero")?; - let fractions = args.named("fractions")?; - let features = args.named("features")?; - - macro_rules! set { - ($target:expr => $source:expr) => { - if let Some(v) = $source { - $target = v; - } - }; - } +/// Which kind of numbers / figures to select. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum NumberType { + /// Numbers that fit well with capital text. ("lnum") + Lining, + /// Numbers that fit well into flow of upper- and lowercase text. ("onum") + OldStyle, +} - let text = ctx.style.text_mut(); - set!(text.families_mut().list => list.clone()); - set!(text.families_mut().serif => serif.clone()); - set!(text.families_mut().sans_serif => sans_serif.clone()); - set!(text.families_mut().monospace => monospace.clone()); - set!(text.fallback => fallback); - set!(text.variant.style => style); - set!(text.variant.weight => weight); - set!(text.variant.stretch => stretch); - set!(text.size => size.map(|v| v.resolve(text.size))); - set!(text.tracking => tracking); - set!(text.top_edge => top_edge); - set!(text.bottom_edge => bottom_edge); - set!(text.fill => fill); - set!(text.features_mut().kerning => kerning); - set!(text.features_mut().smallcaps => smallcaps); - set!(text.features_mut().alternates => alternates); - set!(text.features_mut().stylistic_set => stylistic_set); - set!(text.features_mut().ligatures.standard => ligatures); - set!(text.features_mut().ligatures.discretionary => discretionary_ligatures); - set!(text.features_mut().ligatures.historical => historical_ligatures); - set!(text.features_mut().numbers.type_ => number_type); - set!(text.features_mut().numbers.width => number_width); - set!(text.features_mut().numbers.position => number_position); - set!(text.features_mut().numbers.slashed_zero => slashed_zero); - set!(text.features_mut().numbers.fractions => fractions); - set!(text.features_mut().raw => features.clone()); +castable! { + NumberType, + Expected: "string", + Value::Str(string) => match string.as_str() { + "lining" => Self::Lining, + "old-style" => Self::OldStyle, + _ => Err(r#"expected "lining" or "old-style""#)?, + }, +} - Ok(Value::None) +/// The width of numbers / figures. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum NumberWidth { + /// Number widths are glyph specific. ("pnum") + Proportional, + /// All numbers are of equal width / monospaced. ("tnum") + Tabular, +} + +castable! { + NumberWidth, + Expected: "string", + Value::Str(string) => match string.as_str() { + "proportional" => Self::Proportional, + "tabular" => Self::Tabular, + _ => Err(r#"expected "proportional" or "tabular""#)?, + }, +} + +/// How to position numbers. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum NumberPosition { + /// Numbers are positioned on the same baseline as text. + Normal, + /// Numbers are smaller and placed at the bottom. ("subs") + Subscript, + /// Numbers are smaller and placed at the top. ("sups") + Superscript, +} + +castable! { + NumberPosition, + Expected: "string", + Value::Str(string) => match string.as_str() { + "normal" => Self::Normal, + "subscript" => Self::Subscript, + "superscript" => Self::Superscript, + _ => Err(r#"expected "normal", "subscript" or "superscript""#)?, + }, +} + +castable! { + Vec<(Tag, u32)>, + Expected: "array of strings or dictionary mapping tags to integers", + Value::Array(values) => values + .into_iter() + .filter_map(|v| v.cast().ok()) + .map(|string: EcoString| (Tag::from_bytes_lossy(string.as_bytes()), 1)) + .collect(), + Value::Dict(values) => values + .into_iter() + .filter_map(|(k, v)| { + let tag = Tag::from_bytes_lossy(k.as_bytes()); + let num = v.cast::<i64>().ok()?.try_into().ok()?; + Some((tag, num)) + }) + .collect(), } /// Shape text into [`ShapedText`]. pub fn shape<'a>( ctx: &mut LayoutContext, text: &'a str, - style: &'a TextStyle, + styles: &'a Styles, dir: Dir, ) -> ShapedText<'a> { let mut glyphs = vec![]; @@ -233,188 +341,27 @@ pub fn shape<'a>( &mut glyphs, 0, text, - style.variant(), - style.families(), + variant(styles), + families(styles), None, dir, - &tags(&style.features), + &tags(styles), ); } - track(&mut glyphs, style.tracking); + track(&mut glyphs, styles.get(TextNode::TRACKING)); + let (size, baseline) = measure(ctx, &glyphs, styles); - let (size, baseline) = measure(ctx, &glyphs, style); ShapedText { text, dir, - style, + styles, 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 offset = Length::zero(); - let mut frame = Frame::new(self.size); - frame.baseline = Some(self.baseline); - - 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>( fonts: &mut FontStore, @@ -562,23 +509,27 @@ fn track(glyphs: &mut [ShapedGlyph], tracking: Em) { fn measure( ctx: &mut LayoutContext, glyphs: &[ShapedGlyph], - style: &TextStyle, + styles: &Styles, ) -> (Size, Length) { let mut width = Length::zero(); let mut top = Length::zero(); let mut bottom = Length::zero(); + let size = styles.get(TextNode::SIZE); + let top_edge = styles.get(TextNode::TOP_EDGE); + let bottom_edge = styles.get(TextNode::BOTTOM_EDGE); + // 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)); + top.set_max(face.vertical_metric(top_edge, size)); + bottom.set_max(-face.vertical_metric(bottom_edge, 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) { + for family in families(styles) { + if let Some(face_id) = ctx.fonts.select(family, variant(styles)) { expand(ctx.fonts.get(face_id)); break; } @@ -589,7 +540,7 @@ fn measure( expand(face); for glyph in group { - width += glyph.x_advance.to_length(style.size); + width += glyph.x_advance.to_length(size); } } } @@ -597,75 +548,286 @@ fn measure( (Size::new(width, top + bottom), top) } +/// Resolved the font variant with `STRONG` and `EMPH` factored in. +fn variant(styles: &Styles) -> FontVariant { + let mut variant = FontVariant::new( + styles.get(TextNode::STYLE), + styles.get(TextNode::WEIGHT), + styles.get(TextNode::STRETCH), + ); + + if styles.get(TextNode::STRONG) { + variant.weight = variant.weight.thicken(300); + } + + if styles.get(TextNode::EMPH) { + variant.style = match variant.style { + FontStyle::Normal => FontStyle::Italic, + FontStyle::Italic => FontStyle::Normal, + FontStyle::Oblique => FontStyle::Normal, + } + } + + variant +} + +/// Resolve a prioritized iterator over the font families. +fn families(styles: &Styles) -> impl Iterator<Item = &str> + Clone { + let head = if styles.get(TextNode::MONOSPACE) { + styles.get_ref(TextNode::MONOSPACE_LIST).as_slice() + } else { + &[] + }; + + let core = styles.get_ref(TextNode::FAMILY_LIST).iter().flat_map(move |family| { + match family { + FontFamily::Named(name) => std::slice::from_ref(name), + FontFamily::Serif => styles.get_ref(TextNode::SERIF_LIST), + FontFamily::SansSerif => styles.get_ref(TextNode::SANS_SERIF_LIST), + FontFamily::Monospace => styles.get_ref(TextNode::MONOSPACE_LIST), + } + }); + + let tail: &[&str] = if styles.get(TextNode::FALLBACK) { + &["ibm plex sans", "latin modern math", "twitter color emoji"] + } else { + &[] + }; + + head.iter() + .chain(core) + .map(String::as_str) + .chain(tail.iter().copied()) +} + /// Collect the tags of the OpenType features to apply. -fn tags(features: &FontFeatures) -> Vec<Feature> { +fn tags(styles: &Styles) -> Vec<Feature> { let mut tags = vec![]; let mut feat = |tag, value| { tags.push(Feature::new(Tag::from_bytes(tag), value, ..)); }; // Features that are on by default in Harfbuzz are only added if disabled. - if !features.kerning { + if !styles.get(TextNode::KERNING) { feat(b"kern", 0); } // Features that are off by default in Harfbuzz are only added if enabled. - if features.smallcaps { + if styles.get(TextNode::SMALLCAPS) { feat(b"smcp", 1); } - if features.alternates { + if styles.get(TextNode::ALTERNATES) { feat(b"salt", 1); } let storage; - if let Some(set) = features.stylistic_set { + if let Some(set) = styles.get(TextNode::STYLISTIC_SET) { storage = [b's', b's', b'0' + set.get() / 10, b'0' + set.get() % 10]; feat(&storage, 1); } - if !features.ligatures.standard { + if !styles.get(TextNode::LIGATURES) { feat(b"liga", 0); feat(b"clig", 0); } - if features.ligatures.discretionary { + if styles.get(TextNode::DISCRETIONARY_LIGATURES) { feat(b"dlig", 1); } - if features.ligatures.historical { + if styles.get(TextNode::HISTORICAL_LIGATURES) { feat(b"hilg", 1); } - match features.numbers.type_ { + match styles.get(TextNode::NUMBER_TYPE) { Smart::Auto => {} Smart::Custom(NumberType::Lining) => feat(b"lnum", 1), Smart::Custom(NumberType::OldStyle) => feat(b"onum", 1), } - match features.numbers.width { + match styles.get(TextNode::NUMBER_WIDTH) { Smart::Auto => {} Smart::Custom(NumberWidth::Proportional) => feat(b"pnum", 1), Smart::Custom(NumberWidth::Tabular) => feat(b"tnum", 1), } - match features.numbers.position { + match styles.get(TextNode::NUMBER_POSITION) { NumberPosition::Normal => {} NumberPosition::Subscript => feat(b"subs", 1), NumberPosition::Superscript => feat(b"sups", 1), } - if features.numbers.slashed_zero { + if styles.get(TextNode::SLASHED_ZERO) { feat(b"zero", 1); } - if features.numbers.fractions { + if styles.get(TextNode::FRACTIONS) { feat(b"frac", 1); } - for &(tag, value) in features.raw.iter() { + for &(tag, value) in styles.get_ref(TextNode::FEATURES).iter() { tags.push(Feature::new(tag, value, ..)) } tags } + +/// 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 text's style properties. + pub styles: &'a Styles, + /// 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 offset = Length::zero(); + let mut frame = Frame::new(self.size); + frame.baseline = Some(self.baseline); + + 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.styles.get(TextNode::SIZE), + fill: self.styles.get(TextNode::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, + }); + } + + 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.styles); + Self { + text: &self.text[text_range], + dir: self.dir, + styles: self.styles, + size, + baseline, + glyphs: Cow::Borrowed(glyphs), + } + } else { + shape(ctx, &self.text[text_range], self.styles, 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, +} diff --git a/src/library/transform.rs b/src/library/transform.rs index 1b30c5b0..6a6b4034 100644 --- a/src/library/transform.rs +++ b/src/library/transform.rs @@ -31,6 +31,7 @@ fn transform_impl(args: &mut Args, transform: Transform) -> TypResult<Value> { .named("origin")? .unwrap_or(Spec::splat(None)) .unwrap_or(Align::CENTER_HORIZON); + Ok(Value::inline( body.into_block().transformed(transform, origin), )) |
