This commit is contained in:
Laurenz 2022-04-08 15:08:26 +02:00
parent 977ac77e6a
commit 712c00ecb7
67 changed files with 667 additions and 433 deletions

View File

@ -456,7 +456,7 @@ impl<'a> Builder<'a> {
}) })
.unwrap_or_default() .unwrap_or_default()
{ {
par.push_front(ParChild::Spacing(Spacing::Relative(indent))) par.push_front(ParChild::Spacing(indent.into()));
} }
let node = ParNode(par).pack(); let node = ParNode(par).pack();

View File

@ -5,12 +5,10 @@ use std::fmt::{self, Debug, Formatter};
use std::hash::Hash; use std::hash::Hash;
use std::sync::Arc; use std::sync::Arc;
use super::{Barrier, RawAlign, StyleChain}; use super::{Barrier, RawAlign, RawLength, Resolve, StyleChain};
use crate::diag::TypResult; use crate::diag::TypResult;
use crate::frame::{Element, Frame, Geometry, Shape, Stroke}; use crate::frame::{Element, Frame, Geometry, Shape, Stroke};
use crate::geom::{ use crate::geom::{Align, Length, Paint, Point, Relative, Sides, Size, Spec};
Align, Length, Numeric, Paint, Point, Relative, Sides, Size, Spec, Transform,
};
use crate::library::graphics::MoveNode; use crate::library::graphics::MoveNode;
use crate::library::layout::{AlignNode, PadNode}; use crate::library::layout::{AlignNode, PadNode};
use crate::util::Prehashed; use crate::util::Prehashed;
@ -163,7 +161,7 @@ impl LayoutNode {
} }
/// Force a size for this node. /// Force a size for this node.
pub fn sized(self, sizing: Spec<Option<Relative<Length>>>) -> Self { pub fn sized(self, sizing: Spec<Option<Relative<RawLength>>>) -> Self {
if sizing.any(Option::is_some) { if sizing.any(Option::is_some) {
SizedNode { sizing, child: self }.pack() SizedNode { sizing, child: self }.pack()
} else { } else {
@ -191,7 +189,7 @@ impl LayoutNode {
} }
/// Pad this node at the sides. /// Pad this node at the sides.
pub fn padded(self, padding: Sides<Relative<Length>>) -> Self { pub fn padded(self, padding: Sides<Relative<RawLength>>) -> Self {
if !padding.left.is_zero() if !padding.left.is_zero()
|| !padding.top.is_zero() || !padding.top.is_zero()
|| !padding.right.is_zero() || !padding.right.is_zero()
@ -204,13 +202,9 @@ impl LayoutNode {
} }
/// Transform this node's contents without affecting layout. /// Transform this node's contents without affecting layout.
pub fn moved(self, offset: Point) -> Self { pub fn moved(self, delta: Spec<Relative<RawLength>>) -> Self {
if !offset.is_zero() { if delta.any(|r| !r.is_zero()) {
MoveNode { MoveNode { delta, child: self }.pack()
transform: Transform::translate(offset.x, offset.y),
child: self,
}
.pack()
} else { } else {
self self
} }
@ -294,7 +288,7 @@ impl Layout for EmptyNode {
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
struct SizedNode { struct SizedNode {
/// How to size the node horizontally and vertically. /// How to size the node horizontally and vertically.
sizing: Spec<Option<Relative<Length>>>, sizing: Spec<Option<Relative<RawLength>>>,
/// The node to be sized. /// The node to be sized.
child: LayoutNode, child: LayoutNode,
} }
@ -311,8 +305,9 @@ impl Layout for SizedNode {
// Resolve the sizing to a concrete size. // Resolve the sizing to a concrete size.
let size = self let size = self
.sizing .sizing
.resolve(styles)
.zip(regions.base) .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); .unwrap_or(regions.first);
// Select the appropriate base and expansion for the child depending // Select the appropriate base and expansion for the child depending

View File

@ -43,7 +43,7 @@ use parking_lot::{MappedRwLockWriteGuard, RwLockWriteGuard};
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use crate::diag::{At, StrResult, Trace, Tracepoint, TypResult}; 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::library;
use crate::syntax::ast::*; use crate::syntax::ast::*;
use crate::syntax::{Span, Spanned}; use crate::syntax::{Span, Spanned};
@ -245,10 +245,13 @@ impl Eval for Lit {
LitKind::Bool(v) => Value::Bool(v), LitKind::Bool(v) => Value::Bool(v),
LitKind::Int(v) => Value::Int(v), LitKind::Int(v) => Value::Int(v),
LitKind::Float(v) => Value::Float(v), LitKind::Float(v) => Value::Float(v),
LitKind::Length(v, unit) => Value::Length(Length::with_unit(v, unit)), LitKind::Numeric(v, unit) => match unit {
LitKind::Angle(v, unit) => Value::Angle(Angle::with_unit(v, unit)), Unit::Length(unit) => Length::with_unit(v, unit).into(),
LitKind::Percent(v) => Value::Ratio(Ratio::new(v / 100.0)), Unit::Angle(unit) => Angle::with_unit(v, unit).into(),
LitKind::Fractional(v) => Value::Fraction(Fraction::new(v)), 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()), 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. /// Process an import of a module relative to the current location.
fn import(ctx: &mut Context, path: &str, span: Span) -> TypResult<Module> { fn import(ctx: &mut Context, path: &str, span: Span) -> TypResult<Module> {
// Load the source file. // 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() { let id = ctx.sources.load(&full).map_err(|err| match err.kind() {
std::io::ErrorKind::NotFound => error!(span, "file not found"), std::io::ErrorKind::NotFound => error!(span, "file not found"),
_ => error!(span, "failed to load source file ({})", err), _ => error!(span, "failed to load source file ({})", err),

View File

@ -150,8 +150,8 @@ pub fn mul(lhs: Value, rhs: Value) -> StrResult<Value> {
(Length(a), Int(b)) => Length(a * b as f64), (Length(a), Int(b)) => Length(a * b as f64),
(Length(a), Float(b)) => Length(a * b), (Length(a), Float(b)) => Length(a * b),
(Int(a), Length(b)) => Length(a as f64 * b), (Int(a), Length(b)) => Length(b * a as f64),
(Float(a), Length(b)) => Length(a * b), (Float(a), Length(b)) => Length(b * a),
(Angle(a), Int(b)) => Angle(a * b as f64), (Angle(a), Int(b)) => Angle(a * b as f64),
(Angle(a), Float(b)) => Angle(a * b), (Angle(a), Float(b)) => Angle(a * b),
@ -194,7 +194,6 @@ pub fn div(lhs: Value, rhs: Value) -> StrResult<Value> {
(Length(a), Int(b)) => Length(a / b as f64), (Length(a), Int(b)) => Length(a / b as f64),
(Length(a), Float(b)) => Length(a / b), (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), Int(b)) => Angle(a / b as f64),
(Angle(a), Float(b)) => Angle(a / b), (Angle(a), Float(b)) => Angle(a / b),

View File

@ -1,8 +1,9 @@
use std::fmt::{self, Debug, Formatter}; use std::fmt::{self, Debug, Formatter};
use std::ops::{Add, Div, Mul, Neg};
use super::{Resolve, StyleChain}; use super::{Resolve, StyleChain};
use crate::geom::{Align, SpecAxis}; use crate::geom::{Align, Em, Length, Numeric, Relative, SpecAxis};
use crate::library::text::ParNode; use crate::library::text::{ParNode, TextNode};
/// The unresolved alignment representation. /// The unresolved alignment representation.
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] #[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<Length> for RawLength {
fn from(length: Length) -> Self {
Self { length, em: Em::zero() }
}
}
impl From<Em> for RawLength {
fn from(em: Em) -> Self {
Self { length: Length::zero(), em }
}
}
impl From<Length> for Relative<RawLength> {
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<f64> for RawLength {
type Output = Self;
fn mul(self, rhs: f64) -> Self::Output {
Self {
length: self.length * rhs,
em: self.em * rhs,
}
}
}
impl Div<f64> 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);

View File

@ -4,9 +4,9 @@ use std::fmt::{self, Debug, Formatter};
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use std::sync::Arc; 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::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::library::text::RawNode;
use crate::syntax::{Span, Spanned}; use crate::syntax::{Span, Spanned};
use crate::util::EcoString; use crate::util::EcoString;
@ -25,13 +25,13 @@ pub enum Value {
/// A floating-point number: `1.2`, `10e-4`. /// A floating-point number: `1.2`, `10e-4`.
Float(f64), Float(f64),
/// A length: `12pt`, `3cm`. /// A length: `12pt`, `3cm`.
Length(Length), Length(RawLength),
/// An angle: `1.5rad`, `90deg`. /// An angle: `1.5rad`, `90deg`.
Angle(Angle), Angle(Angle),
/// A ratio: `50%`. /// A ratio: `50%`.
Ratio(Ratio), Ratio(Ratio),
/// A relative length, combination of a ratio and a length: `20% + 5cm`. /// A relative length, combination of a ratio and a length: `20% + 5cm`.
Relative(Relative<Length>), Relative(Relative<RawLength>),
/// A fraction: `1fr`. /// A fraction: `1fr`.
Fraction(Fraction), Fraction(Fraction),
/// A color value: `#f79143ff`. /// A color value: `#f79143ff`.
@ -77,10 +77,10 @@ impl Value {
Self::Bool(_) => bool::TYPE_NAME, Self::Bool(_) => bool::TYPE_NAME,
Self::Int(_) => i64::TYPE_NAME, Self::Int(_) => i64::TYPE_NAME,
Self::Float(_) => f64::TYPE_NAME, Self::Float(_) => f64::TYPE_NAME,
Self::Length(_) => Length::TYPE_NAME, Self::Length(_) => RawLength::TYPE_NAME,
Self::Angle(_) => Angle::TYPE_NAME, Self::Angle(_) => Angle::TYPE_NAME,
Self::Ratio(_) => Ratio::TYPE_NAME, Self::Ratio(_) => Ratio::TYPE_NAME,
Self::Relative(_) => Relative::TYPE_NAME, Self::Relative(_) => Relative::<RawLength>::TYPE_NAME,
Self::Fraction(_) => Fraction::TYPE_NAME, Self::Fraction(_) => Fraction::TYPE_NAME,
Self::Color(_) => Color::TYPE_NAME, Self::Color(_) => Color::TYPE_NAME,
Self::Str(_) => EcoString::TYPE_NAME, Self::Str(_) => EcoString::TYPE_NAME,
@ -320,6 +320,18 @@ impl From<usize> for Value {
} }
} }
impl From<Length> for Value {
fn from(v: Length) -> Self {
Self::Length(v.into())
}
}
impl From<Em> for Value {
fn from(v: Em) -> Self {
Self::Length(v.into())
}
}
impl From<RgbaColor> for Value { impl From<RgbaColor> for Value {
fn from(v: RgbaColor) -> Self { fn from(v: RgbaColor) -> Self {
Self::Color(v.into()) Self::Color(v.into())
@ -546,10 +558,10 @@ macro_rules! castable {
primitive! { bool: "boolean", Bool } primitive! { bool: "boolean", Bool }
primitive! { i64: "integer", Int } primitive! { i64: "integer", Int }
primitive! { f64: "float", Float, Int(v) => v as f64 } primitive! { f64: "float", Float, Int(v) => v as f64 }
primitive! { Length: "length", Length } primitive! { RawLength: "length", Length }
primitive! { Angle: "angle", Angle } primitive! { Angle: "angle", Angle }
primitive! { Ratio: "ratio", Ratio } primitive! { Ratio: "ratio", Ratio }
primitive! { Relative<Length>: "relative length", Relative, Length(v) => v.into(), Ratio(v) => v.into() } primitive! { Relative<RawLength>: "relative length", Relative, Length(v) => v.into(), Ratio(v) => v.into() }
primitive! { Fraction: "fraction", Fraction } primitive! { Fraction: "fraction", Fraction }
primitive! { Color: "color", Color } primitive! { Color: "color", Color }
primitive! { EcoString: "string", Str } primitive! { EcoString: "string", Str }
@ -685,7 +697,10 @@ mod tests {
test(Length::pt(5.5), "5.5pt"); test(Length::pt(5.5), "5.5pt");
test(Angle::deg(90.0), "90deg"); test(Angle::deg(90.0), "90deg");
test(Ratio::one() / 2.0, "50%"); 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(Fraction::one() * 7.55, "7.55fr");
test(Color::Rgba(RgbaColor::new(1, 1, 1, 0xff)), "#010101"); test(Color::Rgba(RgbaColor::new(1, 1, 1, 0xff)), "#010101");

View File

@ -117,14 +117,14 @@ fn render_text(
let mut x = 0.0; let mut x = 0.0;
for glyph in &text.glyphs { for glyph in &text.glyphs {
let id = GlyphId(glyph.id); 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); let ts = ts.pre_translate(offset, 0.0);
render_svg_glyph(canvas, ts, mask, ctx, text, id) render_svg_glyph(canvas, ts, mask, ctx, text, id)
.or_else(|| render_bitmap_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)); .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();
} }
} }

View File

@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize};
use ttf_parser::{name_id, GlyphId, PlatformId, Tag}; use ttf_parser::{name_id, GlyphId, PlatformId, Tag};
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use crate::geom::{Em, Length, Relative}; use crate::geom::Em;
use crate::loading::{FileHash, Loader}; use crate::loading::{FileHash, Loader};
use crate::util::decode_mac_roman; use crate::util::decode_mac_roman;
@ -372,14 +372,13 @@ impl FaceMetrics {
} }
/// Look up a vertical metric at the given font size. /// 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 { match metric {
VerticalFontMetric::Ascender => self.ascender.resolve(size), VerticalFontMetric::Ascender => self.ascender,
VerticalFontMetric::CapHeight => self.cap_height.resolve(size), VerticalFontMetric::CapHeight => self.cap_height,
VerticalFontMetric::XHeight => self.x_height.resolve(size), VerticalFontMetric::XHeight => self.x_height,
VerticalFontMetric::Baseline => Length::zero(), VerticalFontMetric::Baseline => Em::zero(),
VerticalFontMetric::Descender => self.descender.resolve(size), VerticalFontMetric::Descender => self.descender,
VerticalFontMetric::Relative(v) => v.resolve(size),
} }
} }
} }
@ -413,9 +412,6 @@ pub enum VerticalFontMetric {
/// Corresponds to the typographic descender from the `OS/2` table if /// Corresponds to the typographic descender from the `OS/2` table if
/// present and falls back to the descender from the `hhea` table otherwise. /// present and falls back to the descender from the `hhea` table otherwise.
Descender, Descender,
/// An font-size dependent distance from the baseline (positive goes up, negative
/// down).
Relative(Relative<Length>),
} }
/// Properties of a single font face. /// Properties of a single font face.

View File

@ -197,7 +197,7 @@ pub struct Text {
impl Text { impl Text {
/// The width of the text run. /// The width of the text run.
pub fn width(&self) -> Length { 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()
} }
} }

View File

@ -16,18 +16,18 @@ impl Angle {
} }
/// Create an angle from a value in a unit. /// 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())) Self(Scalar(val * unit.raw_scale()))
} }
/// Create an angle from a number of radians. /// Create an angle from a number of radians.
pub fn rad(rad: f64) -> Self { 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. /// Create an angle from a number of degrees.
pub fn deg(deg: f64) -> Self { 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. /// Get the value of this angle in raw units.
@ -36,18 +36,18 @@ impl Angle {
} }
/// Get the value of this length in unit. /// 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() self.to_raw() / unit.raw_scale()
} }
/// Convert this to a number of radians. /// Convert this to a number of radians.
pub fn to_rad(self) -> f64 { pub fn to_rad(self) -> f64 {
self.to_unit(AngularUnit::Rad) self.to_unit(AngleUnit::Rad)
} }
/// Convert this to a number of degrees. /// Convert this to a number of degrees.
pub fn to_deg(self) -> f64 { pub fn to_deg(self) -> f64 {
self.to_unit(AngularUnit::Deg) self.to_unit(AngleUnit::Deg)
} }
/// The absolute value of the this angle. /// The absolute value of the this angle.
@ -145,14 +145,14 @@ impl Sum for Angle {
/// Different units of angular measurement. /// Different units of angular measurement.
#[derive(Copy, Clone, Eq, PartialEq, Hash)] #[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub enum AngularUnit { pub enum AngleUnit {
/// Radians. /// Radians.
Rad, Rad,
/// Degrees. /// Degrees.
Deg, Deg,
} }
impl AngularUnit { impl AngleUnit {
/// How many raw units correspond to a value of `1.0` in this unit. /// How many raw units correspond to a value of `1.0` in this unit.
fn raw_scale(self) -> f64 { fn raw_scale(self) -> f64 {
match self { match self {
@ -162,7 +162,7 @@ impl AngularUnit {
} }
} }
impl Debug for AngularUnit { impl Debug for AngleUnit {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.pad(match self { f.pad(match self {
Self::Rad => "rad", Self::Rad => "rad",

View File

@ -29,7 +29,12 @@ impl Em {
/// Create an em length from a length at the given font size. /// Create an em length from a length at the given font size.
pub fn from_length(length: Length, font_size: Length) -> Self { 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. /// The number of em units.
@ -38,8 +43,9 @@ impl Em {
} }
/// Convert to a length at the given font size. /// Convert to a length at the given font size.
pub fn resolve(self, font_size: Length) -> Length { pub fn at(self, font_size: Length) -> Length {
self.get() * font_size let resolved = font_size * self.get();
if resolved.is_finite() { resolved } else { Length::zero() }
} }
} }

View File

@ -30,8 +30,8 @@ impl Fraction {
Self::new(self.get().abs()) Self::new(self.get().abs())
} }
/// Resolve this fraction's share in the remaining space. /// Determine this fraction's share in the remaining space.
pub fn resolve(self, total: Self, remaining: Length) -> Length { pub fn share(self, total: Self, remaining: Length) -> Length {
let ratio = self / total; let ratio = self / total;
if ratio.is_finite() && remaining.is_finite() { if ratio.is_finite() && remaining.is_finite() {
ratio * remaining ratio * remaining

View File

@ -1,7 +1,7 @@
/// Implement the `Sub` trait based on existing `Neg` and `Add` impls. /// Implement the `Sub` trait based on existing `Neg` and `Add` impls.
macro_rules! sub_impl { macro_rules! sub_impl {
($a:ident - $b:ident -> $c:ident) => { ($a:ident - $b:ident -> $c:ident) => {
impl Sub<$b> for $a { impl std::ops::Sub<$b> for $a {
type Output = $c; type Output = $c;
fn sub(self, other: $b) -> $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. /// Implement an assign trait based on an existing non-assign trait.
macro_rules! assign_impl { macro_rules! assign_impl {
($a:ident += $b:ident) => { ($a:ident += $b:ident) => {
impl AddAssign<$b> for $a { impl std::ops::AddAssign<$b> for $a {
fn add_assign(&mut self, other: $b) { fn add_assign(&mut self, other: $b) {
*self = *self + other; *self = *self + other;
} }
@ -22,7 +22,7 @@ macro_rules! assign_impl {
}; };
($a:ident -= $b:ident) => { ($a:ident -= $b:ident) => {
impl SubAssign<$b> for $a { impl std::ops::SubAssign<$b> for $a {
fn sub_assign(&mut self, other: $b) { fn sub_assign(&mut self, other: $b) {
*self = *self - other; *self = *self - other;
} }
@ -30,7 +30,7 @@ macro_rules! assign_impl {
}; };
($a:ident *= $b:ident) => { ($a:ident *= $b:ident) => {
impl MulAssign<$b> for $a { impl std::ops::MulAssign<$b> for $a {
fn mul_assign(&mut self, other: $b) { fn mul_assign(&mut self, other: $b) {
*self = *self * other; *self = *self * other;
} }
@ -38,7 +38,7 @@ macro_rules! assign_impl {
}; };
($a:ident /= $b:ident) => { ($a:ident /= $b:ident) => {
impl DivAssign<$b> for $a { impl std::ops::DivAssign<$b> for $a {
fn div_assign(&mut self, other: $b) { fn div_assign(&mut self, other: $b) {
*self = *self / other; *self = *self / other;
} }

View File

@ -72,15 +72,15 @@ pub trait Numeric:
+ Mul<f64, Output = Self> + Mul<f64, Output = Self>
+ Div<f64, Output = Self> + Div<f64, Output = Self>
{ {
/// The identity element. /// The identity element for addition.
fn zero() -> Self; fn zero() -> Self;
/// Whether `self` is the identity element. /// Whether `self` is zero.
fn is_zero(self) -> bool { fn is_zero(self) -> bool {
self == Self::zero() self == Self::zero()
} }
/// Whether `self` contains only finite parts. /// Whether `self` consists only of finite parts.
fn is_finite(self) -> bool; fn is_finite(self) -> bool;
} }

View File

@ -38,8 +38,8 @@ impl Point {
/// Transform the point with the given transformation. /// Transform the point with the given transformation.
pub fn transform(self, ts: Transform) -> Self { pub fn transform(self, ts: Transform) -> Self {
Self::new( Self::new(
ts.sx.resolve(self.x) + ts.kx.resolve(self.y) + ts.tx, ts.sx.of(self.x) + ts.kx.of(self.y) + ts.tx,
ts.ky.resolve(self.x) + ts.sy.resolve(self.y) + ts.ty, ts.ky.of(self.x) + ts.sy.of(self.y) + ts.ty,
) )
} }
} }

View File

@ -3,7 +3,7 @@ use super::*;
/// A ratio of a whole. /// A ratio of a whole.
/// ///
/// _Note_: `50%` is represented as `0.5` here, but stored as `50.0` in the /// _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)] #[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Ratio(Scalar); pub struct Ratio(Scalar);
@ -38,13 +38,13 @@ impl Ratio {
self.0 == 1.0 self.0 == 1.0
} }
/// The absolute value of the this ratio. /// The absolute value of this ratio.
pub fn abs(self) -> Self { pub fn abs(self) -> Self {
Self::new(self.get().abs()) Self::new(self.get().abs())
} }
/// Resolve this relative to the given `whole`. /// Return the ratio of the given `whole`.
pub fn resolve<T: Numeric>(self, whole: T) -> T { pub fn of<T: Numeric>(self, whole: T) -> T {
let resolved = whole * self.get(); let resolved = whole * self.get();
if resolved.is_finite() { resolved } else { T::zero() } if resolved.is_finite() { resolved } else { T::zero() }
} }

View File

@ -27,12 +27,17 @@ impl<T: Numeric> Relative<T> {
/// Whether both parts are zero. /// Whether both parts are zero.
pub fn is_zero(self) -> bool { 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`. /// Whether the relative part is one and the absolute part is zero.
pub fn resolve(self, whole: T) -> T { pub fn is_one(self) -> bool {
self.rel.resolve(whole) + self.abs 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`. /// Map the absolute part with `f`.
@ -120,27 +125,31 @@ impl<T: Numeric> Div<f64> for Relative<T> {
} }
} }
impl<T: Numeric> AddAssign for Relative<T> { impl<T: Numeric + AddAssign> AddAssign for Relative<T> {
fn add_assign(&mut self, other: Self) { fn add_assign(&mut self, other: Self) {
*self = *self + other; self.rel += other.rel;
self.abs += other.abs;
} }
} }
impl<T: Numeric> SubAssign for Relative<T> { impl<T: Numeric + SubAssign> SubAssign for Relative<T> {
fn sub_assign(&mut self, other: Self) { fn sub_assign(&mut self, other: Self) {
*self = *self - other; self.rel -= other.rel;
self.abs -= other.abs;
} }
} }
impl<T: Numeric> MulAssign<f64> for Relative<T> { impl<T: Numeric + MulAssign<f64>> MulAssign<f64> for Relative<T> {
fn mul_assign(&mut self, other: f64) { fn mul_assign(&mut self, other: f64) {
*self = *self * other; self.rel *= other;
self.abs *= other;
} }
} }
impl<T: Numeric> DivAssign<f64> for Relative<T> { impl<T: Numeric + DivAssign<f64>> DivAssign<f64> for Relative<T> {
fn div_assign(&mut self, other: f64) { fn div_assign(&mut self, other: f64) {
*self = *self * other; self.rel /= other;
self.abs /= other;
} }
} }

View File

@ -44,13 +44,13 @@ where
} }
impl Sides<Relative<Length>> { impl Sides<Relative<Length>> {
/// Resolve the sides relative to the given `size`. /// Evaluate the sides relative to the given `size`.
pub fn resolve(self, size: Size) -> Sides<Length> { pub fn relative_to(self, size: Size) -> Sides<Length> {
Sides { Sides {
left: self.left.resolve(size.x), left: self.left.relative_to(size.x),
top: self.top.resolve(size.y), top: self.top.relative_to(size.y),
right: self.right.resolve(size.x), right: self.right.relative_to(size.x),
bottom: self.bottom.resolve(size.y), bottom: self.bottom.relative_to(size.y),
} }
} }
} }

View File

@ -59,8 +59,8 @@ impl Transform {
ky: self.ky * prev.sx + self.sy * prev.ky, ky: self.ky * prev.sx + self.sy * prev.ky,
kx: self.sx * prev.kx + self.kx * prev.sy, kx: self.sx * prev.kx + self.kx * prev.sy,
sy: self.ky * prev.kx + self.sy * prev.sy, sy: self.ky * prev.kx + self.sy * prev.sy,
tx: self.sx.resolve(prev.tx) + self.kx.resolve(prev.ty) + self.tx, tx: self.sx.of(prev.tx) + self.kx.of(prev.ty) + self.tx,
ty: self.ky.resolve(prev.tx) + self.sy.resolve(prev.ty) + self.ty, ty: self.ky.of(prev.tx) + self.sy.of(prev.ty) + self.ty,
} }
} }
} }

View File

@ -34,13 +34,14 @@
#[macro_use] #[macro_use]
pub mod util; pub mod util;
#[macro_use] #[macro_use]
pub mod geom;
#[macro_use]
pub mod diag; pub mod diag;
#[macro_use] #[macro_use]
pub mod eval; pub mod eval;
pub mod export; pub mod export;
pub mod font; pub mod font;
pub mod frame; pub mod frame;
pub mod geom;
pub mod image; pub mod image;
pub mod library; pub mod library;
pub mod loading; pub mod loading;
@ -163,7 +164,7 @@ impl Context {
/// Resolve a user-entered path (relative to the current evaluation /// Resolve a user-entered path (relative to the current evaluation
/// location) to be relative to the compilation environment's root. /// 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(&id) = self.route.last() {
if let Some(dir) = self.sources.get(id).path().parent() { if let Some(dir) = self.sources.get(id).path().parent() {
return dir.join(path); return dir.join(path);

View File

@ -13,7 +13,7 @@ impl ImageNode {
fn construct(ctx: &mut Context, args: &mut Args) -> TypResult<Content> { fn construct(ctx: &mut Context, args: &mut Args) -> TypResult<Content> {
let path = args.expect::<Spanned<EcoString>>("path to image file")?; let path = args.expect::<Spanned<EcoString>>("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() { let id = ctx.images.load(&full).map_err(|err| match err.kind() {
std::io::ErrorKind::NotFound => error!(path.span, "file not found"), std::io::ErrorKind::NotFound => error!(path.span, "file not found"),
_ => error!(path.span, "failed to load image ({})", err), _ => error!(path.span, "failed to load image ({})", err),

View File

@ -4,9 +4,9 @@ use crate::library::prelude::*;
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
pub struct LineNode { pub struct LineNode {
/// Where the line starts. /// Where the line starts.
origin: Spec<Relative<Length>>, origin: Spec<Relative<RawLength>>,
/// The offset from the `origin` where the line ends. /// The offset from the `origin` where the line ends.
delta: Spec<Relative<Length>>, delta: Spec<Relative<RawLength>>,
} }
#[node] #[node]
@ -14,15 +14,17 @@ impl LineNode {
/// How to stroke the line. /// How to stroke the line.
pub const STROKE: Paint = Color::BLACK.into(); pub const STROKE: Paint = Color::BLACK.into();
/// The line's thickness. /// 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<Content> { fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> {
let origin = args.named("origin")?.unwrap_or_default(); let origin = args.named("origin")?.unwrap_or_default();
let delta = match args.named::<Spec<Relative<Length>>>("to")? {
let delta = match args.named::<Spec<Relative<RawLength>>>("to")? {
Some(to) => to.zip(origin).map(|(to, from)| to - from), Some(to) => to.zip(origin).map(|(to, from)| to - from),
None => { None => {
let length = args let length = args
.named::<Relative<Length>>("length")? .named::<Relative<RawLength>>("length")?
.unwrap_or(Length::cm(1.0).into()); .unwrap_or(Length::cm(1.0).into());
let angle = args.named::<Angle>("angle")?.unwrap_or_default(); let angle = args.named::<Angle>("angle")?.unwrap_or_default();
@ -50,18 +52,37 @@ impl Layout for LineNode {
thickness, thickness,
}); });
let resolved_origin = let origin = self
self.origin.zip(regions.base).map(|(l, b)| Relative::resolve(l, b)); .origin
let resolved_delta = .resolve(styles)
self.delta.zip(regions.base).map(|(l, b)| Relative::resolve(l, b)); .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 shape = Shape { geometry, fill: None, stroke };
let target = regions.expand.select(regions.first, Size::zero()); let target = regions.expand.select(regions.first, Size::zero());
let mut frame = Frame::new(target); 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)]) Ok(vec![Arc::new(frame)])
} }
} }
castable! {
Spec<Relative<RawLength>>,
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")?,
}
},
}

View File

@ -26,14 +26,15 @@ impl<const S: ShapeKind> ShapeNode<S> {
/// How to stroke the shape. /// How to stroke the shape.
pub const STROKE: Smart<Option<Paint>> = Smart::Auto; pub const STROKE: Smart<Option<Paint>> = Smart::Auto;
/// The stroke's thickness. /// 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. /// How much to pad the shape's content.
pub const PADDING: Relative<Length> = Relative::zero(); pub const PADDING: Relative<RawLength> = Relative::zero();
fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> { fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> {
let size = match S { let size = match S {
SQUARE => args.named::<Length>("size")?.map(Relative::from), SQUARE => args.named::<RawLength>("size")?.map(Relative::from),
CIRCLE => args.named::<Length>("radius")?.map(|r| 2.0 * Relative::from(r)), CIRCLE => args.named::<RawLength>("radius")?.map(|r| 2.0 * Relative::from(r)),
_ => None, _ => None,
}; };

View File

@ -1,6 +1,46 @@
use crate::geom::Transform; use crate::geom::Transform;
use crate::library::prelude::*; 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<Relative<RawLength>>,
/// The node whose contents should be moved.
pub child: LayoutNode,
}
#[node]
impl MoveNode {
fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> {
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<Vec<Arc<Frame>>> {
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. /// Transform a node without affecting layout.
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
pub struct TransformNode<const T: TransformKind> { pub struct TransformNode<const T: TransformKind> {
@ -10,13 +50,10 @@ pub struct TransformNode<const T: TransformKind> {
pub child: LayoutNode, pub child: LayoutNode,
} }
/// Transform a node by translating it without affecting layout. /// Rotate a node without affecting layout.
pub type MoveNode = TransformNode<MOVE>;
/// Transform a node by rotating it without affecting layout.
pub type RotateNode = TransformNode<ROTATE>; pub type RotateNode = TransformNode<ROTATE>;
/// Transform a node by scaling it without affecting layout. /// Scale a node without affecting layout.
pub type ScaleNode = TransformNode<SCALE>; pub type ScaleNode = TransformNode<SCALE>;
#[node] #[node]
@ -27,11 +64,6 @@ impl<const T: TransformKind> TransformNode<T> {
fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> { fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> {
let transform = match T { 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 => { ROTATE => {
let angle = args.named_or_find("angle")?.unwrap_or_default(); let angle = args.named_or_find("angle")?.unwrap_or_default();
Transform::rotate(angle) Transform::rotate(angle)
@ -77,9 +109,6 @@ impl<const T: TransformKind> Layout for TransformNode<T> {
/// Kinds of transformations. /// Kinds of transformations.
pub type TransformKind = usize; pub type TransformKind = usize;
/// A translation on the X and Y axes.
const MOVE: TransformKind = 0;
/// A rotational transformation. /// A rotational transformation.
const ROTATE: TransformKind = 1; const ROTATE: TransformKind = 1;

View File

@ -14,7 +14,8 @@ pub struct ColumnsNode {
#[node] #[node]
impl ColumnsNode { impl ColumnsNode {
/// The size of the gutter space between each column. /// The size of the gutter space between each column.
pub const GUTTER: Relative<Length> = Ratio::new(0.04).into(); #[property(resolve)]
pub const GUTTER: Relative<RawLength> = Ratio::new(0.04).into();
fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> { fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> {
Ok(Content::block(Self { Ok(Content::block(Self {
@ -39,7 +40,7 @@ impl Layout for ColumnsNode {
// Determine the width of the gutter and each column. // Determine the width of the gutter and each column.
let columns = self.columns.get(); 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; let width = (regions.first.x - gutter * (columns - 1) as f64) / columns as f64;
// Create the pod regions. // Create the pod regions.

View File

@ -1,6 +1,6 @@
use super::{AlignNode, PlaceNode, Spacing}; use super::{AlignNode, PlaceNode, Spacing};
use crate::library::prelude::*; 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. /// Arrange spacing, paragraphs and other block-level nodes into a flow.
/// ///
@ -37,22 +37,20 @@ impl Layout for FlowNode {
let styles = map.chain(&styles); let styles = map.chain(&styles);
match child { match child {
FlowChild::Leading => { FlowChild::Leading => {
let em = styles.get(TextNode::SIZE); let amount = styles.get(ParNode::LEADING);
let amount = styles.get(ParNode::LEADING).resolve(em); layouter.layout_spacing(amount.into(), styles);
layouter.layout_spacing(amount.into());
} }
FlowChild::Parbreak => { FlowChild::Parbreak => {
let em = styles.get(TextNode::SIZE);
let leading = styles.get(ParNode::LEADING); let leading = styles.get(ParNode::LEADING);
let spacing = styles.get(ParNode::SPACING); let spacing = styles.get(ParNode::SPACING);
let amount = (leading + spacing).resolve(em); let amount = leading + spacing;
layouter.layout_spacing(amount.into()); layouter.layout_spacing(amount.into(), styles);
} }
FlowChild::Colbreak => { FlowChild::Colbreak => {
layouter.finish_region(); layouter.finish_region();
} }
FlowChild::Spacing(kind) => { FlowChild::Spacing(kind) => {
layouter.layout_spacing(*kind); layouter.layout_spacing(*kind, styles);
} }
FlowChild::Node(ref node) => { FlowChild::Node(ref node) => {
layouter.layout_node(ctx, node, styles)?; layouter.layout_node(ctx, node, styles)?;
@ -142,11 +140,11 @@ impl FlowLayouter {
} }
/// Layout spacing. /// Layout spacing.
pub fn layout_spacing(&mut self, spacing: Spacing) { pub fn layout_spacing(&mut self, spacing: Spacing, styles: StyleChain) {
match spacing { match spacing {
Spacing::Relative(v) => { Spacing::Relative(v) => {
// Resolve the spacing and limit it to the remaining space. // 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); let limited = resolved.min(self.regions.first.y);
self.regions.first.y -= limited; self.regions.first.y -= limited;
self.used.y += limited; self.used.y += limited;
@ -235,7 +233,7 @@ impl FlowLayouter {
offset += v; offset += v;
} }
FlowItem::Fractional(v) => { FlowItem::Fractional(v) => {
offset += v.resolve(self.fr, remaining); offset += v.share(self.fr, remaining);
} }
FlowItem::Frame(frame, aligns) => { FlowItem::Frame(frame, aligns) => {
ruler = ruler.max(aligns.y); ruler = ruler.max(aligns.y);

View File

@ -58,7 +58,7 @@ pub enum TrackSizing {
Auto, Auto,
/// A track size specified in absolute terms and relative to the parent's /// A track size specified in absolute terms and relative to the parent's
/// size. /// size.
Relative(Relative<Length>), Relative(Relative<RawLength>),
/// A track size specified as a fraction of the remaining free space in the /// A track size specified as a fraction of the remaining free space in the
/// parent. /// parent.
Fractional(Fraction), Fractional(Fraction),
@ -236,7 +236,8 @@ impl<'a> GridLayouter<'a> {
match col { match col {
TrackSizing::Auto => {} TrackSizing::Auto => {}
TrackSizing::Relative(v) => { 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; *rcol = resolved;
rel += resolved; rel += resolved;
} }
@ -295,7 +296,8 @@ impl<'a> GridLayouter<'a> {
// base, for auto it's already correct and for fr we could // base, for auto it's already correct and for fr we could
// only guess anyway. // only guess anyway.
if let TrackSizing::Relative(v) = self.rows[y] { 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); 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) { fn grow_fractional_columns(&mut self, remaining: Length, fr: Fraction) {
for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) { for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) {
if let TrackSizing::Fractional(v) = col { 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( fn layout_relative_row(
&mut self, &mut self,
ctx: &mut Context, ctx: &mut Context,
v: Relative<Length>, v: Relative<RawLength>,
y: usize, y: usize,
) -> TypResult<()> { ) -> 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)?; let frame = self.layout_single_row(ctx, resolved, y)?;
// Skip to fitting region. // Skip to fitting region.
@ -543,7 +545,7 @@ impl<'a> GridLayouter<'a> {
Row::Frame(frame) => frame, Row::Frame(frame) => frame,
Row::Fr(v, y) => { Row::Fr(v, y) => {
let remaining = self.full - self.used.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)? self.layout_single_row(ctx, height, y)?
} }
}; };

View File

@ -4,7 +4,7 @@ use crate::library::prelude::*;
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
pub struct PadNode { pub struct PadNode {
/// The amount of padding. /// The amount of padding.
pub padding: Sides<Relative<Length>>, pub padding: Sides<Relative<RawLength>>,
/// The child node whose sides to pad. /// The child node whose sides to pad.
pub child: LayoutNode, pub child: LayoutNode,
} }
@ -33,14 +33,15 @@ impl Layout for PadNode {
styles: StyleChain, styles: StyleChain,
) -> TypResult<Vec<Arc<Frame>>> { ) -> TypResult<Vec<Arc<Frame>>> {
// Layout child into padded regions. // 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)?; let mut frames = self.child.layout(ctx, &pod, styles)?;
for frame in &mut frames { for frame in &mut frames {
// Apply the padding inversely such that the grown size padded // Apply the padding inversely such that the grown size padded
// yields the frame's size. // yields the frame's size.
let padded = grow(frame.size, self.padding); let padded = grow(frame.size, padding);
let padding = self.padding.resolve(padded); let padding = padding.relative_to(padded);
let offset = Point::new(padding.left, padding.top); let offset = Point::new(padding.left, padding.top);
// Grow the frame and translate everything in the frame inwards. // 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. /// Shrink a size by padding relative to the size itself.
fn shrink(size: Size, padding: Sides<Relative<Length>>) -> Size { fn shrink(size: Size, padding: Sides<Relative<Length>>) -> 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. /// Grow a size by padding relative to the grown size.

View File

@ -10,19 +10,21 @@ pub struct PageNode(pub LayoutNode);
#[node] #[node]
impl PageNode { impl PageNode {
/// The unflipped width of the page. /// The unflipped width of the page.
pub const WIDTH: Smart<Length> = Smart::Custom(Paper::A4.width()); #[property(resolve)]
pub const WIDTH: Smart<RawLength> = Smart::Custom(Paper::A4.width().into());
/// The unflipped height of the page. /// The unflipped height of the page.
pub const HEIGHT: Smart<Length> = Smart::Custom(Paper::A4.height()); #[property(resolve)]
pub const HEIGHT: Smart<RawLength> = Smart::Custom(Paper::A4.height().into());
/// Whether the page is flipped into landscape orientation. /// Whether the page is flipped into landscape orientation.
pub const FLIPPED: bool = false; pub const FLIPPED: bool = false;
/// The left margin. /// The left margin.
pub const LEFT: Smart<Relative<Length>> = Smart::Auto; pub const LEFT: Smart<Relative<RawLength>> = Smart::Auto;
/// The right margin. /// The right margin.
pub const RIGHT: Smart<Relative<Length>> = Smart::Auto; pub const RIGHT: Smart<Relative<RawLength>> = Smart::Auto;
/// The top margin. /// The top margin.
pub const TOP: Smart<Relative<Length>> = Smart::Auto; pub const TOP: Smart<Relative<RawLength>> = Smart::Auto;
/// The bottom margin. /// The bottom margin.
pub const BOTTOM: Smart<Relative<Length>> = Smart::Auto; pub const BOTTOM: Smart<Relative<RawLength>> = Smart::Auto;
/// The page's background color. /// The page's background color.
pub const FILL: Option<Paint> = None; pub const FILL: Option<Paint> = None;
/// How many columns the page has. /// How many columns the page has.
@ -42,8 +44,8 @@ impl PageNode {
let mut styles = StyleMap::new(); let mut styles = StyleMap::new();
if let Some(paper) = args.named_or_find::<Paper>("paper")? { if let Some(paper) = args.named_or_find::<Paper>("paper")? {
styles.set(Self::WIDTH, Smart::Custom(paper.width())); styles.set(Self::WIDTH, Smart::Custom(paper.width().into()));
styles.set(Self::HEIGHT, Smart::Custom(paper.height())); styles.set(Self::HEIGHT, Smart::Custom(paper.height().into()));
} }
styles.set_opt(Self::WIDTH, args.named("width")?); styles.set_opt(Self::WIDTH, args.named("width")?);
@ -115,7 +117,7 @@ impl PageNode {
} }
// Layout the child. // 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, &regions, styles)?; let mut frames = child.layout(ctx, &regions, styles)?;
let header = styles.get(Self::HEADER); let header = styles.get(Self::HEADER);
@ -124,7 +126,7 @@ impl PageNode {
// Realize header and footer. // Realize header and footer.
for frame in &mut frames { for frame in &mut frames {
let size = frame.size; let size = frame.size;
let padding = padding.resolve(size); let padding = padding.resolve(styles).relative_to(size);
for (y, h, marginal) in [ for (y, h, marginal) in [
(Length::zero(), padding.top, header), (Length::zero(), padding.top, header),
(size.y - padding.bottom, padding.bottom, footer), (size.y - padding.bottom, padding.bottom, footer),
@ -133,7 +135,7 @@ impl PageNode {
let pos = Point::new(padding.left, y); let pos = Point::new(padding.left, y);
let w = size.x - padding.left - padding.right; let w = size.x - padding.left - padding.right;
let area = Size::new(w, h); 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); let sub = Layout::layout(&content, ctx, &pod, styles)?.remove(0);
Arc::make_mut(frame).push_frame(pos, sub); Arc::make_mut(frame).push_frame(pos, sub);
} }

View File

@ -8,12 +8,12 @@ pub struct PlaceNode(pub LayoutNode);
#[node] #[node]
impl PlaceNode { impl PlaceNode {
fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> { fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> {
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 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")?; let body: LayoutNode = args.expect("body")?;
Ok(Content::block(Self( Ok(Content::block(Self(
body.moved(Point::new(tx, ty)).aligned(aligns), body.moved(Spec::new(dx, dy)).aligned(aligns),
))) )))
} }
} }

View File

@ -24,7 +24,7 @@ impl VNode {
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum Spacing { pub enum Spacing {
/// Spacing specified in absolute terms and relative to the parent's size. /// Spacing specified in absolute terms and relative to the parent's size.
Relative(Relative<Length>), Relative(Relative<RawLength>),
/// Spacing specified as a fraction of the remaining free space in the parent. /// Spacing specified as a fraction of the remaining free space in the parent.
Fractional(Fraction), Fractional(Fraction),
} }

View File

@ -30,7 +30,7 @@ impl Layout for StackNode {
regions: &Regions, regions: &Regions,
styles: StyleChain, styles: StyleChain,
) -> TypResult<Vec<Arc<Frame>>> { ) -> TypResult<Vec<Arc<Frame>>> {
let mut layouter = StackLayouter::new(self.dir, regions); let mut layouter = StackLayouter::new(self.dir, regions, styles);
// Spacing to insert before the next node. // Spacing to insert before the next node.
let mut deferred = None; let mut deferred = None;
@ -85,13 +85,15 @@ castable! {
} }
/// Performs stack layout. /// Performs stack layout.
pub struct StackLayouter { pub struct StackLayouter<'a> {
/// The stacking direction. /// The stacking direction.
dir: Dir, dir: Dir,
/// The axis of the stacking direction. /// The axis of the stacking direction.
axis: SpecAxis, axis: SpecAxis,
/// The regions to layout children into. /// The regions to layout children into.
regions: Regions, regions: Regions,
/// The inherited styles.
styles: StyleChain<'a>,
/// Whether the stack itself should expand to fill the region. /// Whether the stack itself should expand to fill the region.
expand: Spec<bool>, expand: Spec<bool>,
/// The full size of the current region that was available at the start. /// The full size of the current region that was available at the start.
@ -117,9 +119,9 @@ enum StackItem {
Frame(Arc<Frame>, Align), Frame(Arc<Frame>, Align),
} }
impl StackLayouter { impl<'a> StackLayouter<'a> {
/// Create a new stack layouter. /// 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 axis = dir.axis();
let expand = regions.expand; let expand = regions.expand;
let full = regions.first; let full = regions.first;
@ -132,6 +134,7 @@ impl StackLayouter {
dir, dir,
axis, axis,
regions, regions,
styles,
expand, expand,
full, full,
used: Gen::zero(), used: Gen::zero(),
@ -146,7 +149,8 @@ impl StackLayouter {
match spacing { match spacing {
Spacing::Relative(v) => { Spacing::Relative(v) => {
// Resolve the spacing and limit it to the remaining space. // 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 remaining = self.regions.first.get_mut(self.axis);
let limited = resolved.min(*remaining); let limited = resolved.min(*remaining);
*remaining -= limited; *remaining -= limited;
@ -219,7 +223,7 @@ impl StackLayouter {
for item in self.items.drain(..) { for item in self.items.drain(..) {
match item { match item {
StackItem::Absolute(v) => cursor += v, 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) => { StackItem::Frame(frame, align) => {
if self.dir.is_positive() { if self.dir.is_positive() {
ruler = ruler.max(align); ruler = ruler.max(align);

View File

@ -186,15 +186,3 @@ castable! {
Expected: "content", Expected: "content",
Value::Content(content) => content.pack(), Value::Content(content) => content.pack(),
} }
castable! {
Spec<Relative<Length>>,
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")?,
}
},
}

View File

@ -10,8 +10,8 @@ pub use typst_macros::node;
pub use crate::diag::{with_alternative, At, Error, StrResult, TypError, TypResult}; pub use crate::diag::{with_alternative, At, Error, StrResult, TypError, TypResult};
pub use crate::eval::{ pub use crate::eval::{
Arg, Args, Array, Cast, Content, Dict, Fold, Func, Key, Layout, LayoutNode, Merge, Arg, Args, Array, Cast, Content, Dict, Fold, Func, Key, Layout, LayoutNode, Merge,
Node, RawAlign, Regions, Resolve, Scope, Show, ShowNode, Smart, StyleChain, StyleMap, Node, RawAlign, RawLength, Regions, Resolve, Scope, Show, ShowNode, Smart,
StyleVec, Value, StyleChain, StyleMap, StyleVec, Value,
}; };
pub use crate::frame::*; pub use crate::frame::*;
pub use crate::geom::*; pub use crate::geom::*;

View File

@ -1,5 +1,5 @@
use crate::library::prelude::*; use crate::library::prelude::*;
use crate::library::text::{FontFamily, FontSize, TextNode, Toggle}; use crate::library::text::{FontFamily, TextNode, TextSize, Toggle};
/// A section heading. /// A section heading.
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
@ -21,9 +21,9 @@ impl HeadingNode {
pub const FILL: Leveled<Smart<Paint>> = Leveled::Value(Smart::Auto); pub const FILL: Leveled<Smart<Paint>> = Leveled::Value(Smart::Auto);
/// The size of text in the heading. /// The size of text in the heading.
#[property(referenced)] #[property(referenced)]
pub const SIZE: Leveled<FontSize> = Leveled::Mapping(|level| { pub const SIZE: Leveled<TextSize> = Leveled::Mapping(|level| {
let upscale = (1.6 - 0.1 * level as f64).max(0.75); 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. /// Whether text in the heading is strengthend.
#[property(referenced)] #[property(referenced)]
@ -36,10 +36,10 @@ impl HeadingNode {
pub const UNDERLINE: Leveled<bool> = Leveled::Value(false); pub const UNDERLINE: Leveled<bool> = Leveled::Value(false);
/// The extra padding above the heading. /// The extra padding above the heading.
#[property(referenced)] #[property(referenced)]
pub const ABOVE: Leveled<Length> = Leveled::Value(Length::zero()); pub const ABOVE: Leveled<RawLength> = Leveled::Value(Length::zero().into());
/// The extra padding below the heading. /// The extra padding below the heading.
#[property(referenced)] #[property(referenced)]
pub const BELOW: Leveled<Length> = Leveled::Value(Length::zero()); pub const BELOW: Leveled<RawLength> = Leveled::Value(Length::zero().into());
/// Whether the heading is block-level. /// Whether the heading is block-level.
#[property(referenced)] #[property(referenced)]
pub const BLOCK: Leveled<bool> = Leveled::Value(true); pub const BLOCK: Leveled<bool> = Leveled::Value(true);
@ -95,14 +95,14 @@ impl Show for HeadingNode {
let above = resolve!(Self::ABOVE); let above = resolve!(Self::ABOVE);
if !above.is_zero() { if !above.is_zero() {
seq.push(Content::Vertical(above.into())); seq.push(Content::Vertical(above.resolve(styles).into()));
} }
seq.push(body); seq.push(body);
let below = resolve!(Self::BELOW); let below = resolve!(Self::BELOW);
if !below.is_zero() { 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); let mut content = Content::sequence(seq).styled_with_map(map);

View File

@ -1,6 +1,6 @@
use crate::library::layout::{GridNode, TrackSizing}; use crate::library::layout::{GridNode, TrackSizing};
use crate::library::prelude::*; use crate::library::prelude::*;
use crate::library::text::{ParNode, TextNode}; use crate::library::text::ParNode;
use crate::library::utility::Numbering; use crate::library::utility::Numbering;
use crate::parse::Scanner; use crate::parse::Scanner;
@ -34,15 +34,20 @@ impl<const L: ListKind> ListNode<L> {
#[property(referenced)] #[property(referenced)]
pub const LABEL: Label = Label::Default; pub const LABEL: Label = Label::Default;
/// The spacing between the list items of a non-wide list. /// The spacing between the list items of a non-wide list.
pub const SPACING: Relative<Length> = Relative::zero(); #[property(resolve)]
pub const SPACING: RawLength = RawLength::zero();
/// The indentation of each item's label. /// The indentation of each item's label.
pub const INDENT: Relative<Length> = Ratio::new(0.0).into(); #[property(resolve)]
pub const INDENT: RawLength = RawLength::zero();
/// The space between the label and the body of each item. /// The space between the label and the body of each item.
pub const BODY_INDENT: Relative<Length> = Ratio::new(0.5).into(); #[property(resolve)]
pub const BODY_INDENT: RawLength = Em::new(0.5).into();
/// The extra padding above the list. /// 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. /// 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<Content> { fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> {
Ok(Content::show(Self { Ok(Content::show(Self {
@ -77,7 +82,6 @@ impl<const L: ListKind> Show for ListNode<L> {
number += 1; number += 1;
} }
let em = styles.get(TextNode::SIZE);
let leading = styles.get(ParNode::LEADING); let leading = styles.get(ParNode::LEADING);
let spacing = if self.wide { let spacing = if self.wide {
styles.get(ParNode::SPACING) styles.get(ParNode::SPACING)
@ -85,9 +89,9 @@ impl<const L: ListKind> Show for ListNode<L> {
styles.get(Self::SPACING) styles.get(Self::SPACING)
}; };
let gutter = (leading + spacing).resolve(em); let gutter = leading + spacing;
let indent = styles.get(Self::INDENT).resolve(em); let indent = styles.get(Self::INDENT);
let body_indent = styles.get(Self::BODY_INDENT).resolve(em); let body_indent = styles.get(Self::BODY_INDENT);
Content::block(GridNode { Content::block(GridNode {
tracks: Spec::with_x(vec![ tracks: Spec::with_x(vec![

View File

@ -21,9 +21,10 @@ impl TableNode {
/// How to stroke the cells. /// How to stroke the cells.
pub const STROKE: Option<Paint> = Some(Color::BLACK.into()); pub const STROKE: Option<Paint> = Some(Color::BLACK.into());
/// The stroke's thickness. /// 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. /// How much to pad the cells's content.
pub const PADDING: Relative<Length> = Length::pt(5.0).into(); pub const PADDING: Relative<RawLength> = Length::pt(5.0).into();
fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> { fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> {
let columns = args.named("columns")?.unwrap_or_default(); let columns = args.named("columns")?.unwrap_or_default();

View File

@ -23,16 +23,16 @@ impl<const L: DecoLine> DecoNode<L> {
/// Stroke color of the line, defaults to the text color if `None`. /// Stroke color of the line, defaults to the text color if `None`.
#[property(shorthand)] #[property(shorthand)]
pub const STROKE: Option<Paint> = None; pub const STROKE: Option<Paint> = None;
/// Thickness of the line's strokes (dependent on scaled font size), read /// Thickness of the line's strokes, read from the font tables if `auto`.
/// from the font tables if `None`. #[property(shorthand, resolve)]
#[property(shorthand)] pub const THICKNESS: Smart<RawLength> = Smart::Auto;
pub const THICKNESS: Option<Relative<Length>> = None; /// Position of the line relative to the baseline, read from the font tables
/// Position of the line relative to the baseline (dependent on scaled font /// if `auto`.
/// size), read from the font tables if `None`. #[property(resolve)]
pub const OFFSET: Option<Relative<Length>> = None; pub const OFFSET: Smart<RawLength> = Smart::Auto;
/// Amount that the line will be longer or shorter than its associated text /// Amount that the line will be longer or shorter than its associated text.
/// (dependent on scaled font size). #[property(resolve)]
pub const EXTENT: Relative<Length> = Relative::zero(); pub const EXTENT: RawLength = RawLength::zero();
/// Whether the line skips sections in which it would collide /// Whether the line skips sections in which it would collide
/// with the glyphs. Does not apply to strikethrough. /// with the glyphs. Does not apply to strikethrough.
pub const EVADE: bool = true; pub const EVADE: bool = true;
@ -66,9 +66,9 @@ impl<const L: DecoLine> Show for DecoNode<L> {
pub struct Decoration { pub struct Decoration {
pub line: DecoLine, pub line: DecoLine,
pub stroke: Option<Paint>, pub stroke: Option<Paint>,
pub thickness: Option<Relative<Length>>, pub thickness: Smart<Length>,
pub offset: Option<Relative<Length>>, pub offset: Smart<Length>,
pub extent: Relative<Length>, pub extent: Length,
pub evade: bool, pub evade: bool,
} }
@ -102,25 +102,18 @@ pub fn decorate(
}; };
let evade = deco.evade && deco.line != STRIKETHROUGH; let evade = deco.evade && deco.line != STRIKETHROUGH;
let extent = deco.extent.resolve(text.size); let offset = deco.offset.unwrap_or(-metrics.position.at(text.size));
let offset = deco
.offset
.map(|s| s.resolve(text.size))
.unwrap_or(-metrics.position.resolve(text.size));
let stroke = Stroke { let stroke = Stroke {
paint: deco.stroke.unwrap_or(text.fill), paint: deco.stroke.unwrap_or(text.fill),
thickness: deco thickness: deco.thickness.unwrap_or(metrics.thickness.at(text.size)),
.thickness
.map(|s| s.resolve(text.size))
.unwrap_or(metrics.thickness.resolve(text.size)),
}; };
let gap_padding = 0.08 * text.size; let gap_padding = 0.08 * text.size;
let min_width = 0.162 * text.size; let min_width = 0.162 * text.size;
let mut start = pos.x - extent; let mut start = pos.x - deco.extent;
let end = pos.x + (width + 2.0 * extent); let end = pos.x + (width + 2.0 * deco.extent);
let mut push_segment = |from: Length, to: Length| { let mut push_segment = |from: Length, to: Length| {
let origin = Point::new(from, pos.y + offset); let origin = Point::new(from, pos.y + offset);
@ -146,20 +139,20 @@ pub fn decorate(
let mut intersections = vec![]; let mut intersections = vec![];
for glyph in text.glyphs.iter() { 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 = let mut builder =
BezPathBuilder::new(face_metrics.units_per_em, text.size, dx.to_raw()); BezPathBuilder::new(face_metrics.units_per_em, text.size, dx.to_raw());
let bbox = face.ttf().outline_glyph(GlyphId(glyph.id), &mut builder); let bbox = face.ttf().outline_glyph(GlyphId(glyph.id), &mut builder);
let path = builder.finish(); 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 // Only do the costly segments intersection test if the line
// intersects the bounding box. // intersects the bounding box.
if bbox.map_or(false, |bbox| { if bbox.map_or(false, |bbox| {
let y_min = -face.to_em(bbox.y_max).resolve(text.size); let y_min = -face.to_em(bbox.y_max).at(text.size);
let y_max = -face.to_em(bbox.y_min).resolve(text.size); let y_max = -face.to_em(bbox.y_min).at(text.size);
offset >= y_min && offset <= y_max offset >= y_min && offset <= y_max
}) { }) {
@ -225,7 +218,7 @@ impl BezPathBuilder {
} }
fn s(&self, v: f32) -> f64 { 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()
} }
} }

View File

@ -16,7 +16,9 @@ use std::borrow::Cow;
use ttf_parser::Tag; 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::library::prelude::*;
use crate::util::EcoString; use crate::util::EcoString;
@ -39,23 +41,25 @@ impl TextNode {
pub const WEIGHT: FontWeight = FontWeight::REGULAR; pub const WEIGHT: FontWeight = FontWeight::REGULAR;
/// The width of the glyphs. /// The width of the glyphs.
pub const STRETCH: FontStretch = FontStretch::NORMAL; pub const STRETCH: FontStretch = FontStretch::NORMAL;
/// The size of the glyphs. /// The size of the glyphs.
#[property(shorthand, fold)] #[property(shorthand, fold)]
pub const SIZE: FontSize = Length::pt(11.0); pub const SIZE: TextSize = Length::pt(11.0);
/// The glyph fill color. /// The glyph fill color.
#[property(shorthand)] #[property(shorthand)]
pub const FILL: Paint = Color::BLACK.into(); pub const FILL: Paint = Color::BLACK.into();
/// The amount of space that should be added between characters. /// The amount of space that should be added between characters.
pub const TRACKING: Em = Em::zero(); #[property(resolve)]
/// The ratio by which spaces should be stretched. pub const TRACKING: RawLength = RawLength::zero();
pub const SPACING: Ratio = Ratio::one(); /// The width of spaces relative to the default space width.
#[property(resolve)]
pub const SPACING: Relative<RawLength> = Relative::one();
/// Whether glyphs can hang over into the margin. /// Whether glyphs can hang over into the margin.
pub const OVERHANG: bool = true; pub const OVERHANG: bool = true;
/// The top end of the text bounding box. /// 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. /// 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"). /// Whether to apply kerning ("kern").
pub const KERNING: bool = true; pub const KERNING: bool = true;
@ -188,44 +192,53 @@ castable! {
/// The size of text. /// The size of text.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct FontSize(pub Relative<Length>); pub struct TextSize(pub RawLength);
impl Fold for FontSize { impl Fold for TextSize {
type Output = Length; type Output = Length;
fn fold(self, outer: Self::Output) -> Self::Output { 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! { castable! {
FontSize, TextSize,
Expected: "relative length", Expected: "length",
Value::Length(v) => Self(v.into()), Value::Length(v) => Self(v),
Value::Ratio(v) => Self(v.into()), }
Value::Relative(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! { castable! {
Em, TextEdge,
Expected: "float", Expected: "string or length",
Value::Float(v) => Self::new(v), Value::Length(v) => Self::Length(v),
} Value::Str(string) => Self::Metric(match string.as_str() {
"ascender" => VerticalFontMetric::Ascender,
castable! { "cap-height" => VerticalFontMetric::CapHeight,
VerticalFontMetric, "x-height" => VerticalFontMetric::XHeight,
Expected: "string or relative length", "baseline" => VerticalFontMetric::Baseline,
Value::Length(v) => Self::Relative(v.into()), "descender" => VerticalFontMetric::Descender,
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,
_ => Err("unknown font metric")?, _ => Err("unknown font metric")?,
}, }),
} }
/// A stylistic set in a font face. /// A stylistic set in a font face.

View File

@ -42,12 +42,15 @@ impl ParNode {
/// Whether to hyphenate text to improve line breaking. When `auto`, words /// Whether to hyphenate text to improve line breaking. When `auto`, words
/// will will be hyphenated if and only if justification is enabled. /// will will be hyphenated if and only if justification is enabled.
pub const HYPHENATE: Smart<bool> = Smart::Auto; pub const HYPHENATE: Smart<bool> = Smart::Auto;
/// The spacing between lines (dependent on scaled font size). /// The spacing between lines.
pub const LEADING: Relative<Length> = Ratio::new(0.65).into(); #[property(resolve)]
/// The extra spacing between paragraphs (dependent on scaled font size). pub const LEADING: RawLength = Em::new(0.65).into();
pub const SPACING: Relative<Length> = Ratio::new(0.55).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. /// The indent the first line of a consecutive paragraph should have.
pub const INDENT: Relative<Length> = Relative::zero(); #[property(resolve)]
pub const INDENT: RawLength = RawLength::zero();
fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> { fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> {
// The paragraph constructor is special: It doesn't create a paragraph // The paragraph constructor is special: It doesn't create a paragraph
@ -370,7 +373,7 @@ fn prepare<'a>(
} }
ParChild::Spacing(spacing) => match *spacing { ParChild::Spacing(spacing) => match *spacing {
Spacing::Relative(v) => { 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)); items.push(ParItem::Absolute(resolved));
ranges.push(range); ranges.push(range);
} }
@ -772,8 +775,7 @@ fn stack(
regions: &Regions, regions: &Regions,
styles: StyleChain, styles: StyleChain,
) -> Vec<Arc<Frame>> { ) -> Vec<Arc<Frame>> {
let em = styles.get(TextNode::SIZE); let leading = styles.get(ParNode::LEADING);
let leading = styles.get(ParNode::LEADING).resolve(em);
let align = styles.get(ParNode::ALIGN); let align = styles.get(ParNode::ALIGN);
let justify = styles.get(ParNode::JUSTIFY); let justify = styles.get(ParNode::JUSTIFY);
@ -837,7 +839,7 @@ fn commit(
if text.styles.get(TextNode::OVERHANG) { if text.styles.get(TextNode::OVERHANG) {
let start = text.dir.is_positive(); let start = text.dir.is_positive();
let em = text.styles.get(TextNode::SIZE); 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; offset -= amount;
remaining += amount; remaining += amount;
} }
@ -852,7 +854,7 @@ fn commit(
{ {
let start = !text.dir.is_positive(); let start = !text.dir.is_positive();
let em = text.styles.get(TextNode::SIZE); 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; remaining += amount;
} }
} }
@ -887,7 +889,7 @@ fn commit(
match item { match item {
ParItem::Absolute(v) => offset += *v, 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::Text(shaped) => position(shaped.build(fonts, justification)),
ParItem::Frame(frame) => position(frame.clone()), ParItem::Frame(frame) => position(frame.clone()),
} }

View File

@ -132,7 +132,7 @@ impl<'a> ShapedText<'a> {
.filter(|g| g.is_justifiable()) .filter(|g| g.is_justifiable())
.map(|g| g.x_advance) .map(|g| g.x_advance)
.sum::<Em>() .sum::<Em>()
.resolve(self.styles.get(TextNode::SIZE)) .at(self.styles.get(TextNode::SIZE))
} }
/// Reshape a range of the shaped text, reusing information from this /// 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 glyph_id = ttf.glyph_index('-')?;
let x_advance = face.to_em(ttf.glyph_hor_advance(glyph_id)?); let x_advance = face.to_em(ttf.glyph_hor_advance(glyph_id)?);
let cluster = self.glyphs.last().map(|g| g.cluster).unwrap_or_default(); 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 { self.glyphs.to_mut().push(ShapedGlyph {
face_id, face_id,
glyph_id: glyph_id.0, 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. /// Apply tracking and spacing to a slice of shaped glyphs.
fn track_and_space(ctx: &mut ShapingContext) { fn track_and_space(ctx: &mut ShapingContext) {
let tracking = ctx.styles.get(TextNode::TRACKING); let em = ctx.styles.get(TextNode::SIZE);
let spacing = ctx.styles.get(TextNode::SPACING); 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() { if tracking.is_zero() && spacing.is_one() {
return; return;
} }
@ -452,7 +454,7 @@ fn track_and_space(ctx: &mut ShapingContext) {
let mut glyphs = ctx.glyphs.iter_mut().peekable(); let mut glyphs = ctx.glyphs.iter_mut().peekable();
while let Some(glyph) = glyphs.next() { while let Some(glyph) = glyphs.next() {
if glyph.is_space() { 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) { 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. // Expand top and bottom by reading the face's vertical metrics.
let mut expand = |face: &Face| { let mut expand = |face: &Face| {
let metrics = face.metrics(); let metrics = face.metrics();
top.set_max(metrics.vertical(top_edge, size)); top.set_max(top_edge.resolve(styles, metrics));
bottom.set_max(-metrics.vertical(bottom_edge, size)); bottom.set_max(-bottom_edge.resolve(styles, metrics));
}; };
if glyphs.is_empty() { if glyphs.is_empty() {
@ -499,7 +501,7 @@ fn measure(
expand(face); expand(face);
for glyph in group { for glyph in group {
width += glyph.x_advance.resolve(size); width += glyph.x_advance.at(size);
} }
} }
} }

View File

@ -37,12 +37,11 @@ pub fn abs(_: &mut Context, args: &mut Args) -> TypResult<Value> {
Ok(match v { Ok(match v {
Value::Int(v) => Value::Int(v.abs()), Value::Int(v) => Value::Int(v.abs()),
Value::Float(v) => Value::Float(v.abs()), Value::Float(v) => Value::Float(v.abs()),
Value::Length(v) => Value::Length(v.abs()),
Value::Angle(v) => Value::Angle(v.abs()), Value::Angle(v) => Value::Angle(v.abs()),
Value::Ratio(v) => Value::Ratio(v.abs()), Value::Ratio(v) => Value::Ratio(v.abs()),
Value::Fraction(v) => Value::Fraction(v.abs()), Value::Fraction(v) => Value::Fraction(v.abs()),
Value::Relative(_) => { Value::Length(_) | Value::Relative(_) => {
bail!(span, "cannot take absolute value of a relative length") bail!(span, "cannot take absolute value of a length")
} }
v => bail!(span, "expected numeric value, found {}", v.type_name()), v => bail!(span, "expected numeric value, found {}", v.type_name()),
}) })

View File

@ -478,10 +478,7 @@ fn literal(p: &mut Parser) -> bool {
| NodeKind::Int(_) | NodeKind::Int(_)
| NodeKind::Float(_) | NodeKind::Float(_)
| NodeKind::Bool(_) | NodeKind::Bool(_)
| NodeKind::Fraction(_) | NodeKind::Numeric(_, _)
| NodeKind::Length(_, _)
| NodeKind::Angle(_, _)
| NodeKind::Percentage(_)
| NodeKind::Str(_), | NodeKind::Str(_),
) => { ) => {
p.eat(); p.eat();

View File

@ -4,8 +4,8 @@ use super::{
is_id_continue, is_id_start, is_newline, resolve_hex, resolve_raw, resolve_string, is_id_continue, is_id_start, is_newline, resolve_hex, resolve_raw, resolve_string,
Scanner, Scanner,
}; };
use crate::geom::{AngularUnit, LengthUnit}; use crate::geom::{AngleUnit, LengthUnit};
use crate::syntax::ast::{MathNode, RawNode}; use crate::syntax::ast::{MathNode, RawNode, Unit};
use crate::syntax::{ErrorPos, NodeKind}; use crate::syntax::{ErrorPos, NodeKind};
use crate::util::EcoString; use crate::util::EcoString;
@ -462,7 +462,8 @@ impl<'s> Tokens<'s> {
} }
// Read the exponent. // 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('-'); let _ = self.s.eat_if('+') || self.s.eat_if('-');
self.s.eat_while(|c| c.is_ascii_digit()); self.s.eat_while(|c| c.is_ascii_digit());
} }
@ -487,14 +488,15 @@ impl<'s> Tokens<'s> {
if let Ok(f) = number.parse::<f64>() { if let Ok(f) = number.parse::<f64>() {
match suffix { match suffix {
"" => NodeKind::Float(f), "" => NodeKind::Float(f),
"%" => NodeKind::Percentage(f), "pt" => NodeKind::Numeric(f, Unit::Length(LengthUnit::Pt)),
"fr" => NodeKind::Fraction(f), "mm" => NodeKind::Numeric(f, Unit::Length(LengthUnit::Mm)),
"pt" => NodeKind::Length(f, LengthUnit::Pt), "cm" => NodeKind::Numeric(f, Unit::Length(LengthUnit::Cm)),
"mm" => NodeKind::Length(f, LengthUnit::Mm), "in" => NodeKind::Numeric(f, Unit::Length(LengthUnit::In)),
"cm" => NodeKind::Length(f, LengthUnit::Cm), "deg" => NodeKind::Numeric(f, Unit::Angle(AngleUnit::Deg)),
"in" => NodeKind::Length(f, LengthUnit::In), "rad" => NodeKind::Numeric(f, Unit::Angle(AngleUnit::Rad)),
"deg" => NodeKind::Angle(f, AngularUnit::Deg), "em" => NodeKind::Numeric(f, Unit::Em),
"rad" => NodeKind::Angle(f, AngularUnit::Rad), "fr" => NodeKind::Numeric(f, Unit::Fr),
"%" => NodeKind::Numeric(f, Unit::Percent),
_ => NodeKind::Unknown(all.into()), _ => NodeKind::Unknown(all.into()),
} }
} else { } else {
@ -1017,19 +1019,20 @@ mod tests {
// Combined integers and floats. // Combined integers and floats.
let nums = ints.iter().map(|&(k, v)| (k, v as f64)).chain(floats); let nums = ints.iter().map(|&(k, v)| (k, v as f64)).chain(floats);
let suffixes = [ let suffixes: &[(&str, fn(f64) -> NodeKind)] = &[
("%", Percentage as fn(f64) -> NodeKind), ("mm", |x| Numeric(x, Unit::Length(LengthUnit::Mm))),
("fr", Fraction as fn(f64) -> NodeKind), ("pt", |x| Numeric(x, Unit::Length(LengthUnit::Pt))),
("mm", |x| Length(x, LengthUnit::Mm)), ("cm", |x| Numeric(x, Unit::Length(LengthUnit::Cm))),
("pt", |x| Length(x, LengthUnit::Pt)), ("in", |x| Numeric(x, Unit::Length(LengthUnit::In))),
("cm", |x| Length(x, LengthUnit::Cm)), ("rad", |x| Numeric(x, Unit::Angle(AngleUnit::Rad))),
("in", |x| Length(x, LengthUnit::In)), ("deg", |x| Numeric(x, Unit::Angle(AngleUnit::Deg))),
("rad", |x| Angle(x, AngularUnit::Rad)), ("em", |x| Numeric(x, Unit::Em)),
("deg", |x| Angle(x, AngularUnit::Deg)), ("fr", |x| Numeric(x, Unit::Fr)),
("%", |x| Numeric(x, Unit::Percent)),
]; ];
// Numeric types. // Numeric types.
for &(suffix, build) in &suffixes { for &(suffix, build) in suffixes {
for (s, v) in nums.clone() { for (s, v) in nums.clone() {
t!(Code[" /"]: format!("{}{}", s, suffix) => build(v)); t!(Code[" /"]: format!("{}{}", s, suffix) => build(v));
} }
@ -1112,6 +1115,6 @@ mod tests {
// Test invalid number suffixes. // Test invalid number suffixes.
t!(Code[" /"]: "1foo" => Invalid("1foo")); t!(Code[" /"]: "1foo" => Invalid("1foo"));
t!(Code: "1p%" => Invalid("1p"), Invalid("%")); t!(Code: "1p%" => Invalid("1p"), Invalid("%"));
t!(Code: "1%%" => Percentage(1.0), Invalid("%")); t!(Code: "1%%" => Numeric(1.0, Unit::Percent), Invalid("%"));
} }
} }

View File

@ -5,7 +5,7 @@
use std::ops::Deref; use std::ops::Deref;
use super::{Green, GreenData, NodeKind, RedNode, RedRef, Span}; use super::{Green, GreenData, NodeKind, RedNode, RedRef, Span};
use crate::geom::{AngularUnit, LengthUnit}; use crate::geom::{AngleUnit, LengthUnit};
use crate::util::EcoString; use crate::util::EcoString;
/// A typed AST node. /// A typed AST node.
@ -352,10 +352,7 @@ node! {
| NodeKind::Bool(_) | NodeKind::Bool(_)
| NodeKind::Int(_) | NodeKind::Int(_)
| NodeKind::Float(_) | NodeKind::Float(_)
| NodeKind::Length(_, _) | NodeKind::Numeric(_, _)
| NodeKind::Angle(_, _)
| NodeKind::Percentage(_)
| NodeKind::Fraction(_)
| NodeKind::Str(_) | NodeKind::Str(_)
} }
@ -368,10 +365,7 @@ impl Lit {
NodeKind::Bool(v) => LitKind::Bool(v), NodeKind::Bool(v) => LitKind::Bool(v),
NodeKind::Int(v) => LitKind::Int(v), NodeKind::Int(v) => LitKind::Int(v),
NodeKind::Float(v) => LitKind::Float(v), NodeKind::Float(v) => LitKind::Float(v),
NodeKind::Length(v, unit) => LitKind::Length(v, unit), NodeKind::Numeric(v, unit) => LitKind::Numeric(v, unit),
NodeKind::Angle(v, unit) => LitKind::Angle(v, unit),
NodeKind::Percentage(v) => LitKind::Percent(v),
NodeKind::Fraction(v) => LitKind::Fractional(v),
NodeKind::Str(ref v) => LitKind::Str(v.clone()), NodeKind::Str(ref v) => LitKind::Str(v.clone()),
_ => panic!("literal is of wrong kind"), _ => panic!("literal is of wrong kind"),
} }
@ -391,21 +385,27 @@ pub enum LitKind {
Int(i64), Int(i64),
/// A floating-point literal: `1.2`, `10e-4`. /// A floating-point literal: `1.2`, `10e-4`.
Float(f64), Float(f64),
/// A length literal: `12pt`, `3cm`. /// A numeric literal with a unit: `12pt`, `3cm`, `2em`, `90deg`, `50%`.
Length(f64, LengthUnit), Numeric(f64, Unit),
/// 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 string literal: `"hello!"`. /// A string literal: `"hello!"`.
Str(EcoString), 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! { node! {
/// A code block: `{ let x = 1; x + 2 }`. /// A code block: `{ let x = 1; x + 2 }`.
CodeBlock: CodeBlock CodeBlock: CodeBlock

View File

@ -187,10 +187,7 @@ impl Category {
NodeKind::Bool(_) => Some(Category::Bool), NodeKind::Bool(_) => Some(Category::Bool),
NodeKind::Int(_) => Some(Category::Number), NodeKind::Int(_) => Some(Category::Number),
NodeKind::Float(_) => Some(Category::Number), NodeKind::Float(_) => Some(Category::Number),
NodeKind::Length(_, _) => Some(Category::Number), NodeKind::Numeric(_, _) => Some(Category::Number),
NodeKind::Angle(_, _) => Some(Category::Number),
NodeKind::Percentage(_) => Some(Category::Number),
NodeKind::Fraction(_) => Some(Category::Number),
NodeKind::Str(_) => Some(Category::String), NodeKind::Str(_) => Some(Category::String),
NodeKind::Error(_, _) => Some(Category::Invalid), NodeKind::Error(_, _) => Some(Category::Invalid),
NodeKind::Unknown(_) => Some(Category::Invalid), NodeKind::Unknown(_) => Some(Category::Invalid),

View File

@ -12,9 +12,8 @@ use std::sync::Arc;
pub use highlight::*; pub use highlight::*;
pub use span::*; pub use span::*;
use self::ast::{MathNode, RawNode, TypedNode}; use self::ast::{MathNode, RawNode, TypedNode, Unit};
use crate::diag::Error; use crate::diag::Error;
use crate::geom::{AngularUnit, LengthUnit};
use crate::parse::TokenMode; use crate::parse::TokenMode;
use crate::source::SourceId; use crate::source::SourceId;
use crate::util::EcoString; use crate::util::EcoString;
@ -629,17 +628,8 @@ pub enum NodeKind {
Int(i64), Int(i64),
/// A floating-point number: `1.2`, `10e-4`. /// A floating-point number: `1.2`, `10e-4`.
Float(f64), Float(f64),
/// A length: `12pt`, `3cm`. /// A numeric value with a unit: `12pt`, `3cm`, `2em`, `90deg`, `50%`.
Length(f64, LengthUnit), Numeric(f64, Unit),
/// 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 quoted string: `"..."`. /// A quoted string: `"..."`.
Str(EcoString), Str(EcoString),
/// A code block: `{ let x = 1; x + 2 }`. /// A code block: `{ let x = 1; x + 2 }`.
@ -886,10 +876,7 @@ impl NodeKind {
Self::Bool(_) => "boolean", Self::Bool(_) => "boolean",
Self::Int(_) => "integer", Self::Int(_) => "integer",
Self::Float(_) => "float", Self::Float(_) => "float",
Self::Length(_, _) => "length", Self::Numeric(_, _) => "numeric value",
Self::Angle(_, _) => "angle",
Self::Percentage(_) => "percentage",
Self::Fraction(_) => "`fr` value",
Self::Str(_) => "string", Self::Str(_) => "string",
Self::CodeBlock => "code block", Self::CodeBlock => "code block",
Self::ContentBlock => "content block", Self::ContentBlock => "content block",
@ -1010,10 +997,7 @@ impl Hash for NodeKind {
Self::Bool(v) => v.hash(state), Self::Bool(v) => v.hash(state),
Self::Int(v) => v.hash(state), Self::Int(v) => v.hash(state),
Self::Float(v) => v.to_bits().hash(state), Self::Float(v) => v.to_bits().hash(state),
Self::Length(v, u) => (v.to_bits(), u).hash(state), Self::Numeric(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::Str(v) => v.hash(state), Self::Str(v) => v.hash(state),
Self::CodeBlock => {} Self::CodeBlock => {}
Self::ContentBlock => {} Self::ContentBlock => {}

BIN
tests/ref/text/edge.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -63,8 +63,8 @@
test(v + v, 2.0 * v) test(v + v, 2.0 * v)
} }
// Relative lengths cannot be divided by themselves. // Lengths cannot be divided by themselves.
if type(v) != "relative length" { if "length" not in type(v) {
test(v / v, 1.0) test(v / v, 1.0)
test(v / v == 1, true) test(v / v == 1, true)
} }

View File

@ -5,7 +5,7 @@
header: align(horizon, { header: align(horizon, {
text(eastern)[*Typst*] text(eastern)[*Typst*]
h(1fr) h(1fr)
text(80%)[_Chapter 1_] text(0.8em)[_Chapter 1_]
}), }),
footer: page => v(5pt) + align(center)[\~ #page \~], footer: page => v(5pt) + align(center)[\~ #page \~],
) )

View File

@ -22,7 +22,7 @@
--- ---
// The inner rectangle should also be yellow here. // The inner rectangle should also be yellow here.
// (and therefore invisible) // (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. // 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. // The inner list should not be indented extra.
[#set text(100%);#list(indent: 20pt, list[A])] [#set text(1em);#list(indent: 20pt, list[A])]

View File

@ -3,11 +3,11 @@
#set page("a8", footer: p => v(-5pt) + align(right, [#p])) #set page("a8", footer: p => v(-5pt) + align(right, [#p]))
#let i = 1 #let i = 1
#set heading(size: 100%) #set heading(size: 1em)
#show heading(level, body) as { #show heading(level, body) as {
if level == 1 { if level == 1 {
v(10pt) v(10pt)
underline(text(150%, blue)[{i}. #body]) underline(text(1.5em, blue)[{i}. #body])
i += 1 i += 1
} else { } else {
text(red, body) text(red, body)
@ -29,7 +29,7 @@ Some more text.
Another 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] #show heading(a, b) as [B]
A [= Heading] C A [= Heading] C

View File

@ -2,10 +2,10 @@
--- ---
#set page(height: 130pt) #set page(height: 130pt)
#set text(70%) #set text(0.7em)
#align(center)[ #align(center)[
#text(130%)[*Essay on typography*] \ #text(1.3em)[*Essay on typography*] \
T. Ypst T. Ypst
] ]
@ -20,7 +20,7 @@ A [_B #wrap c in [*#c*]; C_] D
--- ---
// Test wrap style precedence. // Test wrap style precedence.
#set text(fill: eastern, size: 150%) #set text(fill: eastern, size: 1.5em)
#wrap body in text(fill: forest, body) #wrap body in text(fill: forest, body)
Forest Forest

View File

@ -1,4 +1,4 @@
// Test text baseline. // Test text baseline.
--- ---
Hi #text(150%)[You], #text(75%)[how are you?] Hi #text(1.5em)[You], #text(0.75em)[how are you?]

View File

@ -19,11 +19,11 @@
#overline(underline[Running amongst the wolves.]) #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( #let highlight = strike.with(
stroke: rgb("abcdef88"), stroke: rgb("abcdef88"),
thickness: 10pt, thickness: 10pt,
extent: 5%, extent: 0.05em,
) )
// Abuse thickness and transparency for redacting and highlighting stuff. // Abuse thickness and transparency for redacting and highlighting stuff.

25
tests/typ/text/edge.typ Normal file
View File

@ -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: "")

View File

@ -4,14 +4,31 @@
#set text(size: 5pt) #set text(size: 5pt)
A // 5pt A // 5pt
[ [
#set text(size: 200%) #set text(size: 2em)
B // 10pt B // 10pt
[ [
#set text(size: 150% + 1pt) #set text(size: 1.5em + 1pt)
C // 16pt C // 16pt
#text(size: 200%)[D] // 32pt #text(size: 2em)[D] // 32pt
E // 16pt E // 16pt
] ]
F // 10pt F // 10pt
] ]
G // 5pt 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)

View File

@ -3,8 +3,8 @@
--- ---
// Set same font size in three different ways. // Set same font size in three different ways.
#text(20pt)[A] #text(20pt)[A]
#text(200%)[A] #text(2em)[A]
#text(size: 15pt + 50%)[A] #text(size: 15pt + 0.5em)[A]
// Do nothing. // Do nothing.
#text()[Normal] #text()[Normal]
@ -38,23 +38,6 @@ Emoji: 🐪, 🌋, 🏞
#set text("PT Sans", "Twitter Color Emoji", fallback: false) #set text("PT Sans", "Twitter Color Emoji", fallback: false)
= 𝛼 + 𝛽. = 𝛼 + 𝛽.
---
// 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 // Error: 11-16 unexpected argument
#set text(false) #set text(false)
@ -63,14 +46,6 @@ Emoji: 🐪, 🌋, 🏞
// Error: 18-24 expected "normal", "italic" or "oblique" // Error: 18-24 expected "normal", "italic" or "oblique"
#set text(style: "bold", weight: "thin") #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 // Error: 23-27 unexpected argument
#set text(size: 10pt, 12pt) #set text(size: 10pt, 12pt)

View File

@ -31,7 +31,7 @@ A #set par(spacing: 0pt, leading: 0pt); B #parbreak() C
--- ---
// Test weird metrics. // Test weird metrics.
#set par(spacing: 100%, leading: 0pt) #set par(spacing: 1em, leading: 0pt)
But, soft! what light through yonder window breaks? But, soft! what light through yonder window breaks?
It is the east, and Juliet is the sun. It is the east, and Juliet is the sun.

View File

@ -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.

View File

@ -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)
טֶקסט

View File

@ -35,8 +35,20 @@
#test(abs(-0.0), 0.0) #test(abs(-0.0), 0.0)
#test(abs(0.0), -0.0) #test(abs(0.0), -0.0)
#test(abs(-3.14), 3.14) #test(abs(-3.14), 3.14)
#test(abs(-12pt), 12pt)
#test(abs(50%), 50%) #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. // Test the `even` and `odd` functions.
@ -61,14 +73,6 @@
// Error: 11-14 divisor must not be zero // Error: 11-14 divisor must not be zero
#mod(3.0, 0.0) #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 the `min` and `max` functions.
#test(min(2, -4), -4) #test(min(2, -4), -4)

View File

@ -13,7 +13,7 @@ use typst::eval::{Smart, StyleMap, Value};
use typst::frame::{Element, Frame}; use typst::frame::{Element, Frame};
use typst::geom::{Length, RgbaColor}; use typst::geom::{Length, RgbaColor};
use typst::library::layout::PageNode; use typst::library::layout::PageNode;
use typst::library::text::{FontSize, TextNode}; use typst::library::text::{TextNode, TextSize};
use typst::loading::FsLoader; use typst::loading::FsLoader;
use typst::parse::Scanner; use typst::parse::Scanner;
use typst::source::SourceFile; use typst::source::SourceFile;
@ -61,13 +61,13 @@ fn main() {
// exactly 100pt wide. Page height is unbounded and font size is 10pt so // exactly 100pt wide. Page height is unbounded and font size is 10pt so
// that it multiplies to nice round numbers. // that it multiplies to nice round numbers.
let mut styles = StyleMap::new(); 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::HEIGHT, Smart::Auto);
styles.set(PageNode::LEFT, Smart::Custom(Length::pt(10.0).into())); 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::TOP, Smart::Custom(Length::pt(10.0).into()));
styles.set(PageNode::RIGHT, 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(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. // Hook up an assert function into the global scope.
let mut std = typst::library::new(); let mut std = typst::library::new();