From 712c00ecb72b67da2c0788e5d3eb4dcc6366b2a7 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Fri, 8 Apr 2022 15:08:26 +0200 Subject: [PATCH] Em units --- src/eval/content.rs | 2 +- src/eval/layout.rs | 25 +++--- src/eval/mod.rs | 15 ++-- src/eval/ops.rs | 5 +- src/eval/raw.rs | 133 +++++++++++++++++++++++++++- src/eval/value.rs | 33 +++++-- src/export/render.rs | 4 +- src/font.rs | 18 ++-- src/frame.rs | 2 +- src/geom/angle.rs | 18 ++-- src/geom/em.rs | 12 ++- src/geom/fraction.rs | 4 +- src/geom/macros.rs | 10 +-- src/geom/mod.rs | 6 +- src/geom/point.rs | 4 +- src/geom/ratio.rs | 8 +- src/geom/relative.rs | 33 ++++--- src/geom/sides.rs | 12 +-- src/geom/transform.rs | 4 +- src/lib.rs | 5 +- src/library/graphics/image.rs | 2 +- src/library/graphics/line.rs | 43 ++++++--- src/library/graphics/shape.rs | 9 +- src/library/graphics/transform.rs | 55 +++++++++--- src/library/layout/columns.rs | 5 +- src/library/layout/flow.rs | 20 ++--- src/library/layout/grid.rs | 16 ++-- src/library/layout/pad.rs | 11 +-- src/library/layout/page.rs | 24 ++--- src/library/layout/place.rs | 6 +- src/library/layout/spacing.rs | 2 +- src/library/layout/stack.rs | 16 ++-- src/library/mod.rs | 12 --- src/library/prelude.rs | 4 +- src/library/structure/heading.rs | 14 +-- src/library/structure/list.rs | 24 ++--- src/library/structure/table.rs | 5 +- src/library/text/deco.rs | 51 +++++------ src/library/text/mod.rs | 81 ++++++++++------- src/library/text/par.rs | 24 ++--- src/library/text/shaping.rs | 18 ++-- src/library/utility/math.rs | 5 +- src/parse/mod.rs | 5 +- src/parse/tokens.rs | 47 +++++----- src/syntax/ast.rs | 40 ++++----- src/syntax/highlight.rs | 5 +- src/syntax/mod.rs | 26 ++---- tests/ref/text/edge.png | Bin 0 -> 15857 bytes tests/ref/text/em.png | Bin 1193 -> 1900 bytes tests/ref/text/font.png | Bin 33753 -> 17334 bytes tests/ref/text/tracking-spacing.png | Bin 0 -> 6474 bytes tests/ref/text/tracking.png | Bin 3854 -> 0 bytes tests/typ/code/ops.typ | 4 +- tests/typ/layout/page-marginals.typ | 2 +- tests/typ/style/construct.typ | 4 +- tests/typ/style/show.typ | 6 +- tests/typ/style/wrap.typ | 6 +- tests/typ/text/baseline.typ | 2 +- tests/typ/text/deco.typ | 4 +- tests/typ/text/edge.typ | 25 ++++++ tests/typ/text/em.typ | 23 ++++- tests/typ/text/font.typ | 29 +----- tests/typ/text/par.typ | 2 +- tests/typ/text/tracking-spacing.typ | 30 +++++++ tests/typ/text/tracking.typ | 12 --- tests/typ/utility/math.typ | 22 +++-- tests/typeset.rs | 6 +- 67 files changed, 667 insertions(+), 433 deletions(-) create mode 100644 tests/ref/text/edge.png create mode 100644 tests/ref/text/tracking-spacing.png delete mode 100644 tests/ref/text/tracking.png create mode 100644 tests/typ/text/edge.typ create mode 100644 tests/typ/text/tracking-spacing.typ delete mode 100644 tests/typ/text/tracking.typ diff --git a/src/eval/content.rs b/src/eval/content.rs index 3c1fb310d..166cf6f07 100644 --- a/src/eval/content.rs +++ b/src/eval/content.rs @@ -456,7 +456,7 @@ impl<'a> Builder<'a> { }) .unwrap_or_default() { - par.push_front(ParChild::Spacing(Spacing::Relative(indent))) + par.push_front(ParChild::Spacing(indent.into())); } let node = ParNode(par).pack(); diff --git a/src/eval/layout.rs b/src/eval/layout.rs index 09b692539..f92a31f5f 100644 --- a/src/eval/layout.rs +++ b/src/eval/layout.rs @@ -5,12 +5,10 @@ use std::fmt::{self, Debug, Formatter}; use std::hash::Hash; use std::sync::Arc; -use super::{Barrier, RawAlign, StyleChain}; +use super::{Barrier, RawAlign, RawLength, Resolve, StyleChain}; use crate::diag::TypResult; use crate::frame::{Element, Frame, Geometry, Shape, Stroke}; -use crate::geom::{ - Align, Length, Numeric, Paint, Point, Relative, Sides, Size, Spec, Transform, -}; +use crate::geom::{Align, Length, Paint, Point, Relative, Sides, Size, Spec}; use crate::library::graphics::MoveNode; use crate::library::layout::{AlignNode, PadNode}; use crate::util::Prehashed; @@ -163,7 +161,7 @@ impl LayoutNode { } /// Force a size for this node. - pub fn sized(self, sizing: Spec>>) -> Self { + pub fn sized(self, sizing: Spec>>) -> Self { if sizing.any(Option::is_some) { SizedNode { sizing, child: self }.pack() } else { @@ -191,7 +189,7 @@ impl LayoutNode { } /// Pad this node at the sides. - pub fn padded(self, padding: Sides>) -> Self { + pub fn padded(self, padding: Sides>) -> Self { if !padding.left.is_zero() || !padding.top.is_zero() || !padding.right.is_zero() @@ -204,13 +202,9 @@ impl LayoutNode { } /// Transform this node's contents without affecting layout. - pub fn moved(self, offset: Point) -> Self { - if !offset.is_zero() { - MoveNode { - transform: Transform::translate(offset.x, offset.y), - child: self, - } - .pack() + pub fn moved(self, delta: Spec>) -> Self { + if delta.any(|r| !r.is_zero()) { + MoveNode { delta, child: self }.pack() } else { self } @@ -294,7 +288,7 @@ impl Layout for EmptyNode { #[derive(Debug, Hash)] struct SizedNode { /// How to size the node horizontally and vertically. - sizing: Spec>>, + sizing: Spec>>, /// The node to be sized. child: LayoutNode, } @@ -311,8 +305,9 @@ impl Layout for SizedNode { // Resolve the sizing to a concrete size. let size = self .sizing + .resolve(styles) .zip(regions.base) - .map(|(s, b)| s.map(|v| v.resolve(b))) + .map(|(s, b)| s.map(|v| v.relative_to(b))) .unwrap_or(regions.first); // Select the appropriate base and expansion for the child depending diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 8b777a649..3f5801782 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -43,7 +43,7 @@ use parking_lot::{MappedRwLockWriteGuard, RwLockWriteGuard}; use unicode_segmentation::UnicodeSegmentation; use crate::diag::{At, StrResult, Trace, Tracepoint, TypResult}; -use crate::geom::{Angle, Fraction, Length, Ratio}; +use crate::geom::{Angle, Em, Fraction, Length, Ratio}; use crate::library; use crate::syntax::ast::*; use crate::syntax::{Span, Spanned}; @@ -245,10 +245,13 @@ impl Eval for Lit { LitKind::Bool(v) => Value::Bool(v), LitKind::Int(v) => Value::Int(v), LitKind::Float(v) => Value::Float(v), - LitKind::Length(v, unit) => Value::Length(Length::with_unit(v, unit)), - LitKind::Angle(v, unit) => Value::Angle(Angle::with_unit(v, unit)), - LitKind::Percent(v) => Value::Ratio(Ratio::new(v / 100.0)), - LitKind::Fractional(v) => Value::Fraction(Fraction::new(v)), + LitKind::Numeric(v, unit) => match unit { + Unit::Length(unit) => Length::with_unit(v, unit).into(), + Unit::Angle(unit) => Angle::with_unit(v, unit).into(), + Unit::Em => Em::new(v).into(), + Unit::Fr => Fraction::new(v).into(), + Unit::Percent => Ratio::new(v / 100.0).into(), + }, LitKind::Str(ref v) => Value::Str(v.clone()), }) } @@ -735,7 +738,7 @@ impl Eval for IncludeExpr { /// Process an import of a module relative to the current location. fn import(ctx: &mut Context, path: &str, span: Span) -> TypResult { // Load the source file. - let full = ctx.resolve(path); + let full = ctx.complete_path(path); let id = ctx.sources.load(&full).map_err(|err| match err.kind() { std::io::ErrorKind::NotFound => error!(span, "file not found"), _ => error!(span, "failed to load source file ({})", err), diff --git a/src/eval/ops.rs b/src/eval/ops.rs index 0ba4320ec..898029495 100644 --- a/src/eval/ops.rs +++ b/src/eval/ops.rs @@ -150,8 +150,8 @@ pub fn mul(lhs: Value, rhs: Value) -> StrResult { (Length(a), Int(b)) => Length(a * b as f64), (Length(a), Float(b)) => Length(a * b), - (Int(a), Length(b)) => Length(a as f64 * b), - (Float(a), Length(b)) => Length(a * b), + (Int(a), Length(b)) => Length(b * a as f64), + (Float(a), Length(b)) => Length(b * a), (Angle(a), Int(b)) => Angle(a * b as f64), (Angle(a), Float(b)) => Angle(a * b), @@ -194,7 +194,6 @@ pub fn div(lhs: Value, rhs: Value) -> StrResult { (Length(a), Int(b)) => Length(a / b as f64), (Length(a), Float(b)) => Length(a / b), - (Length(a), Length(b)) => Float(a / b), (Angle(a), Int(b)) => Angle(a / b as f64), (Angle(a), Float(b)) => Angle(a / b), diff --git a/src/eval/raw.rs b/src/eval/raw.rs index 337638f99..622a0562c 100644 --- a/src/eval/raw.rs +++ b/src/eval/raw.rs @@ -1,8 +1,9 @@ use std::fmt::{self, Debug, Formatter}; +use std::ops::{Add, Div, Mul, Neg}; use super::{Resolve, StyleChain}; -use crate::geom::{Align, SpecAxis}; -use crate::library::text::ParNode; +use crate::geom::{Align, Em, Length, Numeric, Relative, SpecAxis}; +use crate::library::text::{ParNode, TextNode}; /// The unresolved alignment representation. #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] @@ -47,3 +48,131 @@ impl Debug for RawAlign { } } } + +/// The unresolved length representation. +/// +/// Currently supports absolute and em units, but support could quite easily be +/// extended to other units that can be resolved through a style chain. +/// Probably, it would be a good idea to then move to an enum representation +/// that has a small footprint and allocates for the rare case that units are +/// mixed. +#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct RawLength { + /// The absolute part. + pub length: Length, + /// The font-relative part. + pub em: Em, +} + +impl RawLength { + /// The zero length. + pub const fn zero() -> Self { + Self { length: Length::zero(), em: Em::zero() } + } +} + +impl Debug for RawLength { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match (self.length.is_zero(), self.em.is_zero()) { + (false, false) => write!(f, "{:?} + {:?}", self.length, self.em), + (true, false) => self.em.fmt(f), + (_, true) => self.length.fmt(f), + } + } +} + +impl Resolve for Em { + type Output = Length; + + fn resolve(self, styles: StyleChain) -> Self::Output { + if self.is_zero() { + Length::zero() + } else { + self.at(styles.get(TextNode::SIZE)) + } + } +} + +impl Resolve for RawLength { + type Output = Length; + + fn resolve(self, styles: StyleChain) -> Self::Output { + self.length + self.em.resolve(styles) + } +} + +impl From for RawLength { + fn from(length: Length) -> Self { + Self { length, em: Em::zero() } + } +} + +impl From for RawLength { + fn from(em: Em) -> Self { + Self { length: Length::zero(), em } + } +} + +impl From for Relative { + fn from(length: Length) -> Self { + Relative::from(RawLength::from(length)) + } +} + +impl Numeric for RawLength { + fn zero() -> Self { + Self::zero() + } + + fn is_finite(self) -> bool { + self.length.is_finite() && self.em.is_finite() + } +} + +impl Neg for RawLength { + type Output = Self; + + fn neg(self) -> Self::Output { + Self { length: -self.length, em: -self.em } + } +} + +impl Add for RawLength { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + Self { + length: self.length + rhs.length, + em: self.em + rhs.em, + } + } +} + +sub_impl!(RawLength - RawLength -> RawLength); + +impl Mul for RawLength { + type Output = Self; + + fn mul(self, rhs: f64) -> Self::Output { + Self { + length: self.length * rhs, + em: self.em * rhs, + } + } +} + +impl Div for RawLength { + type Output = Self; + + fn div(self, rhs: f64) -> Self::Output { + Self { + length: self.length / rhs, + em: self.em / rhs, + } + } +} + +assign_impl!(RawLength += RawLength); +assign_impl!(RawLength -= RawLength); +assign_impl!(RawLength *= f64); +assign_impl!(RawLength /= f64); diff --git a/src/eval/value.rs b/src/eval/value.rs index 12948d72f..1851cf281 100644 --- a/src/eval/value.rs +++ b/src/eval/value.rs @@ -4,9 +4,9 @@ use std::fmt::{self, Debug, Formatter}; use std::hash::{Hash, Hasher}; use std::sync::Arc; -use super::{ops, Args, Array, Content, Context, Dict, Func, Layout, StrExt}; +use super::{ops, Args, Array, Content, Context, Dict, Func, Layout, RawLength, StrExt}; use crate::diag::{with_alternative, At, StrResult, TypResult}; -use crate::geom::{Angle, Color, Fraction, Length, Ratio, Relative, RgbaColor}; +use crate::geom::{Angle, Color, Em, Fraction, Length, Ratio, Relative, RgbaColor}; use crate::library::text::RawNode; use crate::syntax::{Span, Spanned}; use crate::util::EcoString; @@ -25,13 +25,13 @@ pub enum Value { /// A floating-point number: `1.2`, `10e-4`. Float(f64), /// A length: `12pt`, `3cm`. - Length(Length), + Length(RawLength), /// An angle: `1.5rad`, `90deg`. Angle(Angle), /// A ratio: `50%`. Ratio(Ratio), /// A relative length, combination of a ratio and a length: `20% + 5cm`. - Relative(Relative), + Relative(Relative), /// A fraction: `1fr`. Fraction(Fraction), /// A color value: `#f79143ff`. @@ -77,10 +77,10 @@ impl Value { Self::Bool(_) => bool::TYPE_NAME, Self::Int(_) => i64::TYPE_NAME, Self::Float(_) => f64::TYPE_NAME, - Self::Length(_) => Length::TYPE_NAME, + Self::Length(_) => RawLength::TYPE_NAME, Self::Angle(_) => Angle::TYPE_NAME, Self::Ratio(_) => Ratio::TYPE_NAME, - Self::Relative(_) => Relative::TYPE_NAME, + Self::Relative(_) => Relative::::TYPE_NAME, Self::Fraction(_) => Fraction::TYPE_NAME, Self::Color(_) => Color::TYPE_NAME, Self::Str(_) => EcoString::TYPE_NAME, @@ -320,6 +320,18 @@ impl From for Value { } } +impl From for Value { + fn from(v: Length) -> Self { + Self::Length(v.into()) + } +} + +impl From for Value { + fn from(v: Em) -> Self { + Self::Length(v.into()) + } +} + impl From for Value { fn from(v: RgbaColor) -> Self { Self::Color(v.into()) @@ -546,10 +558,10 @@ macro_rules! castable { primitive! { bool: "boolean", Bool } primitive! { i64: "integer", Int } primitive! { f64: "float", Float, Int(v) => v as f64 } -primitive! { Length: "length", Length } +primitive! { RawLength: "length", Length } primitive! { Angle: "angle", Angle } primitive! { Ratio: "ratio", Ratio } -primitive! { Relative: "relative length", Relative, Length(v) => v.into(), Ratio(v) => v.into() } +primitive! { Relative: "relative length", Relative, Length(v) => v.into(), Ratio(v) => v.into() } primitive! { Fraction: "fraction", Fraction } primitive! { Color: "color", Color } primitive! { EcoString: "string", Str } @@ -685,7 +697,10 @@ mod tests { test(Length::pt(5.5), "5.5pt"); test(Angle::deg(90.0), "90deg"); test(Ratio::one() / 2.0, "50%"); - test(Ratio::new(0.3) + Length::cm(2.0), "30% + 56.69pt"); + test( + Ratio::new(0.3) + RawLength::from(Length::cm(2.0)), + "30% + 56.69pt", + ); test(Fraction::one() * 7.55, "7.55fr"); test(Color::Rgba(RgbaColor::new(1, 1, 1, 0xff)), "#010101"); diff --git a/src/export/render.rs b/src/export/render.rs index 89a17eeaa..d6f82121d 100644 --- a/src/export/render.rs +++ b/src/export/render.rs @@ -117,14 +117,14 @@ fn render_text( let mut x = 0.0; for glyph in &text.glyphs { let id = GlyphId(glyph.id); - let offset = x + glyph.x_offset.resolve(text.size).to_f32(); + let offset = x + glyph.x_offset.at(text.size).to_f32(); let ts = ts.pre_translate(offset, 0.0); render_svg_glyph(canvas, ts, mask, ctx, text, id) .or_else(|| render_bitmap_glyph(canvas, ts, mask, ctx, text, id)) .or_else(|| render_outline_glyph(canvas, ts, mask, ctx, text, id)); - x += glyph.x_advance.resolve(text.size).to_f32(); + x += glyph.x_advance.at(text.size).to_f32(); } } diff --git a/src/font.rs b/src/font.rs index e1d0c4e64..ce1c48f89 100644 --- a/src/font.rs +++ b/src/font.rs @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize}; use ttf_parser::{name_id, GlyphId, PlatformId, Tag}; use unicode_segmentation::UnicodeSegmentation; -use crate::geom::{Em, Length, Relative}; +use crate::geom::Em; use crate::loading::{FileHash, Loader}; use crate::util::decode_mac_roman; @@ -372,14 +372,13 @@ impl FaceMetrics { } /// Look up a vertical metric at the given font size. - pub fn vertical(&self, metric: VerticalFontMetric, size: Length) -> Length { + pub fn vertical(&self, metric: VerticalFontMetric) -> Em { match metric { - VerticalFontMetric::Ascender => self.ascender.resolve(size), - VerticalFontMetric::CapHeight => self.cap_height.resolve(size), - VerticalFontMetric::XHeight => self.x_height.resolve(size), - VerticalFontMetric::Baseline => Length::zero(), - VerticalFontMetric::Descender => self.descender.resolve(size), - VerticalFontMetric::Relative(v) => v.resolve(size), + VerticalFontMetric::Ascender => self.ascender, + VerticalFontMetric::CapHeight => self.cap_height, + VerticalFontMetric::XHeight => self.x_height, + VerticalFontMetric::Baseline => Em::zero(), + VerticalFontMetric::Descender => self.descender, } } } @@ -413,9 +412,6 @@ pub enum VerticalFontMetric { /// Corresponds to the typographic descender from the `OS/2` table if /// present and falls back to the descender from the `hhea` table otherwise. Descender, - /// An font-size dependent distance from the baseline (positive goes up, negative - /// down). - Relative(Relative), } /// Properties of a single font face. diff --git a/src/frame.rs b/src/frame.rs index 5698560f1..a104c0695 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -197,7 +197,7 @@ pub struct Text { impl Text { /// The width of the text run. pub fn width(&self) -> Length { - self.glyphs.iter().map(|g| g.x_advance.resolve(self.size)).sum() + self.glyphs.iter().map(|g| g.x_advance.at(self.size)).sum() } } diff --git a/src/geom/angle.rs b/src/geom/angle.rs index a0900ce5b..888442f7a 100644 --- a/src/geom/angle.rs +++ b/src/geom/angle.rs @@ -16,18 +16,18 @@ impl Angle { } /// Create an angle from a value in a unit. - pub fn with_unit(val: f64, unit: AngularUnit) -> Self { + pub fn with_unit(val: f64, unit: AngleUnit) -> Self { Self(Scalar(val * unit.raw_scale())) } /// Create an angle from a number of radians. pub fn rad(rad: f64) -> Self { - Self::with_unit(rad, AngularUnit::Rad) + Self::with_unit(rad, AngleUnit::Rad) } /// Create an angle from a number of degrees. pub fn deg(deg: f64) -> Self { - Self::with_unit(deg, AngularUnit::Deg) + Self::with_unit(deg, AngleUnit::Deg) } /// Get the value of this angle in raw units. @@ -36,18 +36,18 @@ impl Angle { } /// Get the value of this length in unit. - pub fn to_unit(self, unit: AngularUnit) -> f64 { + pub fn to_unit(self, unit: AngleUnit) -> f64 { self.to_raw() / unit.raw_scale() } /// Convert this to a number of radians. pub fn to_rad(self) -> f64 { - self.to_unit(AngularUnit::Rad) + self.to_unit(AngleUnit::Rad) } /// Convert this to a number of degrees. pub fn to_deg(self) -> f64 { - self.to_unit(AngularUnit::Deg) + self.to_unit(AngleUnit::Deg) } /// The absolute value of the this angle. @@ -145,14 +145,14 @@ impl Sum for Angle { /// Different units of angular measurement. #[derive(Copy, Clone, Eq, PartialEq, Hash)] -pub enum AngularUnit { +pub enum AngleUnit { /// Radians. Rad, /// Degrees. Deg, } -impl AngularUnit { +impl AngleUnit { /// How many raw units correspond to a value of `1.0` in this unit. fn raw_scale(self) -> f64 { match self { @@ -162,7 +162,7 @@ impl AngularUnit { } } -impl Debug for AngularUnit { +impl Debug for AngleUnit { fn fmt(&self, f: &mut Formatter) -> fmt::Result { f.pad(match self { Self::Rad => "rad", diff --git a/src/geom/em.rs b/src/geom/em.rs index 3c772d961..ec1cfbda6 100644 --- a/src/geom/em.rs +++ b/src/geom/em.rs @@ -29,7 +29,12 @@ impl Em { /// Create an em length from a length at the given font size. pub fn from_length(length: Length, font_size: Length) -> Self { - Self(Scalar(length / font_size)) + let result = length / font_size; + if result.is_finite() { + Self(Scalar(result)) + } else { + Self::zero() + } } /// The number of em units. @@ -38,8 +43,9 @@ impl Em { } /// Convert to a length at the given font size. - pub fn resolve(self, font_size: Length) -> Length { - self.get() * font_size + pub fn at(self, font_size: Length) -> Length { + let resolved = font_size * self.get(); + if resolved.is_finite() { resolved } else { Length::zero() } } } diff --git a/src/geom/fraction.rs b/src/geom/fraction.rs index 2f33a1342..f71886032 100644 --- a/src/geom/fraction.rs +++ b/src/geom/fraction.rs @@ -30,8 +30,8 @@ impl Fraction { Self::new(self.get().abs()) } - /// Resolve this fraction's share in the remaining space. - pub fn resolve(self, total: Self, remaining: Length) -> Length { + /// Determine this fraction's share in the remaining space. + pub fn share(self, total: Self, remaining: Length) -> Length { let ratio = self / total; if ratio.is_finite() && remaining.is_finite() { ratio * remaining diff --git a/src/geom/macros.rs b/src/geom/macros.rs index 615eb31c5..b1b50e22a 100644 --- a/src/geom/macros.rs +++ b/src/geom/macros.rs @@ -1,7 +1,7 @@ /// Implement the `Sub` trait based on existing `Neg` and `Add` impls. macro_rules! sub_impl { ($a:ident - $b:ident -> $c:ident) => { - impl Sub<$b> for $a { + impl std::ops::Sub<$b> for $a { type Output = $c; fn sub(self, other: $b) -> $c { @@ -14,7 +14,7 @@ macro_rules! sub_impl { /// Implement an assign trait based on an existing non-assign trait. macro_rules! assign_impl { ($a:ident += $b:ident) => { - impl AddAssign<$b> for $a { + impl std::ops::AddAssign<$b> for $a { fn add_assign(&mut self, other: $b) { *self = *self + other; } @@ -22,7 +22,7 @@ macro_rules! assign_impl { }; ($a:ident -= $b:ident) => { - impl SubAssign<$b> for $a { + impl std::ops::SubAssign<$b> for $a { fn sub_assign(&mut self, other: $b) { *self = *self - other; } @@ -30,7 +30,7 @@ macro_rules! assign_impl { }; ($a:ident *= $b:ident) => { - impl MulAssign<$b> for $a { + impl std::ops::MulAssign<$b> for $a { fn mul_assign(&mut self, other: $b) { *self = *self * other; } @@ -38,7 +38,7 @@ macro_rules! assign_impl { }; ($a:ident /= $b:ident) => { - impl DivAssign<$b> for $a { + impl std::ops::DivAssign<$b> for $a { fn div_assign(&mut self, other: $b) { *self = *self / other; } diff --git a/src/geom/mod.rs b/src/geom/mod.rs index bfb450a1a..a6f53c87e 100644 --- a/src/geom/mod.rs +++ b/src/geom/mod.rs @@ -72,15 +72,15 @@ pub trait Numeric: + Mul + Div { - /// The identity element. + /// The identity element for addition. fn zero() -> Self; - /// Whether `self` is the identity element. + /// Whether `self` is zero. fn is_zero(self) -> bool { self == Self::zero() } - /// Whether `self` contains only finite parts. + /// Whether `self` consists only of finite parts. fn is_finite(self) -> bool; } diff --git a/src/geom/point.rs b/src/geom/point.rs index afce68ba3..dd89fbf56 100644 --- a/src/geom/point.rs +++ b/src/geom/point.rs @@ -38,8 +38,8 @@ impl Point { /// Transform the point with the given transformation. pub fn transform(self, ts: Transform) -> Self { Self::new( - ts.sx.resolve(self.x) + ts.kx.resolve(self.y) + ts.tx, - ts.ky.resolve(self.x) + ts.sy.resolve(self.y) + ts.ty, + ts.sx.of(self.x) + ts.kx.of(self.y) + ts.tx, + ts.ky.of(self.x) + ts.sy.of(self.y) + ts.ty, ) } } diff --git a/src/geom/ratio.rs b/src/geom/ratio.rs index 7dca53c2c..69f06dd28 100644 --- a/src/geom/ratio.rs +++ b/src/geom/ratio.rs @@ -3,7 +3,7 @@ use super::*; /// A ratio of a whole. /// /// _Note_: `50%` is represented as `0.5` here, but stored as `50.0` in the -/// corresponding [literal](crate::syntax::ast::LitKind::Percent). +/// corresponding [literal](crate::syntax::ast::LitKind::Numeric). #[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct Ratio(Scalar); @@ -38,13 +38,13 @@ impl Ratio { self.0 == 1.0 } - /// The absolute value of the this ratio. + /// The absolute value of this ratio. pub fn abs(self) -> Self { Self::new(self.get().abs()) } - /// Resolve this relative to the given `whole`. - pub fn resolve(self, whole: T) -> T { + /// Return the ratio of the given `whole`. + pub fn of(self, whole: T) -> T { let resolved = whole * self.get(); if resolved.is_finite() { resolved } else { T::zero() } } diff --git a/src/geom/relative.rs b/src/geom/relative.rs index 066b8c15d..fc77fb9ff 100644 --- a/src/geom/relative.rs +++ b/src/geom/relative.rs @@ -27,12 +27,17 @@ impl Relative { /// Whether both parts are zero. pub fn is_zero(self) -> bool { - self.rel.is_zero() && self.abs.is_zero() + self.rel.is_zero() && self.abs == T::zero() } - /// Resolve this relative to the given `whole`. - pub fn resolve(self, whole: T) -> T { - self.rel.resolve(whole) + self.abs + /// Whether the relative part is one and the absolute part is zero. + pub fn is_one(self) -> bool { + self.rel.is_one() && self.abs == T::zero() + } + + /// Evaluate this relative to the given `whole`. + pub fn relative_to(self, whole: T) -> T { + self.rel.of(whole) + self.abs } /// Map the absolute part with `f`. @@ -120,27 +125,31 @@ impl Div for Relative { } } -impl AddAssign for Relative { +impl AddAssign for Relative { fn add_assign(&mut self, other: Self) { - *self = *self + other; + self.rel += other.rel; + self.abs += other.abs; } } -impl SubAssign for Relative { +impl SubAssign for Relative { fn sub_assign(&mut self, other: Self) { - *self = *self - other; + self.rel -= other.rel; + self.abs -= other.abs; } } -impl MulAssign for Relative { +impl> MulAssign for Relative { fn mul_assign(&mut self, other: f64) { - *self = *self * other; + self.rel *= other; + self.abs *= other; } } -impl DivAssign for Relative { +impl> DivAssign for Relative { fn div_assign(&mut self, other: f64) { - *self = *self * other; + self.rel /= other; + self.abs /= other; } } diff --git a/src/geom/sides.rs b/src/geom/sides.rs index 4539728fc..3584a1ce9 100644 --- a/src/geom/sides.rs +++ b/src/geom/sides.rs @@ -44,13 +44,13 @@ where } impl Sides> { - /// Resolve the sides relative to the given `size`. - pub fn resolve(self, size: Size) -> Sides { + /// Evaluate the sides relative to the given `size`. + pub fn relative_to(self, size: Size) -> Sides { Sides { - left: self.left.resolve(size.x), - top: self.top.resolve(size.y), - right: self.right.resolve(size.x), - bottom: self.bottom.resolve(size.y), + left: self.left.relative_to(size.x), + top: self.top.relative_to(size.y), + right: self.right.relative_to(size.x), + bottom: self.bottom.relative_to(size.y), } } } diff --git a/src/geom/transform.rs b/src/geom/transform.rs index c0a06e33c..28a1af809 100644 --- a/src/geom/transform.rs +++ b/src/geom/transform.rs @@ -59,8 +59,8 @@ impl Transform { ky: self.ky * prev.sx + self.sy * prev.ky, kx: self.sx * prev.kx + self.kx * prev.sy, sy: self.ky * prev.kx + self.sy * prev.sy, - tx: self.sx.resolve(prev.tx) + self.kx.resolve(prev.ty) + self.tx, - ty: self.ky.resolve(prev.tx) + self.sy.resolve(prev.ty) + self.ty, + tx: self.sx.of(prev.tx) + self.kx.of(prev.ty) + self.tx, + ty: self.ky.of(prev.tx) + self.sy.of(prev.ty) + self.ty, } } } diff --git a/src/lib.rs b/src/lib.rs index cb434e62a..6dc52b67c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,13 +34,14 @@ #[macro_use] pub mod util; #[macro_use] +pub mod geom; +#[macro_use] pub mod diag; #[macro_use] pub mod eval; pub mod export; pub mod font; pub mod frame; -pub mod geom; pub mod image; pub mod library; pub mod loading; @@ -163,7 +164,7 @@ impl Context { /// Resolve a user-entered path (relative to the current evaluation /// location) to be relative to the compilation environment's root. - pub fn resolve(&self, path: &str) -> PathBuf { + pub fn complete_path(&self, path: &str) -> PathBuf { if let Some(&id) = self.route.last() { if let Some(dir) = self.sources.get(id).path().parent() { return dir.join(path); diff --git a/src/library/graphics/image.rs b/src/library/graphics/image.rs index 23ad52abb..193dc60eb 100644 --- a/src/library/graphics/image.rs +++ b/src/library/graphics/image.rs @@ -13,7 +13,7 @@ impl ImageNode { fn construct(ctx: &mut Context, args: &mut Args) -> TypResult { let path = args.expect::>("path to image file")?; - let full = ctx.resolve(&path.v); + let full = ctx.complete_path(&path.v); let id = ctx.images.load(&full).map_err(|err| match err.kind() { std::io::ErrorKind::NotFound => error!(path.span, "file not found"), _ => error!(path.span, "failed to load image ({})", err), diff --git a/src/library/graphics/line.rs b/src/library/graphics/line.rs index 571506c12..1dd138e68 100644 --- a/src/library/graphics/line.rs +++ b/src/library/graphics/line.rs @@ -4,9 +4,9 @@ use crate::library::prelude::*; #[derive(Debug, Hash)] pub struct LineNode { /// Where the line starts. - origin: Spec>, + origin: Spec>, /// The offset from the `origin` where the line ends. - delta: Spec>, + delta: Spec>, } #[node] @@ -14,15 +14,17 @@ impl LineNode { /// How to stroke the line. pub const STROKE: Paint = Color::BLACK.into(); /// The line's thickness. - pub const THICKNESS: Length = Length::pt(1.0); + #[property(resolve)] + pub const THICKNESS: RawLength = Length::pt(1.0).into(); fn construct(_: &mut Context, args: &mut Args) -> TypResult { let origin = args.named("origin")?.unwrap_or_default(); - let delta = match args.named::>>("to")? { + + let delta = match args.named::>>("to")? { Some(to) => to.zip(origin).map(|(to, from)| to - from), None => { let length = args - .named::>("length")? + .named::>("length")? .unwrap_or(Length::cm(1.0).into()); let angle = args.named::("angle")?.unwrap_or_default(); @@ -50,18 +52,37 @@ impl Layout for LineNode { thickness, }); - let resolved_origin = - self.origin.zip(regions.base).map(|(l, b)| Relative::resolve(l, b)); - let resolved_delta = - self.delta.zip(regions.base).map(|(l, b)| Relative::resolve(l, b)); + let origin = self + .origin + .resolve(styles) + .zip(regions.base) + .map(|(l, b)| l.relative_to(b)); - let geometry = Geometry::Line(resolved_delta.to_point()); + let delta = self + .delta + .resolve(styles) + .zip(regions.base) + .map(|(l, b)| l.relative_to(b)); + + let geometry = Geometry::Line(delta.to_point()); let shape = Shape { geometry, fill: None, stroke }; let target = regions.expand.select(regions.first, Size::zero()); let mut frame = Frame::new(target); - frame.push(resolved_origin.to_point(), Element::Shape(shape)); + frame.push(origin.to_point(), Element::Shape(shape)); Ok(vec![Arc::new(frame)]) } } + +castable! { + Spec>, + Expected: "array of two relative lengths", + Value::Array(array) => { + let mut iter = array.into_iter(); + match (iter.next(), iter.next(), iter.next()) { + (Some(a), Some(b), None) => Spec::new(a.cast()?, b.cast()?), + _ => Err("point array must contain exactly two entries")?, + } + }, +} diff --git a/src/library/graphics/shape.rs b/src/library/graphics/shape.rs index 9faa4c52a..ec6f735bf 100644 --- a/src/library/graphics/shape.rs +++ b/src/library/graphics/shape.rs @@ -26,14 +26,15 @@ impl ShapeNode { /// How to stroke the shape. pub const STROKE: Smart> = Smart::Auto; /// The stroke's thickness. - pub const THICKNESS: Length = Length::pt(1.0); + #[property(resolve)] + pub const THICKNESS: RawLength = Length::pt(1.0).into(); /// How much to pad the shape's content. - pub const PADDING: Relative = Relative::zero(); + pub const PADDING: Relative = Relative::zero(); fn construct(_: &mut Context, args: &mut Args) -> TypResult { let size = match S { - SQUARE => args.named::("size")?.map(Relative::from), - CIRCLE => args.named::("radius")?.map(|r| 2.0 * Relative::from(r)), + SQUARE => args.named::("size")?.map(Relative::from), + CIRCLE => args.named::("radius")?.map(|r| 2.0 * Relative::from(r)), _ => None, }; diff --git a/src/library/graphics/transform.rs b/src/library/graphics/transform.rs index 67f9cad97..ea021cc19 100644 --- a/src/library/graphics/transform.rs +++ b/src/library/graphics/transform.rs @@ -1,6 +1,46 @@ use crate::geom::Transform; use crate::library::prelude::*; +/// Move a node without affecting layout. +#[derive(Debug, Hash)] +pub struct MoveNode { + /// The offset by which to move the node. + pub delta: Spec>, + /// The node whose contents should be moved. + pub child: LayoutNode, +} + +#[node] +impl MoveNode { + fn construct(_: &mut Context, args: &mut Args) -> TypResult { + let dx = args.named("x")?.unwrap_or_default(); + let dy = args.named("y")?.unwrap_or_default(); + Ok(Content::inline(Self { + delta: Spec::new(dx, dy), + child: args.expect("body")?, + })) + } +} + +impl Layout for MoveNode { + fn layout( + &self, + ctx: &mut Context, + regions: &Regions, + styles: StyleChain, + ) -> TypResult>> { + let mut frames = self.child.layout(ctx, regions, styles)?; + + let delta = self.delta.resolve(styles); + for frame in &mut frames { + let delta = delta.zip(frame.size).map(|(d, s)| d.relative_to(s)); + Arc::make_mut(frame).translate(delta.to_point()); + } + + Ok(frames) + } +} + /// Transform a node without affecting layout. #[derive(Debug, Hash)] pub struct TransformNode { @@ -10,13 +50,10 @@ pub struct TransformNode { pub child: LayoutNode, } -/// Transform a node by translating it without affecting layout. -pub type MoveNode = TransformNode; - -/// Transform a node by rotating it without affecting layout. +/// Rotate a node without affecting layout. pub type RotateNode = TransformNode; -/// Transform a node by scaling it without affecting layout. +/// Scale a node without affecting layout. pub type ScaleNode = TransformNode; #[node] @@ -27,11 +64,6 @@ impl TransformNode { fn construct(_: &mut Context, args: &mut Args) -> TypResult { let transform = match T { - MOVE => { - let tx = args.named("x")?.unwrap_or_default(); - let ty = args.named("y")?.unwrap_or_default(); - Transform::translate(tx, ty) - } ROTATE => { let angle = args.named_or_find("angle")?.unwrap_or_default(); Transform::rotate(angle) @@ -77,9 +109,6 @@ impl Layout for TransformNode { /// Kinds of transformations. pub type TransformKind = usize; -/// A translation on the X and Y axes. -const MOVE: TransformKind = 0; - /// A rotational transformation. const ROTATE: TransformKind = 1; diff --git a/src/library/layout/columns.rs b/src/library/layout/columns.rs index 1cb45c37f..3ef66b406 100644 --- a/src/library/layout/columns.rs +++ b/src/library/layout/columns.rs @@ -14,7 +14,8 @@ pub struct ColumnsNode { #[node] impl ColumnsNode { /// The size of the gutter space between each column. - pub const GUTTER: Relative = Ratio::new(0.04).into(); + #[property(resolve)] + pub const GUTTER: Relative = Ratio::new(0.04).into(); fn construct(_: &mut Context, args: &mut Args) -> TypResult { Ok(Content::block(Self { @@ -39,7 +40,7 @@ impl Layout for ColumnsNode { // Determine the width of the gutter and each column. let columns = self.columns.get(); - let gutter = styles.get(Self::GUTTER).resolve(regions.base.x); + let gutter = styles.get(Self::GUTTER).relative_to(regions.base.x); let width = (regions.first.x - gutter * (columns - 1) as f64) / columns as f64; // Create the pod regions. diff --git a/src/library/layout/flow.rs b/src/library/layout/flow.rs index a53b03041..a3947e346 100644 --- a/src/library/layout/flow.rs +++ b/src/library/layout/flow.rs @@ -1,6 +1,6 @@ use super::{AlignNode, PlaceNode, Spacing}; use crate::library::prelude::*; -use crate::library::text::{ParNode, TextNode}; +use crate::library::text::ParNode; /// Arrange spacing, paragraphs and other block-level nodes into a flow. /// @@ -37,22 +37,20 @@ impl Layout for FlowNode { let styles = map.chain(&styles); match child { FlowChild::Leading => { - let em = styles.get(TextNode::SIZE); - let amount = styles.get(ParNode::LEADING).resolve(em); - layouter.layout_spacing(amount.into()); + let amount = styles.get(ParNode::LEADING); + layouter.layout_spacing(amount.into(), styles); } FlowChild::Parbreak => { - let em = styles.get(TextNode::SIZE); let leading = styles.get(ParNode::LEADING); let spacing = styles.get(ParNode::SPACING); - let amount = (leading + spacing).resolve(em); - layouter.layout_spacing(amount.into()); + let amount = leading + spacing; + layouter.layout_spacing(amount.into(), styles); } FlowChild::Colbreak => { layouter.finish_region(); } FlowChild::Spacing(kind) => { - layouter.layout_spacing(*kind); + layouter.layout_spacing(*kind, styles); } FlowChild::Node(ref node) => { layouter.layout_node(ctx, node, styles)?; @@ -142,11 +140,11 @@ impl FlowLayouter { } /// Layout spacing. - pub fn layout_spacing(&mut self, spacing: Spacing) { + pub fn layout_spacing(&mut self, spacing: Spacing, styles: StyleChain) { match spacing { Spacing::Relative(v) => { // Resolve the spacing and limit it to the remaining space. - let resolved = v.resolve(self.full.y); + let resolved = v.resolve(styles).relative_to(self.full.y); let limited = resolved.min(self.regions.first.y); self.regions.first.y -= limited; self.used.y += limited; @@ -235,7 +233,7 @@ impl FlowLayouter { offset += v; } FlowItem::Fractional(v) => { - offset += v.resolve(self.fr, remaining); + offset += v.share(self.fr, remaining); } FlowItem::Frame(frame, aligns) => { ruler = ruler.max(aligns.y); diff --git a/src/library/layout/grid.rs b/src/library/layout/grid.rs index b1e5e54c3..ad6323d5a 100644 --- a/src/library/layout/grid.rs +++ b/src/library/layout/grid.rs @@ -58,7 +58,7 @@ pub enum TrackSizing { Auto, /// A track size specified in absolute terms and relative to the parent's /// size. - Relative(Relative), + Relative(Relative), /// A track size specified as a fraction of the remaining free space in the /// parent. Fractional(Fraction), @@ -236,7 +236,8 @@ impl<'a> GridLayouter<'a> { match col { TrackSizing::Auto => {} TrackSizing::Relative(v) => { - let resolved = v.resolve(self.regions.base.x); + let resolved = + v.resolve(self.styles).relative_to(self.regions.base.x); *rcol = resolved; rel += resolved; } @@ -295,7 +296,8 @@ impl<'a> GridLayouter<'a> { // base, for auto it's already correct and for fr we could // only guess anyway. if let TrackSizing::Relative(v) = self.rows[y] { - pod.base.y = v.resolve(self.regions.base.y); + pod.base.y = + v.resolve(self.styles).relative_to(self.regions.base.y); } let frame = node.layout(ctx, &pod, self.styles)?.remove(0); @@ -315,7 +317,7 @@ impl<'a> GridLayouter<'a> { fn grow_fractional_columns(&mut self, remaining: Length, fr: Fraction) { for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) { if let TrackSizing::Fractional(v) = col { - *rcol = v.resolve(fr, remaining); + *rcol = v.share(fr, remaining); } } } @@ -422,10 +424,10 @@ impl<'a> GridLayouter<'a> { fn layout_relative_row( &mut self, ctx: &mut Context, - v: Relative, + v: Relative, y: usize, ) -> TypResult<()> { - let resolved = v.resolve(self.regions.base.y); + let resolved = v.resolve(self.styles).relative_to(self.regions.base.y); let frame = self.layout_single_row(ctx, resolved, y)?; // Skip to fitting region. @@ -543,7 +545,7 @@ impl<'a> GridLayouter<'a> { Row::Frame(frame) => frame, Row::Fr(v, y) => { let remaining = self.full - self.used.y; - let height = v.resolve(self.fr, remaining); + let height = v.share(self.fr, remaining); self.layout_single_row(ctx, height, y)? } }; diff --git a/src/library/layout/pad.rs b/src/library/layout/pad.rs index b7470540d..e688e4231 100644 --- a/src/library/layout/pad.rs +++ b/src/library/layout/pad.rs @@ -4,7 +4,7 @@ use crate::library::prelude::*; #[derive(Debug, Hash)] pub struct PadNode { /// The amount of padding. - pub padding: Sides>, + pub padding: Sides>, /// The child node whose sides to pad. pub child: LayoutNode, } @@ -33,14 +33,15 @@ impl Layout for PadNode { styles: StyleChain, ) -> TypResult>> { // Layout child into padded regions. - let pod = regions.map(|size| shrink(size, self.padding)); + let padding = self.padding.resolve(styles); + let pod = regions.map(|size| shrink(size, padding)); let mut frames = self.child.layout(ctx, &pod, styles)?; for frame in &mut frames { // Apply the padding inversely such that the grown size padded // yields the frame's size. - let padded = grow(frame.size, self.padding); - let padding = self.padding.resolve(padded); + let padded = grow(frame.size, padding); + let padding = padding.relative_to(padded); let offset = Point::new(padding.left, padding.top); // Grow the frame and translate everything in the frame inwards. @@ -55,7 +56,7 @@ impl Layout for PadNode { /// Shrink a size by padding relative to the size itself. fn shrink(size: Size, padding: Sides>) -> Size { - size - padding.resolve(size).sum_by_axis() + size - padding.relative_to(size).sum_by_axis() } /// Grow a size by padding relative to the grown size. diff --git a/src/library/layout/page.rs b/src/library/layout/page.rs index 37a87ae22..7aa53b237 100644 --- a/src/library/layout/page.rs +++ b/src/library/layout/page.rs @@ -10,19 +10,21 @@ pub struct PageNode(pub LayoutNode); #[node] impl PageNode { /// The unflipped width of the page. - pub const WIDTH: Smart = Smart::Custom(Paper::A4.width()); + #[property(resolve)] + pub const WIDTH: Smart = Smart::Custom(Paper::A4.width().into()); /// The unflipped height of the page. - pub const HEIGHT: Smart = Smart::Custom(Paper::A4.height()); + #[property(resolve)] + pub const HEIGHT: Smart = Smart::Custom(Paper::A4.height().into()); /// Whether the page is flipped into landscape orientation. pub const FLIPPED: bool = false; /// The left margin. - pub const LEFT: Smart> = Smart::Auto; + pub const LEFT: Smart> = Smart::Auto; /// The right margin. - pub const RIGHT: Smart> = Smart::Auto; + pub const RIGHT: Smart> = Smart::Auto; /// The top margin. - pub const TOP: Smart> = Smart::Auto; + pub const TOP: Smart> = Smart::Auto; /// The bottom margin. - pub const BOTTOM: Smart> = Smart::Auto; + pub const BOTTOM: Smart> = Smart::Auto; /// The page's background color. pub const FILL: Option = None; /// How many columns the page has. @@ -42,8 +44,8 @@ impl PageNode { let mut styles = StyleMap::new(); if let Some(paper) = args.named_or_find::("paper")? { - styles.set(Self::WIDTH, Smart::Custom(paper.width())); - styles.set(Self::HEIGHT, Smart::Custom(paper.height())); + styles.set(Self::WIDTH, Smart::Custom(paper.width().into())); + styles.set(Self::HEIGHT, Smart::Custom(paper.height().into())); } styles.set_opt(Self::WIDTH, args.named("width")?); @@ -115,7 +117,7 @@ impl PageNode { } // Layout the child. - let regions = Regions::repeat(size, size, size.map(Numeric::is_finite)); + let regions = Regions::repeat(size, size, size.map(Length::is_finite)); let mut frames = child.layout(ctx, ®ions, styles)?; let header = styles.get(Self::HEADER); @@ -124,7 +126,7 @@ impl PageNode { // Realize header and footer. for frame in &mut frames { let size = frame.size; - let padding = padding.resolve(size); + let padding = padding.resolve(styles).relative_to(size); for (y, h, marginal) in [ (Length::zero(), padding.top, header), (size.y - padding.bottom, padding.bottom, footer), @@ -133,7 +135,7 @@ impl PageNode { let pos = Point::new(padding.left, y); let w = size.x - padding.left - padding.right; let area = Size::new(w, h); - let pod = Regions::one(area, area, area.map(Numeric::is_finite)); + let pod = Regions::one(area, area, area.map(Length::is_finite)); let sub = Layout::layout(&content, ctx, &pod, styles)?.remove(0); Arc::make_mut(frame).push_frame(pos, sub); } diff --git a/src/library/layout/place.rs b/src/library/layout/place.rs index eefa6a9b0..e74776db4 100644 --- a/src/library/layout/place.rs +++ b/src/library/layout/place.rs @@ -8,12 +8,12 @@ pub struct PlaceNode(pub LayoutNode); #[node] impl PlaceNode { fn construct(_: &mut Context, args: &mut Args) -> TypResult { - let tx = args.named("dx")?.unwrap_or_default(); - let ty = args.named("dy")?.unwrap_or_default(); let aligns = args.find()?.unwrap_or(Spec::with_x(Some(RawAlign::Start))); + let dx = args.named("dx")?.unwrap_or_default(); + let dy = args.named("dy")?.unwrap_or_default(); let body: LayoutNode = args.expect("body")?; Ok(Content::block(Self( - body.moved(Point::new(tx, ty)).aligned(aligns), + body.moved(Spec::new(dx, dy)).aligned(aligns), ))) } } diff --git a/src/library/layout/spacing.rs b/src/library/layout/spacing.rs index e9837ef5e..3468af5ec 100644 --- a/src/library/layout/spacing.rs +++ b/src/library/layout/spacing.rs @@ -24,7 +24,7 @@ impl VNode { #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum Spacing { /// Spacing specified in absolute terms and relative to the parent's size. - Relative(Relative), + Relative(Relative), /// Spacing specified as a fraction of the remaining free space in the parent. Fractional(Fraction), } diff --git a/src/library/layout/stack.rs b/src/library/layout/stack.rs index 312757f37..f915c2151 100644 --- a/src/library/layout/stack.rs +++ b/src/library/layout/stack.rs @@ -30,7 +30,7 @@ impl Layout for StackNode { regions: &Regions, styles: StyleChain, ) -> TypResult>> { - let mut layouter = StackLayouter::new(self.dir, regions); + let mut layouter = StackLayouter::new(self.dir, regions, styles); // Spacing to insert before the next node. let mut deferred = None; @@ -85,13 +85,15 @@ castable! { } /// Performs stack layout. -pub struct StackLayouter { +pub struct StackLayouter<'a> { /// The stacking direction. dir: Dir, /// The axis of the stacking direction. axis: SpecAxis, /// The regions to layout children into. regions: Regions, + /// The inherited styles. + styles: StyleChain<'a>, /// Whether the stack itself should expand to fill the region. expand: Spec, /// The full size of the current region that was available at the start. @@ -117,9 +119,9 @@ enum StackItem { Frame(Arc, Align), } -impl StackLayouter { +impl<'a> StackLayouter<'a> { /// Create a new stack layouter. - pub fn new(dir: Dir, regions: &Regions) -> Self { + pub fn new(dir: Dir, regions: &Regions, styles: StyleChain<'a>) -> Self { let axis = dir.axis(); let expand = regions.expand; let full = regions.first; @@ -132,6 +134,7 @@ impl StackLayouter { dir, axis, regions, + styles, expand, full, used: Gen::zero(), @@ -146,7 +149,8 @@ impl StackLayouter { match spacing { Spacing::Relative(v) => { // Resolve the spacing and limit it to the remaining space. - let resolved = v.resolve(self.regions.base.get(self.axis)); + let resolved = + v.resolve(self.styles).relative_to(self.regions.base.get(self.axis)); let remaining = self.regions.first.get_mut(self.axis); let limited = resolved.min(*remaining); *remaining -= limited; @@ -219,7 +223,7 @@ impl StackLayouter { for item in self.items.drain(..) { match item { StackItem::Absolute(v) => cursor += v, - StackItem::Fractional(v) => cursor += v.resolve(self.fr, remaining), + StackItem::Fractional(v) => cursor += v.share(self.fr, remaining), StackItem::Frame(frame, align) => { if self.dir.is_positive() { ruler = ruler.max(align); diff --git a/src/library/mod.rs b/src/library/mod.rs index 358c2204b..a5f0b50c4 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -186,15 +186,3 @@ castable! { Expected: "content", Value::Content(content) => content.pack(), } - -castable! { - Spec>, - Expected: "array of two relative lengths", - Value::Array(array) => { - let mut iter = array.into_iter(); - match (iter.next(), iter.next(), iter.next()) { - (Some(a), Some(b), None) => Spec::new(a.cast()?, b.cast()?), - _ => Err("point array must contain exactly two entries")?, - } - }, -} diff --git a/src/library/prelude.rs b/src/library/prelude.rs index f052a43a7..d74a5d852 100644 --- a/src/library/prelude.rs +++ b/src/library/prelude.rs @@ -10,8 +10,8 @@ pub use typst_macros::node; pub use crate::diag::{with_alternative, At, Error, StrResult, TypError, TypResult}; pub use crate::eval::{ Arg, Args, Array, Cast, Content, Dict, Fold, Func, Key, Layout, LayoutNode, Merge, - Node, RawAlign, Regions, Resolve, Scope, Show, ShowNode, Smart, StyleChain, StyleMap, - StyleVec, Value, + Node, RawAlign, RawLength, Regions, Resolve, Scope, Show, ShowNode, Smart, + StyleChain, StyleMap, StyleVec, Value, }; pub use crate::frame::*; pub use crate::geom::*; diff --git a/src/library/structure/heading.rs b/src/library/structure/heading.rs index 8b1438654..dcf87f908 100644 --- a/src/library/structure/heading.rs +++ b/src/library/structure/heading.rs @@ -1,5 +1,5 @@ use crate::library::prelude::*; -use crate::library::text::{FontFamily, FontSize, TextNode, Toggle}; +use crate::library::text::{FontFamily, TextNode, TextSize, Toggle}; /// A section heading. #[derive(Debug, Hash)] @@ -21,9 +21,9 @@ impl HeadingNode { pub const FILL: Leveled> = Leveled::Value(Smart::Auto); /// The size of text in the heading. #[property(referenced)] - pub const SIZE: Leveled = Leveled::Mapping(|level| { + pub const SIZE: Leveled = Leveled::Mapping(|level| { let upscale = (1.6 - 0.1 * level as f64).max(0.75); - FontSize(Ratio::new(upscale).into()) + TextSize(Em::new(upscale).into()) }); /// Whether text in the heading is strengthend. #[property(referenced)] @@ -36,10 +36,10 @@ impl HeadingNode { pub const UNDERLINE: Leveled = Leveled::Value(false); /// The extra padding above the heading. #[property(referenced)] - pub const ABOVE: Leveled = Leveled::Value(Length::zero()); + pub const ABOVE: Leveled = Leveled::Value(Length::zero().into()); /// The extra padding below the heading. #[property(referenced)] - pub const BELOW: Leveled = Leveled::Value(Length::zero()); + pub const BELOW: Leveled = Leveled::Value(Length::zero().into()); /// Whether the heading is block-level. #[property(referenced)] pub const BLOCK: Leveled = Leveled::Value(true); @@ -95,14 +95,14 @@ impl Show for HeadingNode { let above = resolve!(Self::ABOVE); if !above.is_zero() { - seq.push(Content::Vertical(above.into())); + seq.push(Content::Vertical(above.resolve(styles).into())); } seq.push(body); let below = resolve!(Self::BELOW); if !below.is_zero() { - seq.push(Content::Vertical(below.into())); + seq.push(Content::Vertical(below.resolve(styles).into())); } let mut content = Content::sequence(seq).styled_with_map(map); diff --git a/src/library/structure/list.rs b/src/library/structure/list.rs index c58e8648a..c3eae1af0 100644 --- a/src/library/structure/list.rs +++ b/src/library/structure/list.rs @@ -1,6 +1,6 @@ use crate::library::layout::{GridNode, TrackSizing}; use crate::library::prelude::*; -use crate::library::text::{ParNode, TextNode}; +use crate::library::text::ParNode; use crate::library::utility::Numbering; use crate::parse::Scanner; @@ -34,15 +34,20 @@ impl ListNode { #[property(referenced)] pub const LABEL: Label = Label::Default; /// The spacing between the list items of a non-wide list. - pub const SPACING: Relative = Relative::zero(); + #[property(resolve)] + pub const SPACING: RawLength = RawLength::zero(); /// The indentation of each item's label. - pub const INDENT: Relative = Ratio::new(0.0).into(); + #[property(resolve)] + pub const INDENT: RawLength = RawLength::zero(); /// The space between the label and the body of each item. - pub const BODY_INDENT: Relative = Ratio::new(0.5).into(); + #[property(resolve)] + pub const BODY_INDENT: RawLength = Em::new(0.5).into(); /// The extra padding above the list. - pub const ABOVE: Length = Length::zero(); + #[property(resolve)] + pub const ABOVE: RawLength = RawLength::zero(); /// The extra padding below the list. - pub const BELOW: Length = Length::zero(); + #[property(resolve)] + pub const BELOW: RawLength = RawLength::zero(); fn construct(_: &mut Context, args: &mut Args) -> TypResult { Ok(Content::show(Self { @@ -77,7 +82,6 @@ impl Show for ListNode { number += 1; } - let em = styles.get(TextNode::SIZE); let leading = styles.get(ParNode::LEADING); let spacing = if self.wide { styles.get(ParNode::SPACING) @@ -85,9 +89,9 @@ impl Show for ListNode { styles.get(Self::SPACING) }; - let gutter = (leading + spacing).resolve(em); - let indent = styles.get(Self::INDENT).resolve(em); - let body_indent = styles.get(Self::BODY_INDENT).resolve(em); + let gutter = leading + spacing; + let indent = styles.get(Self::INDENT); + let body_indent = styles.get(Self::BODY_INDENT); Content::block(GridNode { tracks: Spec::with_x(vec![ diff --git a/src/library/structure/table.rs b/src/library/structure/table.rs index e01ae9081..d0ab0716e 100644 --- a/src/library/structure/table.rs +++ b/src/library/structure/table.rs @@ -21,9 +21,10 @@ impl TableNode { /// How to stroke the cells. pub const STROKE: Option = Some(Color::BLACK.into()); /// The stroke's thickness. - pub const THICKNESS: Length = Length::pt(1.0); + #[property(resolve)] + pub const THICKNESS: RawLength = Length::pt(1.0).into(); /// How much to pad the cells's content. - pub const PADDING: Relative = Length::pt(5.0).into(); + pub const PADDING: Relative = Length::pt(5.0).into(); fn construct(_: &mut Context, args: &mut Args) -> TypResult { let columns = args.named("columns")?.unwrap_or_default(); diff --git a/src/library/text/deco.rs b/src/library/text/deco.rs index da1a11418..f5ed47445 100644 --- a/src/library/text/deco.rs +++ b/src/library/text/deco.rs @@ -23,16 +23,16 @@ impl DecoNode { /// Stroke color of the line, defaults to the text color if `None`. #[property(shorthand)] pub const STROKE: Option = None; - /// Thickness of the line's strokes (dependent on scaled font size), read - /// from the font tables if `None`. - #[property(shorthand)] - pub const THICKNESS: Option> = None; - /// Position of the line relative to the baseline (dependent on scaled font - /// size), read from the font tables if `None`. - pub const OFFSET: Option> = None; - /// Amount that the line will be longer or shorter than its associated text - /// (dependent on scaled font size). - pub const EXTENT: Relative = Relative::zero(); + /// Thickness of the line's strokes, read from the font tables if `auto`. + #[property(shorthand, resolve)] + pub const THICKNESS: Smart = Smart::Auto; + /// Position of the line relative to the baseline, read from the font tables + /// if `auto`. + #[property(resolve)] + pub const OFFSET: Smart = Smart::Auto; + /// Amount that the line will be longer or shorter than its associated text. + #[property(resolve)] + pub const EXTENT: RawLength = RawLength::zero(); /// Whether the line skips sections in which it would collide /// with the glyphs. Does not apply to strikethrough. pub const EVADE: bool = true; @@ -66,9 +66,9 @@ impl Show for DecoNode { pub struct Decoration { pub line: DecoLine, pub stroke: Option, - pub thickness: Option>, - pub offset: Option>, - pub extent: Relative, + pub thickness: Smart, + pub offset: Smart, + pub extent: Length, pub evade: bool, } @@ -102,25 +102,18 @@ pub fn decorate( }; let evade = deco.evade && deco.line != STRIKETHROUGH; - let extent = deco.extent.resolve(text.size); - let offset = deco - .offset - .map(|s| s.resolve(text.size)) - .unwrap_or(-metrics.position.resolve(text.size)); + let offset = deco.offset.unwrap_or(-metrics.position.at(text.size)); let stroke = Stroke { paint: deco.stroke.unwrap_or(text.fill), - thickness: deco - .thickness - .map(|s| s.resolve(text.size)) - .unwrap_or(metrics.thickness.resolve(text.size)), + thickness: deco.thickness.unwrap_or(metrics.thickness.at(text.size)), }; let gap_padding = 0.08 * text.size; let min_width = 0.162 * text.size; - let mut start = pos.x - extent; - let end = pos.x + (width + 2.0 * extent); + let mut start = pos.x - deco.extent; + let end = pos.x + (width + 2.0 * deco.extent); let mut push_segment = |from: Length, to: Length| { let origin = Point::new(from, pos.y + offset); @@ -146,20 +139,20 @@ pub fn decorate( let mut intersections = vec![]; for glyph in text.glyphs.iter() { - let dx = glyph.x_offset.resolve(text.size) + x; + let dx = glyph.x_offset.at(text.size) + x; let mut builder = BezPathBuilder::new(face_metrics.units_per_em, text.size, dx.to_raw()); let bbox = face.ttf().outline_glyph(GlyphId(glyph.id), &mut builder); let path = builder.finish(); - x += glyph.x_advance.resolve(text.size); + x += glyph.x_advance.at(text.size); // Only do the costly segments intersection test if the line // intersects the bounding box. if bbox.map_or(false, |bbox| { - let y_min = -face.to_em(bbox.y_max).resolve(text.size); - let y_max = -face.to_em(bbox.y_min).resolve(text.size); + let y_min = -face.to_em(bbox.y_max).at(text.size); + let y_max = -face.to_em(bbox.y_min).at(text.size); offset >= y_min && offset <= y_max }) { @@ -225,7 +218,7 @@ impl BezPathBuilder { } fn s(&self, v: f32) -> f64 { - Em::from_units(v, self.units_per_em).resolve(self.font_size).to_raw() + Em::from_units(v, self.units_per_em).at(self.font_size).to_raw() } } diff --git a/src/library/text/mod.rs b/src/library/text/mod.rs index 4a139fb3d..b5ccc6366 100644 --- a/src/library/text/mod.rs +++ b/src/library/text/mod.rs @@ -16,7 +16,9 @@ use std::borrow::Cow; use ttf_parser::Tag; -use crate::font::{Face, FontStretch, FontStyle, FontWeight, VerticalFontMetric}; +use crate::font::{ + Face, FaceMetrics, FontStretch, FontStyle, FontWeight, VerticalFontMetric, +}; use crate::library::prelude::*; use crate::util::EcoString; @@ -39,23 +41,25 @@ impl TextNode { pub const WEIGHT: FontWeight = FontWeight::REGULAR; /// The width of the glyphs. pub const STRETCH: FontStretch = FontStretch::NORMAL; + /// The size of the glyphs. #[property(shorthand, fold)] - pub const SIZE: FontSize = Length::pt(11.0); + pub const SIZE: TextSize = Length::pt(11.0); /// The glyph fill color. #[property(shorthand)] pub const FILL: Paint = Color::BLACK.into(); - /// The amount of space that should be added between characters. - pub const TRACKING: Em = Em::zero(); - /// The ratio by which spaces should be stretched. - pub const SPACING: Ratio = Ratio::one(); + #[property(resolve)] + pub const TRACKING: RawLength = RawLength::zero(); + /// The width of spaces relative to the default space width. + #[property(resolve)] + pub const SPACING: Relative = Relative::one(); /// Whether glyphs can hang over into the margin. pub const OVERHANG: bool = true; /// The top end of the text bounding box. - pub const TOP_EDGE: VerticalFontMetric = VerticalFontMetric::CapHeight; + pub const TOP_EDGE: TextEdge = TextEdge::Metric(VerticalFontMetric::CapHeight); /// The bottom end of the text bounding box. - pub const BOTTOM_EDGE: VerticalFontMetric = VerticalFontMetric::Baseline; + pub const BOTTOM_EDGE: TextEdge = TextEdge::Metric(VerticalFontMetric::Baseline); /// Whether to apply kerning ("kern"). pub const KERNING: bool = true; @@ -188,44 +192,53 @@ castable! { /// The size of text. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct FontSize(pub Relative); +pub struct TextSize(pub RawLength); -impl Fold for FontSize { +impl Fold for TextSize { type Output = Length; fn fold(self, outer: Self::Output) -> Self::Output { - self.0.rel.resolve(outer) + self.0.abs + self.0.em.at(outer) + self.0.length } } castable! { - FontSize, - Expected: "relative length", - Value::Length(v) => Self(v.into()), - Value::Ratio(v) => Self(v.into()), - Value::Relative(v) => Self(v), + TextSize, + Expected: "length", + Value::Length(v) => Self(v), +} + +/// Specifies the bottom or top edge of text. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum TextEdge { + /// An edge specified using one of the well-known font metrics. + Metric(VerticalFontMetric), + /// An edge specified as a length. + Length(RawLength), +} + +impl TextEdge { + /// Resolve the value of the text edge given a font face. + pub fn resolve(self, styles: StyleChain, metrics: &FaceMetrics) -> Length { + match self { + Self::Metric(metric) => metrics.vertical(metric).resolve(styles), + Self::Length(length) => length.resolve(styles), + } + } } castable! { - Em, - Expected: "float", - Value::Float(v) => Self::new(v), -} - -castable! { - VerticalFontMetric, - Expected: "string or relative length", - Value::Length(v) => Self::Relative(v.into()), - Value::Ratio(v) => Self::Relative(v.into()), - Value::Relative(v) => Self::Relative(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, + TextEdge, + Expected: "string or length", + Value::Length(v) => Self::Length(v), + Value::Str(string) => Self::Metric(match string.as_str() { + "ascender" => VerticalFontMetric::Ascender, + "cap-height" => VerticalFontMetric::CapHeight, + "x-height" => VerticalFontMetric::XHeight, + "baseline" => VerticalFontMetric::Baseline, + "descender" => VerticalFontMetric::Descender, _ => Err("unknown font metric")?, - }, + }), } /// A stylistic set in a font face. diff --git a/src/library/text/par.rs b/src/library/text/par.rs index dc7c9dcfd..57e2b45d8 100644 --- a/src/library/text/par.rs +++ b/src/library/text/par.rs @@ -42,12 +42,15 @@ impl ParNode { /// Whether to hyphenate text to improve line breaking. When `auto`, words /// will will be hyphenated if and only if justification is enabled. pub const HYPHENATE: Smart = Smart::Auto; - /// The spacing between lines (dependent on scaled font size). - pub const LEADING: Relative = Ratio::new(0.65).into(); - /// The extra spacing between paragraphs (dependent on scaled font size). - pub const SPACING: Relative = Ratio::new(0.55).into(); + /// The spacing between lines. + #[property(resolve)] + pub const LEADING: RawLength = Em::new(0.65).into(); + /// The extra spacing between paragraphs. + #[property(resolve)] + pub const SPACING: RawLength = Em::new(0.55).into(); /// The indent the first line of a consecutive paragraph should have. - pub const INDENT: Relative = Relative::zero(); + #[property(resolve)] + pub const INDENT: RawLength = RawLength::zero(); fn construct(_: &mut Context, args: &mut Args) -> TypResult { // The paragraph constructor is special: It doesn't create a paragraph @@ -370,7 +373,7 @@ fn prepare<'a>( } ParChild::Spacing(spacing) => match *spacing { Spacing::Relative(v) => { - let resolved = v.resolve(regions.base.x); + let resolved = v.resolve(styles).relative_to(regions.base.x); items.push(ParItem::Absolute(resolved)); ranges.push(range); } @@ -772,8 +775,7 @@ fn stack( regions: &Regions, styles: StyleChain, ) -> Vec> { - let em = styles.get(TextNode::SIZE); - let leading = styles.get(ParNode::LEADING).resolve(em); + let leading = styles.get(ParNode::LEADING); let align = styles.get(ParNode::ALIGN); let justify = styles.get(ParNode::JUSTIFY); @@ -837,7 +839,7 @@ fn commit( if text.styles.get(TextNode::OVERHANG) { let start = text.dir.is_positive(); let em = text.styles.get(TextNode::SIZE); - let amount = overhang(glyph.c, start) * glyph.x_advance.resolve(em); + let amount = overhang(glyph.c, start) * glyph.x_advance.at(em); offset -= amount; remaining += amount; } @@ -852,7 +854,7 @@ fn commit( { let start = !text.dir.is_positive(); let em = text.styles.get(TextNode::SIZE); - let amount = overhang(glyph.c, start) * glyph.x_advance.resolve(em); + let amount = overhang(glyph.c, start) * glyph.x_advance.at(em); remaining += amount; } } @@ -887,7 +889,7 @@ fn commit( match item { ParItem::Absolute(v) => offset += *v, - ParItem::Fractional(v) => offset += v.resolve(line.fr, remaining), + ParItem::Fractional(v) => offset += v.share(line.fr, remaining), ParItem::Text(shaped) => position(shaped.build(fonts, justification)), ParItem::Frame(frame) => position(frame.clone()), } diff --git a/src/library/text/shaping.rs b/src/library/text/shaping.rs index d398e56de..32177f0a6 100644 --- a/src/library/text/shaping.rs +++ b/src/library/text/shaping.rs @@ -132,7 +132,7 @@ impl<'a> ShapedText<'a> { .filter(|g| g.is_justifiable()) .map(|g| g.x_advance) .sum::() - .resolve(self.styles.get(TextNode::SIZE)) + .at(self.styles.get(TextNode::SIZE)) } /// Reshape a range of the shaped text, reusing information from this @@ -168,7 +168,7 @@ impl<'a> ShapedText<'a> { let glyph_id = ttf.glyph_index('-')?; let x_advance = face.to_em(ttf.glyph_hor_advance(glyph_id)?); let cluster = self.glyphs.last().map(|g| g.cluster).unwrap_or_default(); - self.size.x += x_advance.resolve(size); + self.size.x += x_advance.at(size); self.glyphs.to_mut().push(ShapedGlyph { face_id, glyph_id: glyph_id.0, @@ -443,8 +443,10 @@ fn shape_tofus(ctx: &mut ShapingContext, base: usize, text: &str, face_id: FaceI /// Apply tracking and spacing to a slice of shaped glyphs. fn track_and_space(ctx: &mut ShapingContext) { - let tracking = ctx.styles.get(TextNode::TRACKING); - let spacing = ctx.styles.get(TextNode::SPACING); + let em = ctx.styles.get(TextNode::SIZE); + let tracking = Em::from_length(ctx.styles.get(TextNode::TRACKING), em); + let spacing = ctx.styles.get(TextNode::SPACING).map(|abs| Em::from_length(abs, em)); + if tracking.is_zero() && spacing.is_one() { return; } @@ -452,7 +454,7 @@ fn track_and_space(ctx: &mut ShapingContext) { let mut glyphs = ctx.glyphs.iter_mut().peekable(); while let Some(glyph) = glyphs.next() { if glyph.is_space() { - glyph.x_advance *= spacing.get(); + glyph.x_advance = spacing.relative_to(glyph.x_advance); } if glyphs.peek().map_or(false, |next| glyph.cluster != next.cluster) { @@ -479,8 +481,8 @@ fn measure( // Expand top and bottom by reading the face's vertical metrics. let mut expand = |face: &Face| { let metrics = face.metrics(); - top.set_max(metrics.vertical(top_edge, size)); - bottom.set_max(-metrics.vertical(bottom_edge, size)); + top.set_max(top_edge.resolve(styles, metrics)); + bottom.set_max(-bottom_edge.resolve(styles, metrics)); }; if glyphs.is_empty() { @@ -499,7 +501,7 @@ fn measure( expand(face); for glyph in group { - width += glyph.x_advance.resolve(size); + width += glyph.x_advance.at(size); } } } diff --git a/src/library/utility/math.rs b/src/library/utility/math.rs index 272ececa6..63ec5e555 100644 --- a/src/library/utility/math.rs +++ b/src/library/utility/math.rs @@ -37,12 +37,11 @@ pub fn abs(_: &mut Context, args: &mut Args) -> TypResult { Ok(match v { Value::Int(v) => Value::Int(v.abs()), Value::Float(v) => Value::Float(v.abs()), - Value::Length(v) => Value::Length(v.abs()), Value::Angle(v) => Value::Angle(v.abs()), Value::Ratio(v) => Value::Ratio(v.abs()), Value::Fraction(v) => Value::Fraction(v.abs()), - Value::Relative(_) => { - bail!(span, "cannot take absolute value of a relative length") + Value::Length(_) | Value::Relative(_) => { + bail!(span, "cannot take absolute value of a length") } v => bail!(span, "expected numeric value, found {}", v.type_name()), }) diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 0e68c0d17..88d0c244a 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -478,10 +478,7 @@ fn literal(p: &mut Parser) -> bool { | NodeKind::Int(_) | NodeKind::Float(_) | NodeKind::Bool(_) - | NodeKind::Fraction(_) - | NodeKind::Length(_, _) - | NodeKind::Angle(_, _) - | NodeKind::Percentage(_) + | NodeKind::Numeric(_, _) | NodeKind::Str(_), ) => { p.eat(); diff --git a/src/parse/tokens.rs b/src/parse/tokens.rs index 0c05d7707..40ea134e5 100644 --- a/src/parse/tokens.rs +++ b/src/parse/tokens.rs @@ -4,8 +4,8 @@ use super::{ is_id_continue, is_id_start, is_newline, resolve_hex, resolve_raw, resolve_string, Scanner, }; -use crate::geom::{AngularUnit, LengthUnit}; -use crate::syntax::ast::{MathNode, RawNode}; +use crate::geom::{AngleUnit, LengthUnit}; +use crate::syntax::ast::{MathNode, RawNode, Unit}; use crate::syntax::{ErrorPos, NodeKind}; use crate::util::EcoString; @@ -462,7 +462,8 @@ impl<'s> Tokens<'s> { } // Read the exponent. - if self.s.eat_if('e') || self.s.eat_if('E') { + let em = self.s.rest().starts_with("em"); + if !em && self.s.eat_if('e') || self.s.eat_if('E') { let _ = self.s.eat_if('+') || self.s.eat_if('-'); self.s.eat_while(|c| c.is_ascii_digit()); } @@ -487,14 +488,15 @@ impl<'s> Tokens<'s> { if let Ok(f) = number.parse::() { match suffix { "" => NodeKind::Float(f), - "%" => NodeKind::Percentage(f), - "fr" => NodeKind::Fraction(f), - "pt" => NodeKind::Length(f, LengthUnit::Pt), - "mm" => NodeKind::Length(f, LengthUnit::Mm), - "cm" => NodeKind::Length(f, LengthUnit::Cm), - "in" => NodeKind::Length(f, LengthUnit::In), - "deg" => NodeKind::Angle(f, AngularUnit::Deg), - "rad" => NodeKind::Angle(f, AngularUnit::Rad), + "pt" => NodeKind::Numeric(f, Unit::Length(LengthUnit::Pt)), + "mm" => NodeKind::Numeric(f, Unit::Length(LengthUnit::Mm)), + "cm" => NodeKind::Numeric(f, Unit::Length(LengthUnit::Cm)), + "in" => NodeKind::Numeric(f, Unit::Length(LengthUnit::In)), + "deg" => NodeKind::Numeric(f, Unit::Angle(AngleUnit::Deg)), + "rad" => NodeKind::Numeric(f, Unit::Angle(AngleUnit::Rad)), + "em" => NodeKind::Numeric(f, Unit::Em), + "fr" => NodeKind::Numeric(f, Unit::Fr), + "%" => NodeKind::Numeric(f, Unit::Percent), _ => NodeKind::Unknown(all.into()), } } else { @@ -1017,19 +1019,20 @@ mod tests { // Combined integers and floats. let nums = ints.iter().map(|&(k, v)| (k, v as f64)).chain(floats); - let suffixes = [ - ("%", Percentage as fn(f64) -> NodeKind), - ("fr", Fraction as fn(f64) -> NodeKind), - ("mm", |x| Length(x, LengthUnit::Mm)), - ("pt", |x| Length(x, LengthUnit::Pt)), - ("cm", |x| Length(x, LengthUnit::Cm)), - ("in", |x| Length(x, LengthUnit::In)), - ("rad", |x| Angle(x, AngularUnit::Rad)), - ("deg", |x| Angle(x, AngularUnit::Deg)), + let suffixes: &[(&str, fn(f64) -> NodeKind)] = &[ + ("mm", |x| Numeric(x, Unit::Length(LengthUnit::Mm))), + ("pt", |x| Numeric(x, Unit::Length(LengthUnit::Pt))), + ("cm", |x| Numeric(x, Unit::Length(LengthUnit::Cm))), + ("in", |x| Numeric(x, Unit::Length(LengthUnit::In))), + ("rad", |x| Numeric(x, Unit::Angle(AngleUnit::Rad))), + ("deg", |x| Numeric(x, Unit::Angle(AngleUnit::Deg))), + ("em", |x| Numeric(x, Unit::Em)), + ("fr", |x| Numeric(x, Unit::Fr)), + ("%", |x| Numeric(x, Unit::Percent)), ]; // Numeric types. - for &(suffix, build) in &suffixes { + for &(suffix, build) in suffixes { for (s, v) in nums.clone() { t!(Code[" /"]: format!("{}{}", s, suffix) => build(v)); } @@ -1112,6 +1115,6 @@ mod tests { // Test invalid number suffixes. t!(Code[" /"]: "1foo" => Invalid("1foo")); t!(Code: "1p%" => Invalid("1p"), Invalid("%")); - t!(Code: "1%%" => Percentage(1.0), Invalid("%")); + t!(Code: "1%%" => Numeric(1.0, Unit::Percent), Invalid("%")); } } diff --git a/src/syntax/ast.rs b/src/syntax/ast.rs index cb0a99b9d..97ab055f7 100644 --- a/src/syntax/ast.rs +++ b/src/syntax/ast.rs @@ -5,7 +5,7 @@ use std::ops::Deref; use super::{Green, GreenData, NodeKind, RedNode, RedRef, Span}; -use crate::geom::{AngularUnit, LengthUnit}; +use crate::geom::{AngleUnit, LengthUnit}; use crate::util::EcoString; /// A typed AST node. @@ -352,10 +352,7 @@ node! { | NodeKind::Bool(_) | NodeKind::Int(_) | NodeKind::Float(_) - | NodeKind::Length(_, _) - | NodeKind::Angle(_, _) - | NodeKind::Percentage(_) - | NodeKind::Fraction(_) + | NodeKind::Numeric(_, _) | NodeKind::Str(_) } @@ -368,10 +365,7 @@ impl Lit { NodeKind::Bool(v) => LitKind::Bool(v), NodeKind::Int(v) => LitKind::Int(v), NodeKind::Float(v) => LitKind::Float(v), - NodeKind::Length(v, unit) => LitKind::Length(v, unit), - NodeKind::Angle(v, unit) => LitKind::Angle(v, unit), - NodeKind::Percentage(v) => LitKind::Percent(v), - NodeKind::Fraction(v) => LitKind::Fractional(v), + NodeKind::Numeric(v, unit) => LitKind::Numeric(v, unit), NodeKind::Str(ref v) => LitKind::Str(v.clone()), _ => panic!("literal is of wrong kind"), } @@ -391,21 +385,27 @@ pub enum LitKind { Int(i64), /// A floating-point literal: `1.2`, `10e-4`. Float(f64), - /// A length literal: `12pt`, `3cm`. - Length(f64, LengthUnit), - /// An angle literal: `1.5rad`, `90deg`. - Angle(f64, AngularUnit), - /// A percent literal: `50%`. - /// - /// _Note_: `50%` is stored as `50.0` here, but as `0.5` in the - /// corresponding [value](crate::geom::Relative). - Percent(f64), - /// A fraction unit literal: `1fr`. - Fractional(f64), + /// A numeric literal with a unit: `12pt`, `3cm`, `2em`, `90deg`, `50%`. + Numeric(f64, Unit), /// A string literal: `"hello!"`. Str(EcoString), } +/// Unit of a numeric value. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum Unit { + /// An absolute length unit. + Length(LengthUnit), + /// An angular unit. + Angle(AngleUnit), + /// Font-relative: `1em` is the same as the font size. + Em, + /// Fractions: `fr`. + Fr, + /// Percentage: `%`. + Percent, +} + node! { /// A code block: `{ let x = 1; x + 2 }`. CodeBlock: CodeBlock diff --git a/src/syntax/highlight.rs b/src/syntax/highlight.rs index bad434b98..b0486c7bc 100644 --- a/src/syntax/highlight.rs +++ b/src/syntax/highlight.rs @@ -187,10 +187,7 @@ impl Category { NodeKind::Bool(_) => Some(Category::Bool), NodeKind::Int(_) => Some(Category::Number), NodeKind::Float(_) => Some(Category::Number), - NodeKind::Length(_, _) => Some(Category::Number), - NodeKind::Angle(_, _) => Some(Category::Number), - NodeKind::Percentage(_) => Some(Category::Number), - NodeKind::Fraction(_) => Some(Category::Number), + NodeKind::Numeric(_, _) => Some(Category::Number), NodeKind::Str(_) => Some(Category::String), NodeKind::Error(_, _) => Some(Category::Invalid), NodeKind::Unknown(_) => Some(Category::Invalid), diff --git a/src/syntax/mod.rs b/src/syntax/mod.rs index d0920d203..b4908ff22 100644 --- a/src/syntax/mod.rs +++ b/src/syntax/mod.rs @@ -12,9 +12,8 @@ use std::sync::Arc; pub use highlight::*; pub use span::*; -use self::ast::{MathNode, RawNode, TypedNode}; +use self::ast::{MathNode, RawNode, TypedNode, Unit}; use crate::diag::Error; -use crate::geom::{AngularUnit, LengthUnit}; use crate::parse::TokenMode; use crate::source::SourceId; use crate::util::EcoString; @@ -629,17 +628,8 @@ pub enum NodeKind { Int(i64), /// A floating-point number: `1.2`, `10e-4`. Float(f64), - /// A length: `12pt`, `3cm`. - Length(f64, LengthUnit), - /// An angle: `90deg`. - Angle(f64, AngularUnit), - /// A percentage: `50%`. - /// - /// _Note_: `50%` is stored as `50.0` here, as in the corresponding - /// [literal](ast::LitKind::Percent). - Percentage(f64), - /// A fraction unit: `3fr`. - Fraction(f64), + /// A numeric value with a unit: `12pt`, `3cm`, `2em`, `90deg`, `50%`. + Numeric(f64, Unit), /// A quoted string: `"..."`. Str(EcoString), /// A code block: `{ let x = 1; x + 2 }`. @@ -886,10 +876,7 @@ impl NodeKind { Self::Bool(_) => "boolean", Self::Int(_) => "integer", Self::Float(_) => "float", - Self::Length(_, _) => "length", - Self::Angle(_, _) => "angle", - Self::Percentage(_) => "percentage", - Self::Fraction(_) => "`fr` value", + Self::Numeric(_, _) => "numeric value", Self::Str(_) => "string", Self::CodeBlock => "code block", Self::ContentBlock => "content block", @@ -1010,10 +997,7 @@ impl Hash for NodeKind { Self::Bool(v) => v.hash(state), Self::Int(v) => v.hash(state), Self::Float(v) => v.to_bits().hash(state), - Self::Length(v, u) => (v.to_bits(), u).hash(state), - Self::Angle(v, u) => (v.to_bits(), u).hash(state), - Self::Percentage(v) => v.to_bits().hash(state), - Self::Fraction(v) => v.to_bits().hash(state), + Self::Numeric(v, u) => (v.to_bits(), u).hash(state), Self::Str(v) => v.hash(state), Self::CodeBlock => {} Self::ContentBlock => {} diff --git a/tests/ref/text/edge.png b/tests/ref/text/edge.png new file mode 100644 index 0000000000000000000000000000000000000000..0514d628eb8c762b052ec95e9d81ee1c3865170f GIT binary patch literal 15857 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 delta 1186 zcmV;T1YP^=4yg%{7k_C800000`F;ZC000DVNkl=Ln3m>Wl^bm$lFS)q66>}v!p`5$Q)WF@Qg_sB1Uq-V z5jGt`WorGX=?E&bPnv>&FsnScL}z|Mj((;)el}}QXw#bj#yYCSZTt77sQvk}f@tER?m1O!x5 zU^v2lt4j5GV~P=mCaG$V*$8+@Rh5P#EK}7Y!x5fWRleZ}6{^ZM9KkwW;=cuf`LBtD zFqR;~FB1jf*Vi7N#xshl&&P|v%pu2G)9Pz;F;ftLv42;rcZAHGyFFe6<`d2}$BXID z6a-vZMU+$4@ae{-Ok5o=0*BoQ{uMeaE@TP|D507M%*4R!ar_=6PY#>0QBY1 z%pv>sv8go2D*~`qRe6{Q2y2OU>f6{nzuPMUu-g4zI#UsV0-C9hj=rdv8q;0 z1cc*6fqz)te<0kcs!f@O0GvLc(tcWR1I?~Xn(@~^q^kZ zO0+96gwIryHRKVY&VGHf(gC3%6bf~|O67Xx9ip$Vhv5DgP6v2gH4EWJpgBIBW(Pu3 zNabicAhcRPXX_&yUQZO^Fsr87jleq0yv2d=Lw~x=ghq}cEaOY|%3BE{M4nW|EmzzK z%;oEJI?@!u-V^RA)GPHwdlNyZu?qJUMuiY-CZr1j8mTrI41TpH@1S1UNz@SUU zyU=|gh=ss>zh6#U|j-h0(!yx6kK$AUEp+-2oUWWBb8*JAdE> z;r`05Sg*-(#>oA#5OCQ4$Job@BokpvFMvvQjucuAUkO*gd&85%(l1VIo4K@bE%5ClOG z1VIo4K@bE%5ClOGg#QacB9TZW5*vv`A{jsb0_L%H4?jNV6#xJL07*qoM6N<$f@F*y AB>(^b diff --git a/tests/ref/text/font.png b/tests/ref/text/font.png index 1c1a71eef1c868c653fe2f463156eb735cd2bba5..6725137112e681724cc827f500dab39a74379970 100644 GIT binary patch literal 17334 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

