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 000000000..0514d628e Binary files /dev/null and b/tests/ref/text/edge.png differ diff --git a/tests/ref/text/em.png b/tests/ref/text/em.png index e989eadeb..c0afa6fb5 100644 Binary files a/tests/ref/text/em.png and b/tests/ref/text/em.png differ diff --git a/tests/ref/text/font.png b/tests/ref/text/font.png index 1c1a71eef..672513711 100644 Binary files a/tests/ref/text/font.png and b/tests/ref/text/font.png differ diff --git a/tests/ref/text/tracking-spacing.png b/tests/ref/text/tracking-spacing.png new file mode 100644 index 000000000..ec130c993 Binary files /dev/null and b/tests/ref/text/tracking-spacing.png differ diff --git a/tests/ref/text/tracking.png b/tests/ref/text/tracking.png deleted file mode 100644 index 5c35d94c6..000000000 Binary files a/tests/ref/text/tracking.png and /dev/null differ 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();