Rn3g 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 WDMF0Oh=gldwz9m&g9=&m(EkV9U@Q6n diff --git a/tests/ref/text/tracking-spacing.png b/tests/ref/text/tracking-spacing.png new file mode 100644 index 0000000000000000000000000000000000000000..ec130c993ca86342e2795742a800a845471586c8 GIT binary patch literal 6474 zcmai(1yCDIyYGXQ;GvY_R)ONs;_gr=?hq(W@f6n(N`MO1QoKc9T!IHD#ajv#3GP}F zTmv-a^gZW(_ue`4e&_sVcXnoXXLok?+2{H1?i&L=O)7F`asU88^;ApE2ml};{*zmQ ze<}b&g{~6-02A#~HD#0FxxK{@u!-pn>gYtKX9SS{ib5#4E%iFh@OTp8uqAjCW%+5lqokeS}SeW0ot-%CNF7cv%uD8_cw=bMT!n-0T zTefrj?3WhK;>i(3ykM~A$$CeTV8f=#3p_WKZWdg$E*Z9DfAJUT-(2UJJs4X~{vFGG^$ zb~c=u7uNy+ni%YG%Dehi=5ji*a1_lI7U>l0aSCCjZGxd<&5 zoPl8-K|6fTllA7X#l~=EvkCd#HqH8efIoFy2(;Bb4|<<;#N@=y#B4d&TeeDWdydC2 z0$*cvxARogl={U<0aJ$aaTUu?agx}O9Kww1i{E%4#pyjQw-&;s${_Q_==fwe$v^a(%I)wZBjYFN@Eg@X{fZ+i(PpzI<1R>F{5B6Ru z5l+?S#sag|5w}h8(4i?SW4QzUhX?gWF3qf0Q0{b&Q-!Yt^0X^a8pu@$&fs}%y6*^~ z$xpyCrsGG6ZURA#E{}|i4G*P87oUS>Aad2v5Q?Ir9c4u1yi4H zXZ7D|DrZ{xv$O=_;6<#IlI)X1QqZv z4?*rJe6gyK;k}Ajhpg~Klsf7MS57E&3@>fC@4bH4-&rV3Bw+BS9h;dN*@O6qoWcSr z(UE{>i?<33~M3Jfz#Co_+lmF5a~qlp#K`V z7}KX@X8NSPdX(__PYcv-oU&Tb3J%lWMPR=K_ub^4xu}1HUqD(b9u+k7I- zN{38@bj^B7Ec-miiK%%kBmL}#7Ol*!abZ;kcI_?qmF}NB3#%w{xz8LW-WoqHZcbpK z`Ejase0#*}V&bN55l%%uReZ}D_DS+rqtFwQJo6gqxyDiE6P5YJU~7FF@gp?IuA%E3 zx^Y_imUr}U@K$%CJ=t(ur*!jnREYyTzok!{Z*OIM@D#4Sv zoTugMzk&2Ol&hvlWvRz!XSMTy!~s@CF`VMW9ufI35il~r5}1&^F}yE7snCg=7*wc_ zUyZh*<2QZ%rnW|!8+&}Av+^Bhi{IWR_zw09*~Qk6CZBA2s3I9i;mrWQ#c}o?*rt1^ zMHMHyxB7<$e-&1Fo%m)H zta=#?7mD22OrEn{(s`$#QfH2&tQ^BwTU4`aJrvf~twq$${KhM;bwzTtG3HM zJF%t-!uI#D07^p|U2A0S^npB4{p2H*tUvvC^G1!$mm&?JW17L8l_Z>X+ydr)Fo7fn z3QiM}V12d?Z4?XO4PO(Wn;W*E190XbZI>mQq#h)?!knnH=et`2Zeb_{%!dfdoA_Wq zh*$W}D6%Kb@4oB%P`o4rgG{w_$h?OI(ZJ5_0d!{v>TFyC>(bXp9kWWMbe4d8(Q}0P z7Lzpw%-9`6!we4%^Wae_5rLJ32EAXVd*%7OG+~O==k^s_wqON2-BgXFmz!RF9N1oU{o?K0-4UHCLAAER|H8Lb-tR zr;IDFBJ+WocU|e7;h(O8!kay{)qZ}((pGW_7Iuxs)qJ_gVbZxK5f|Gpv3=5N`W8^bWh9%+pa68<@9fDW=R(2Cg7!&t479KU^ZnIV zl*5Sj(feX)msyLyj$E%?U`c-&JeQ|FD1H~2vNLjOFZxT zWAxKz_>)mh=)l}STE*fU$N@q18Lx<;)VFXD)GZYC=EuIs_l6=V(q@BDr@IXIBlM*M zk7S{-Y*92a5~9YM+wXnN2TiY!Ddp)$8cLH1veDg<#P;_OtJulFk2`WW>~BGav9sY3 zvP+*MtsT*~V79i2i`O(T^()O|Q~$G|2kDePB+2bVoiC`cgf%u1=A`_&2r3cmxpbT{pE@dyC_&Sfe}*r;$tCx?3^&T-$dj<9{jkGY7X zv`12LRT^^vө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 fD*vJ&8K)S1Sqv!tSyuM2n zdS3a>bEnngOW15{VQgyxRPUQug1Nvmz1&wO)sEA;u|=X9jQ%(n zU*&JvQeMa!kelA>zUkM0`DXt$#LC|&I-ojpFGxjb4njCp1s;GC-u~ADQKb@li_n+< zo$;n4Dy2<*n;j8bz$n}J8Y8*TdT99C!%_@JB8n*dX357FOI(;^Gc9l8-JHFj1fl;! zsF{8s0fd~#Ny&7Sr;4(%uz~?){O=7(F?08jJ$zwBq+aT}bEg>Kn?`?f1;%b3 z6C03M$9~yQh)jns?TuN8{j!5IN2{eMVRGm~@C@O-E6hokK%0+K58S#rV|dG=L&`BI zNve=wkQ{6vWTs}x)sWY;9DrLJnv07EO@kRmd>+~&&yVGB_x%E`Y|=}Xe;rY4c|&zk z+74ry?D4g%@Kqg6q7KWQW)XFn=eBgj359)W&~y5$mrxuo?e$FTprxL?vlClhl%fDQVl^iu-`RIJZyYHS< zVYWiy-HeCn=iBX4?%E7FO-%9KcUfk;oFd3cZ&%m%(mJ&#&3!ZrguuMU({{xuO*iRE7K}N*&f;b(4Pn?@xFx$IHUbll%b|2fgto&zJSTs* zRGhlghd>cX=jcVTe?2&JvxW!`?w}Ba!;JMQINTL!2fu)SGE^FbQT1g~y3oy@-RZbu z9bY$-7F{;;Y{<6g;^8}?h|6`GUvX@M=FQ}`8-dAj z(N)FgE_~zg$x`2;S6ORdxYn8NOWpfYNFx7Jej8kpL00#OkgqaaJ*;%^GrWf4=kI>% zK)8L*+(DwNGJl8d8Y1}Ph7Cqn;Q+ofd|UEHoL@8~7Q*m{%`5n;g^j5>URQ+C5l-$9 zGixC*{HJH$vwIVZOxm&rXmSNxe@Xk6qyXn0SUerVwKn{T-krjy>mDV8SsS zYeZ89T&fAL7^XxWSwLL%>k+&rogldu7>9^B<(f?%IbqcOf+YdAG`+-9;&LyR?otTVmLnrMJcNaC0?Zlzv z<8*sREM|Ppto)ltc*|Ag>AI@y9)?{CJ+!+%r-+3b=3T&X|DT`)xYj3<>T~^QnM+?~ z&%JC5!X5meMipjhgIyI@BzKG;9{RA9;slRHfK!xpt2j=z5<+c)G?OoggAWFTM`ioeD_#@Y6kf=0?F_gi|fX z5me7%i6FyTmZ|l9Qu-@wF}7jw?Z~40AkNrVNsHcs=}kx>+;BeaGWUKJ+|NwZhg>S} z44D7LLv+M8P~bv;#&?9(|KgY#wyH_Z%r&5N=F|j4ZoXIcL~CV*5qXIEm55WC1)0;- zLK;54iC;rn(iXm`AvSDPjjtazT35O#3B9fm)1xSV%MlN1W_&o zj;GhucVAueJHnG|-Gm~` zZ7&S|(V!*ibdaKeYCAic_*TteD_s3d`6P9~i&b<)s+8aC$v%{C}M!vqET(@$^ z=}4I#@@2E&{9;xoXssipLOx#|nTenhdhZT7y{(n$rZYc6*f{+vPRJvBt})y-(G?Px z&|EL)wf6bk^F3<(!f zT%c2XzDvAsWlGt5!Y>!LFhE*>JPjJz$!RKW7QM^R$DoNmaM3pB%mmll@?fRy7fsgR zE^O0-9nejkK~>y@x-v=xGJMgIw(-;M`WduyPE_a~SD9^EAS)7(4+Qn5YCV%j(%mwrc#jadnUSC-B}g&5iW!Maw;Mw$G`%_xUZScm}A z81K@xfAJ`5Q*}?jicenu0y}_dYk*dY5UxVSeWJKa?Os6IBh7|C=Y=p<|{0Wexw%@-v@k zn^zs33#hp7w*Ig&FI}KWW@$}hwHan3ysVh9#czOSXNYih3KIv1BU${w#EiWGD5?OO zsOhqCQQDs|D?{;7MuS2;?U&^l#6E5>B}UAziFW<)<=yrca}79Ww!iCUH*qyhHJtM7 zuQ#qdL4{3p-=+OX(_iJa)OAyAw2bk2%dR>!j6pk6LXt%6vPv`%wJS@dGnzDFcD{Ti zwrG{8`z#Ij*h!q6x-a{@b~W0k?UUk&>t=3ELoiG3dpF8}Jl7HGfW|mo|14oHLJ5hsx5&TC4G8LzTh6HTgBt3WM1o>nSN-@r6@Ih zq00OWvJNv$J|`k-m~9yt`F#8HY>>)d&#_PovKD;ii+$Ngqv)j+>w&#AkI<*_=y>y2 zA06sqL-`>(1YtSqZYjE2ElXfR^04+2X)*8w*Y;@uXw^K}udU_3BP#1)v9er%(VLrO zsK z!TEA%c!dH{$f8q&uTl>^)^{99Ce?y$#gynF-tMhbW(zPXbON^V2Gs;cbSf2&#J8aISc=_DQ?p+I=RfN^BqD$T2H@#x24E*V1q`2a9vxON zAD}q=y`eZU?p@;zrKy#CVhQ#oU5x3xcCN=C3xzc#jn&U-T+FAb`mh(E^9FDA0Z+D* z2&K=f_1Zj&rbEV@@QsjrWD@rul$Q4~G!Sht@M7PCwQa92AHAXL-1EzAU)CyRhGtbw1qqb;dL$!)N9|{5b z$#{hvk)Wch6ak0;xTh^r1Ug<`7xruA4fZQ(yoa}IUMt3qT@buG>E3tKT112mVir~v zM$c6wY~~@V#@SKwdF~Dth$@fQ9Dn$=H=KWfK zyvL@ODNiQcGXrfMsHn>w^oG-AfC)i<#;hnfn&B2;K$bN-H$F1K=)Pwz>Pt{ypv6BHPjL5zZ`WIYNJd)x5HI3bdtaK5RF{W5WIVR6w+Gj_6r@-D{T zX>kn;yz4j7HT37AM~Q=yaOboRsuNxBn1Z;OW)XjAJ6t>Gw(P{;(r0=-Jb=YrTfDxj z>MVZf+d6?A;91qVTcj$An2Nog$z1pCq6i4ud{55`Xf_`!5{ZqGy#y@+T_y6q0{CL7NA|hg)kFx*M=;~+~sFx!g GBK{B2LO3S? diff --git a/tests/typ/code/ops.typ b/tests/typ/code/ops.typ index b5a4d8d46..53cf488e5 100644 --- a/tests/typ/code/ops.typ +++ b/tests/typ/code/ops.typ @@ -63,8 +63,8 @@ test(v + v, 2.0 * v) } - // Relative lengths cannot be divided by themselves. - if type(v) != "relative length" { + // Lengths cannot be divided by themselves. + if "length" not in type(v) { test(v / v, 1.0) test(v / v == 1, true) } diff --git a/tests/typ/layout/page-marginals.typ b/tests/typ/layout/page-marginals.typ index 2d9696474..9fd193c62 100644 --- a/tests/typ/layout/page-marginals.typ +++ b/tests/typ/layout/page-marginals.typ @@ -5,7 +5,7 @@ header: align(horizon, { text(eastern)[*Typst*] h(1fr) - text(80%)[_Chapter 1_] + text(0.8em)[_Chapter 1_] }), footer: page => v(5pt) + align(center)[\~ #page \~], ) diff --git a/tests/typ/style/construct.typ b/tests/typ/style/construct.typ index 8bc348a94..f01b534b4 100644 --- a/tests/typ/style/construct.typ +++ b/tests/typ/style/construct.typ @@ -22,7 +22,7 @@ --- // The inner rectangle should also be yellow here. // (and therefore invisible) -[#set rect(fill: yellow);#text(100%, rect(padding: 5pt, rect()))] +[#set rect(fill: yellow);#text(1em, rect(padding: 5pt, rect()))] --- // The inner rectangle should not be yellow here. @@ -30,4 +30,4 @@ A #rect(fill: yellow, padding: 5pt, rect()) B --- // The inner list should not be indented extra. -[#set text(100%);#list(indent: 20pt, list[A])] +[#set text(1em);#list(indent: 20pt, list[A])] diff --git a/tests/typ/style/show.typ b/tests/typ/style/show.typ index 2e003b0a8..7a5aba8f1 100644 --- a/tests/typ/style/show.typ +++ b/tests/typ/style/show.typ @@ -3,11 +3,11 @@ #set page("a8", footer: p => v(-5pt) + align(right, [#p])) #let i = 1 -#set heading(size: 100%) +#set heading(size: 1em) #show heading(level, body) as { if level == 1 { v(10pt) - underline(text(150%, blue)[{i}. #body]) + underline(text(1.5em, blue)[{i}. #body]) i += 1 } else { text(red, body) @@ -29,7 +29,7 @@ Some more text. Another text. --- -#set heading(size: 100%, strong: false, block: false) +#set heading(size: 1em, strong: false, block: false) #show heading(a, b) as [B] A [= Heading] C diff --git a/tests/typ/style/wrap.typ b/tests/typ/style/wrap.typ index b549f7d07..2a9074cb0 100644 --- a/tests/typ/style/wrap.typ +++ b/tests/typ/style/wrap.typ @@ -2,10 +2,10 @@ --- #set page(height: 130pt) -#set text(70%) +#set text(0.7em) #align(center)[ - #text(130%)[*Essay on typography*] \ + #text(1.3em)[*Essay on typography*] \ T. Ypst ] @@ -20,7 +20,7 @@ A [_B #wrap c in [*#c*]; C_] D --- // Test wrap style precedence. -#set text(fill: eastern, size: 150%) +#set text(fill: eastern, size: 1.5em) #wrap body in text(fill: forest, body) Forest diff --git a/tests/typ/text/baseline.typ b/tests/typ/text/baseline.typ index 7100ab528..5f4515632 100644 --- a/tests/typ/text/baseline.typ +++ b/tests/typ/text/baseline.typ @@ -1,4 +1,4 @@ // Test text baseline. --- -Hi #text(150%)[You], #text(75%)[how are you?] +Hi #text(1.5em)[You], #text(0.75em)[how are you?] diff --git a/tests/typ/text/deco.typ b/tests/typ/text/deco.typ index a9f380b9c..071208ac6 100644 --- a/tests/typ/text/deco.typ +++ b/tests/typ/text/deco.typ @@ -19,11 +19,11 @@ #overline(underline[Running amongst the wolves.]) --- -#let redact = strike.with(10pt, extent: 5%) +#let redact = strike.with(10pt, extent: 0.05em) #let highlight = strike.with( stroke: rgb("abcdef88"), thickness: 10pt, - extent: 5%, + extent: 0.05em, ) // Abuse thickness and transparency for redacting and highlighting stuff. diff --git a/tests/typ/text/edge.typ b/tests/typ/text/edge.typ new file mode 100644 index 000000000..c3c60b280 --- /dev/null +++ b/tests/typ/text/edge.typ @@ -0,0 +1,25 @@ +// Test top and bottom text edge. + +--- +#set page(width: 160pt) +#set text(size: 8pt) + +#let try(top, bottom) = rect(fill: conifer)[ + #set text("IBM Plex Mono", top-edge: top, bottom-edge: bottom) + From #top to #bottom +] + +#try("ascender", "descender") +#try("ascender", "baseline") +#try("cap-height", "baseline") +#try("x-height", "baseline") +#try(4pt, -2pt) +#try(1pt + 0.3em, -0.15em) + +--- +// Error: 21-23 expected string or length, found array +#set text(top-edge: ()) + +--- +// Error: 24-26 unknown font metric +#set text(bottom-edge: "") diff --git a/tests/typ/text/em.typ b/tests/typ/text/em.typ index d9b00f068..dd0a436e2 100644 --- a/tests/typ/text/em.typ +++ b/tests/typ/text/em.typ @@ -4,14 +4,31 @@ #set text(size: 5pt) A // 5pt [ - #set text(size: 200%) + #set text(size: 2em) B // 10pt [ - #set text(size: 150% + 1pt) + #set text(size: 1.5em + 1pt) C // 16pt - #text(size: 200%)[D] // 32pt + #text(size: 2em)[D] // 32pt E // 16pt ] F // 10pt ] G // 5pt + +--- +// Test using ems in arbitrary places. +#set text(size: 5pt) +#set text(size: 2em) +#set square(fill: red) + +#let size = { + let size = 0.25em + 1pt + for _ in range(3) { + size *= 2 + } + size - 3pt +} + +#square(size: size) +#square(size: 25pt) diff --git a/tests/typ/text/font.typ b/tests/typ/text/font.typ index 4b7d75347..b29091827 100644 --- a/tests/typ/text/font.typ +++ b/tests/typ/text/font.typ @@ -3,8 +3,8 @@ --- // Set same font size in three different ways. #text(20pt)[A] -#text(200%)[A] -#text(size: 15pt + 50%)[A] +#text(2em)[A] +#text(size: 15pt + 0.5em)[A] // Do nothing. #text()[Normal] @@ -38,23 +38,6 @@ Emoji: 🐪, 🌋, 🏞 #set text("PT Sans", "Twitter Color Emoji", fallback: false) 2π = 𝛼 + 𝛽. ✅ ---- -// Test top and bottom edge. -#set page(width: 150pt) -#set text(size: 8pt) - -#let try(top, bottom) = rect(fill: conifer)[ - #set text("IBM Plex Mono", top-edge: top, bottom-edge: bottom) - From #top to #bottom -] - -#try("ascender", "descender") -#try("ascender", "baseline") -#try("cap-height", "baseline") -#try("x-height", "baseline") -#try(4pt, -2pt) -#try(1pt + 27%, -18%) - --- // Error: 11-16 unexpected argument #set text(false) @@ -63,14 +46,6 @@ Emoji: 🐪, 🌋, 🏞 // Error: 18-24 expected "normal", "italic" or "oblique" #set text(style: "bold", weight: "thin") ---- -// Error: 21-23 expected string or relative length, found array -#set text(top-edge: ()) - ---- -// Error: 21-23 unknown font metric -#set text(top-edge: "") - --- // Error: 23-27 unexpected argument #set text(size: 10pt, 12pt) diff --git a/tests/typ/text/par.typ b/tests/typ/text/par.typ index 2bff4a474..6b7c0f59a 100644 --- a/tests/typ/text/par.typ +++ b/tests/typ/text/par.typ @@ -31,7 +31,7 @@ A #set par(spacing: 0pt, leading: 0pt); B #parbreak() C --- // Test weird metrics. -#set par(spacing: 100%, leading: 0pt) +#set par(spacing: 1em, leading: 0pt) But, soft! what light through yonder window breaks? It is the east, and Juliet is the sun. diff --git a/tests/typ/text/tracking-spacing.typ b/tests/typ/text/tracking-spacing.typ new file mode 100644 index 000000000..b938af36e --- /dev/null +++ b/tests/typ/text/tracking-spacing.typ @@ -0,0 +1,30 @@ +// Test tracking characters apart or together. + +--- +// Test tracking. +#set text(tracking: -0.01em) +I saw Zoe yӛsterday, on the tram. + +--- +// Test tracking for only part of paragraph. +I'm in#text(tracking: 0.15em + 1.5pt)[ spaace]! + +--- +// Test that tracking doesn't disrupt mark placement. +#set text(tracking: 0.3em) +טֶקסט + +--- +// Test tracking in arabic text (makes no sense whatsoever) +#set text(tracking: 0.3em) +النص + +--- +// Test word spacing. +#set text(spacing: 1em) +My text has spaces. + +--- +// Test word spacing relative to the font's space width. +#set text(spacing: 50% + 1pt) +This is tight. diff --git a/tests/typ/text/tracking.typ b/tests/typ/text/tracking.typ deleted file mode 100644 index e3ff70ff9..000000000 --- a/tests/typ/text/tracking.typ +++ /dev/null @@ -1,12 +0,0 @@ -// Test tracking characters apart or together. - ---- -#set text(tracking: -0.01) -I saw Zoe yӛsterday, on the tram. - ---- -I'm in#text(tracking: 0.3)[ spaace]! - ---- -#set text("Noto Serif Hebrew", tracking: 0.3) -טֶקסט diff --git a/tests/typ/utility/math.typ b/tests/typ/utility/math.typ index bb7298900..4ccefa229 100644 --- a/tests/typ/utility/math.typ +++ b/tests/typ/utility/math.typ @@ -35,8 +35,20 @@ #test(abs(-0.0), 0.0) #test(abs(0.0), -0.0) #test(abs(-3.14), 3.14) -#test(abs(-12pt), 12pt) #test(abs(50%), 50%) +#test(abs(-25%), 25%) + +--- +// Error: 6-17 expected numeric value, found string +#abs("no number") + +--- +// Error: 6-11 cannot take absolute value of a length +#abs(-12pt) + +--- +// Error: 6-16 cannot take absolute value of a length +#abs(50% - 12pt) --- // Test the `even` and `odd` functions. @@ -61,14 +73,6 @@ // Error: 11-14 divisor must not be zero #mod(3.0, 0.0) ---- -// Error: 6-16 cannot take absolute value of a relative length -#abs(10pt + 50%) - ---- -// Error: 6-17 expected numeric value, found string -#abs("no number") - --- // Test the `min` and `max` functions. #test(min(2, -4), -4) diff --git a/tests/typeset.rs b/tests/typeset.rs index 2d8af2ff1..bba826219 100644 --- a/tests/typeset.rs +++ b/tests/typeset.rs @@ -13,7 +13,7 @@ use typst::eval::{Smart, StyleMap, Value}; use typst::frame::{Element, Frame}; use typst::geom::{Length, RgbaColor}; use typst::library::layout::PageNode; -use typst::library::text::{FontSize, TextNode}; +use typst::library::text::{TextNode, TextSize}; use typst::loading::FsLoader; use typst::parse::Scanner; use typst::source::SourceFile; @@ -61,13 +61,13 @@ fn main() { // exactly 100pt wide. Page height is unbounded and font size is 10pt so // that it multiplies to nice round numbers. let mut styles = StyleMap::new(); - styles.set(PageNode::WIDTH, Smart::Custom(Length::pt(120.0))); + styles.set(PageNode::WIDTH, Smart::Custom(Length::pt(120.0).into())); styles.set(PageNode::HEIGHT, Smart::Auto); styles.set(PageNode::LEFT, Smart::Custom(Length::pt(10.0).into())); styles.set(PageNode::TOP, Smart::Custom(Length::pt(10.0).into())); styles.set(PageNode::RIGHT, Smart::Custom(Length::pt(10.0).into())); styles.set(PageNode::BOTTOM, Smart::Custom(Length::pt(10.0).into())); - styles.set(TextNode::SIZE, FontSize(Length::pt(10.0).into())); + styles.set(TextNode::SIZE, TextSize(Length::pt(10.0).into())); // Hook up an assert function into the global scope. let mut std = typst::library::new();