summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/eval/layout.rs8
-rw-r--r--src/eval/ops.rs41
-rw-r--r--src/eval/raw.rs134
-rw-r--r--src/eval/styles.rs44
-rw-r--r--src/eval/value.rs70
-rw-r--r--src/export/pdf.rs6
-rw-r--r--src/export/render.rs4
-rw-r--r--src/frame.rs43
-rw-r--r--src/geom/paint.rs28
-rw-r--r--src/library/graphics/line.rs17
-rw-r--r--src/library/graphics/shape.rs15
-rw-r--r--src/library/mod.rs62
-rw-r--r--src/library/prelude.rs2
-rw-r--r--src/library/structure/table.rs10
-rw-r--r--src/library/text/deco.rs27
15 files changed, 328 insertions, 183 deletions
diff --git a/src/eval/layout.rs b/src/eval/layout.rs
index f92a31f5..117c269a 100644
--- a/src/eval/layout.rs
+++ b/src/eval/layout.rs
@@ -7,8 +7,8 @@ use std::sync::Arc;
use super::{Barrier, RawAlign, RawLength, Resolve, StyleChain};
use crate::diag::TypResult;
-use crate::frame::{Element, Frame, Geometry, Shape, Stroke};
-use crate::geom::{Align, Length, Paint, Point, Relative, Sides, Size, Spec};
+use crate::frame::{Element, Frame, Geometry};
+use crate::geom::{Align, Length, Paint, Point, Relative, Sides, Size, Spec, Stroke};
use crate::library::graphics::MoveNode;
use crate::library::layout::{AlignNode, PadNode};
use crate::util::Prehashed;
@@ -349,7 +349,7 @@ impl Layout for FillNode {
) -> TypResult<Vec<Arc<Frame>>> {
let mut frames = self.child.layout(ctx, regions, styles)?;
for frame in &mut frames {
- let shape = Shape::filled(Geometry::Rect(frame.size), self.fill);
+ let shape = Geometry::Rect(frame.size).filled(self.fill);
Arc::make_mut(frame).prepend(Point::zero(), Element::Shape(shape));
}
Ok(frames)
@@ -374,7 +374,7 @@ impl Layout for StrokeNode {
) -> TypResult<Vec<Arc<Frame>>> {
let mut frames = self.child.layout(ctx, regions, styles)?;
for frame in &mut frames {
- let shape = Shape::stroked(Geometry::Rect(frame.size), self.stroke);
+ let shape = Geometry::Rect(frame.size).stroked(self.stroke);
Arc::make_mut(frame).prepend(Point::zero(), Element::Shape(shape));
}
Ok(frames)
diff --git a/src/eval/ops.rs b/src/eval/ops.rs
index 89802949..4796e042 100644
--- a/src/eval/ops.rs
+++ b/src/eval/ops.rs
@@ -1,6 +1,6 @@
use std::cmp::Ordering;
-use super::{Dynamic, RawAlign, StrExt, Value};
+use super::{Dynamic, RawAlign, RawStroke, Smart, StrExt, Value};
use crate::diag::StrResult;
use crate::geom::{Numeric, Spec, SpecAxis};
use Value::*;
@@ -90,25 +90,32 @@ pub fn add(lhs: Value, rhs: Value) -> StrResult<Value> {
(Array(a), Array(b)) => Array(a + b),
(Dict(a), Dict(b)) => Dict(a + b),
- (a, b) => {
- if let (Dyn(a), Dyn(b)) = (&a, &b) {
- // 1D alignments can be summed into 2D alignments.
- if let (Some(&a), Some(&b)) =
- (a.downcast::<RawAlign>(), b.downcast::<RawAlign>())
- {
- return if a.axis() != b.axis() {
- Ok(Dyn(Dynamic::new(match a.axis() {
- SpecAxis::Horizontal => Spec { x: a, y: b },
- SpecAxis::Vertical => Spec { x: b, y: a },
- })))
- } else {
- Err(format!("cannot add two {:?} alignments", a.axis()))
- };
+ (Color(color), Length(thickness)) | (Length(thickness), Color(color)) => {
+ Dyn(Dynamic::new(RawStroke {
+ paint: Smart::Custom(color.into()),
+ thickness: Smart::Custom(thickness),
+ }))
+ }
+
+ (Dyn(a), Dyn(b)) => {
+ // 1D alignments can be summed into 2D alignments.
+ if let (Some(&a), Some(&b)) =
+ (a.downcast::<RawAlign>(), b.downcast::<RawAlign>())
+ {
+ if a.axis() != b.axis() {
+ Dyn(Dynamic::new(match a.axis() {
+ SpecAxis::Horizontal => Spec { x: a, y: b },
+ SpecAxis::Vertical => Spec { x: b, y: a },
+ }))
+ } else {
+ return Err(format!("cannot add two {:?} alignments", a.axis()));
}
+ } else {
+ mismatch!("cannot add {} and {}", a, b);
}
-
- mismatch!("cannot add {} and {}", a, b);
}
+
+ (a, b) => mismatch!("cannot add {} and {}", a, b),
})
}
diff --git a/src/eval/raw.rs b/src/eval/raw.rs
index 622a0562..b0f46fc9 100644
--- a/src/eval/raw.rs
+++ b/src/eval/raw.rs
@@ -1,8 +1,11 @@
+use std::cmp::Ordering;
use std::fmt::{self, Debug, Formatter};
use std::ops::{Add, Div, Mul, Neg};
-use super::{Resolve, StyleChain};
-use crate::geom::{Align, Em, Length, Numeric, Relative, SpecAxis};
+use super::{Fold, Resolve, Smart, StyleChain, Value};
+use crate::geom::{
+ Align, Em, Get, Length, Numeric, Paint, Relative, Spec, SpecAxis, Stroke,
+};
use crate::library::text::{ParNode, TextNode};
/// The unresolved alignment representation.
@@ -49,6 +52,101 @@ impl Debug for RawAlign {
}
}
+dynamic! {
+ RawAlign: "alignment",
+}
+
+dynamic! {
+ Spec<RawAlign>: "2d alignment",
+}
+
+castable! {
+ Spec<Option<RawAlign>>,
+ Expected: "1d or 2d alignment",
+ @align: RawAlign => {
+ let mut aligns = Spec::default();
+ aligns.set(align.axis(), Some(*align));
+ aligns
+ },
+ @aligns: Spec<RawAlign> => aligns.map(Some),
+}
+
+/// The unresolved stroke representation.
+///
+/// In this representation, both fields are optional so that you can pass either
+/// just a paint (`red`), just a thickness (`0.1em`) or both (`2pt + red`) where
+/// this is expected.
+#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct RawStroke<T = RawLength> {
+ /// The stroke's paint.
+ pub paint: Smart<Paint>,
+ /// The stroke's thickness.
+ pub thickness: Smart<T>,
+}
+
+impl RawStroke<Length> {
+ /// Unpack the stroke, filling missing fields with `default`.
+ pub fn unwrap_or(self, default: Stroke) -> Stroke {
+ Stroke {
+ paint: self.paint.unwrap_or(default.paint),
+ thickness: self.thickness.unwrap_or(default.thickness),
+ }
+ }
+
+ /// Unpack the stroke, filling missing fields with the default values.
+ pub fn unwrap_or_default(self) -> Stroke {
+ self.unwrap_or(Stroke::default())
+ }
+}
+
+impl Resolve for RawStroke {
+ type Output = RawStroke<Length>;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ RawStroke {
+ paint: self.paint,
+ thickness: self.thickness.resolve(styles),
+ }
+ }
+}
+
+// This faciliates RawStroke => Stroke.
+impl Fold for RawStroke<Length> {
+ type Output = Self;
+
+ fn fold(self, outer: Self::Output) -> Self::Output {
+ Self {
+ paint: self.paint.or(outer.paint),
+ thickness: self.thickness.or(outer.thickness),
+ }
+ }
+}
+
+impl<T: Debug> Debug for RawStroke<T> {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ match (self.paint, &self.thickness) {
+ (Smart::Custom(paint), Smart::Custom(thickness)) => {
+ write!(f, "{thickness:?} + {paint:?}")
+ }
+ (Smart::Custom(paint), Smart::Auto) => paint.fmt(f),
+ (Smart::Auto, Smart::Custom(thickness)) => thickness.fmt(f),
+ (Smart::Auto, Smart::Auto) => f.pad("<stroke>"),
+ }
+ }
+}
+
+dynamic! {
+ RawStroke: "stroke",
+ Value::Length(thickness) => Self {
+ paint: Smart::Auto,
+ thickness: Smart::Custom(thickness),
+ },
+ Value::Color(color) => Self {
+ paint: Smart::Custom(color.into()),
+ thickness: Smart::Auto,
+ },
+}
+
/// The unresolved length representation.
///
/// Currently supports absolute and em units, but support could quite easily be
@@ -56,7 +154,7 @@ impl Debug for RawAlign {
/// 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)]
+#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
pub struct RawLength {
/// The absolute part.
pub length: Length,
@@ -101,6 +199,26 @@ impl Resolve for RawLength {
}
}
+impl Numeric for RawLength {
+ fn zero() -> Self {
+ Self::zero()
+ }
+
+ fn is_finite(self) -> bool {
+ self.length.is_finite() && self.em.is_finite()
+ }
+}
+
+impl PartialOrd for RawLength {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ if self.em.is_zero() && other.em.is_zero() {
+ self.length.partial_cmp(&other.length)
+ } else {
+ None
+ }
+ }
+}
+
impl From<Length> for RawLength {
fn from(length: Length) -> Self {
Self { length, em: Em::zero() }
@@ -119,16 +237,6 @@ impl From<Length> for Relative<RawLength> {
}
}
-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;
diff --git a/src/eval/styles.rs b/src/eval/styles.rs
index 575518f5..71293f40 100644
--- a/src/eval/styles.rs
+++ b/src/eval/styles.rs
@@ -287,15 +287,6 @@ pub trait Key<'a>: 'static {
) -> Self::Output;
}
-/// A property that is folded to determine its final value.
-pub trait Fold {
- /// The type of the folded output.
- type Output;
-
- /// Fold this inner value with an outer folded value.
- fn fold(self, outer: Self::Output) -> Self::Output;
-}
-
/// A property that is resolved with other properties from the style chain.
pub trait Resolve {
/// The type of the resolved output.
@@ -354,6 +345,39 @@ where
}
}
+/// A property that is folded to determine its final value.
+pub trait Fold {
+ /// The type of the folded output.
+ type Output;
+
+ /// Fold this inner value with an outer folded value.
+ fn fold(self, outer: Self::Output) -> Self::Output;
+}
+
+impl<T> Fold for Option<T>
+where
+ T: Fold,
+ T::Output: Default,
+{
+ type Output = Option<T::Output>;
+
+ fn fold(self, outer: Self::Output) -> Self::Output {
+ self.map(|inner| inner.fold(outer.unwrap_or_default()))
+ }
+}
+
+impl<T> Fold for Smart<T>
+where
+ T: Fold,
+ T::Output: Default,
+{
+ type Output = Smart<T::Output>;
+
+ fn fold(self, outer: Self::Output) -> Self::Output {
+ self.map(|inner| inner.fold(outer.unwrap_or_default()))
+ }
+}
+
/// A show rule recipe.
#[derive(Clone, PartialEq, Hash)]
struct Recipe {
@@ -472,7 +496,7 @@ impl<'a> StyleChain<'a> {
/// Get the output value of a style property.
///
/// Returns the property's default value if no map in the chain contains an
- /// entry for it. Also takes care of folding and resolving and returns
+ /// entry for it. Also takes care of resolving and folding and returns
/// references where applicable.
pub fn get<K: Key<'a>>(self, key: K) -> K::Output {
K::get(self, self.values(key))
diff --git a/src/eval/value.rs b/src/eval/value.rs
index 1851cf28..cc312c5a 100644
--- a/src/eval/value.rs
+++ b/src/eval/value.rs
@@ -2,11 +2,16 @@ use std::any::Any;
use std::cmp::Ordering;
use std::fmt::{self, Debug, Formatter};
use std::hash::{Hash, Hasher};
+use std::num::NonZeroUsize;
use std::sync::Arc;
-use super::{ops, Args, Array, Content, Context, Dict, Func, Layout, RawLength, StrExt};
+use super::{
+ ops, Args, Array, Content, Context, Dict, Func, Layout, LayoutNode, RawLength, StrExt,
+};
use crate::diag::{with_alternative, At, StrResult, TypResult};
-use crate::geom::{Angle, Color, Em, Fraction, Length, Ratio, Relative, RgbaColor};
+use crate::geom::{
+ Angle, Color, Dir, Em, Fraction, Length, Paint, Ratio, Relative, RgbaColor,
+};
use crate::library::text::RawNode;
use crate::syntax::{Span, Spanned};
use crate::util::EcoString;
@@ -526,7 +531,7 @@ macro_rules! castable {
$(@$dyn_in:ident: $dyn_type:ty => $dyn_out:expr,)*
) => {
impl $crate::eval::Cast<$crate::eval::Value> for $type {
- fn is(value: &Value) -> bool {
+ fn is(value: &$crate::eval::Value) -> bool {
#[allow(unused_variables)]
match value {
$($pattern => true,)*
@@ -637,6 +642,14 @@ impl<T> Smart<T> {
}
}
+ /// Keeps `self` if it contains a custom value, otherwise returns `other`.
+ pub fn or(self, other: Smart<T>) -> Self {
+ match self {
+ Self::Custom(x) => Self::Custom(x),
+ Self::Auto => other,
+ }
+ }
+
/// Returns the contained custom value or a provided default value.
pub fn unwrap_or(self, default: T) -> T {
match self {
@@ -655,6 +668,14 @@ impl<T> Smart<T> {
Self::Custom(x) => x,
}
}
+
+ /// Returns the contained custom value or the default value.
+ pub fn unwrap_or_default(self) -> T
+ where
+ T: Default,
+ {
+ self.unwrap_or_else(T::default)
+ }
}
impl<T> Default for Smart<T> {
@@ -678,6 +699,49 @@ impl<T: Cast> Cast for Smart<T> {
}
}
+dynamic! {
+ Dir: "direction",
+}
+
+castable! {
+ usize,
+ Expected: "non-negative integer",
+ Value::Int(int) => int.try_into().map_err(|_| {
+ if int < 0 {
+ "must be at least zero"
+ } else {
+ "number too large"
+ }
+ })?,
+}
+
+castable! {
+ NonZeroUsize,
+ Expected: "positive integer",
+ Value::Int(int) => Value::Int(int)
+ .cast::<usize>()?
+ .try_into()
+ .map_err(|_| "must be positive")?,
+}
+
+castable! {
+ Paint,
+ Expected: "color",
+ Value::Color(color) => Paint::Solid(color),
+}
+
+castable! {
+ String,
+ Expected: "string",
+ Value::Str(string) => string.into(),
+}
+
+castable! {
+ LayoutNode,
+ Expected: "content",
+ Value::Content(content) => content.pack(),
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/src/export/pdf.rs b/src/export/pdf.rs
index 95d20c51..067eb277 100644
--- a/src/export/pdf.rs
+++ b/src/export/pdf.rs
@@ -16,8 +16,10 @@ use ttf_parser::{name_id, GlyphId, Tag};
use super::subset::subset;
use crate::font::{find_name, FaceId, FontStore};
-use crate::frame::{Element, Frame, Geometry, Group, Shape, Stroke, Text};
-use crate::geom::{self, Color, Em, Length, Numeric, Paint, Point, Size, Transform};
+use crate::frame::{Element, Frame, Geometry, Group, Shape, Text};
+use crate::geom::{
+ self, Color, Em, Length, Numeric, Paint, Point, Size, Stroke, Transform,
+};
use crate::image::{Image, ImageId, ImageStore, RasterImage};
use crate::Context;
diff --git a/src/export/render.rs b/src/export/render.rs
index d6f82121..c3b92d31 100644
--- a/src/export/render.rs
+++ b/src/export/render.rs
@@ -7,8 +7,8 @@ use tiny_skia as sk;
use ttf_parser::{GlyphId, OutlineBuilder};
use usvg::FitTo;
-use crate::frame::{Element, Frame, Geometry, Group, Shape, Stroke, Text};
-use crate::geom::{self, Length, Paint, PathElement, Size, Transform};
+use crate::frame::{Element, Frame, Geometry, Group, Shape, Text};
+use crate::geom::{self, Length, Paint, PathElement, Size, Stroke, Transform};
use crate::image::{Image, RasterImage, Svg};
use crate::Context;
diff --git a/src/frame.rs b/src/frame.rs
index a104c069..9613e485 100644
--- a/src/frame.rs
+++ b/src/frame.rs
@@ -5,7 +5,7 @@ use std::sync::Arc;
use crate::font::FaceId;
use crate::geom::{
- Align, Em, Length, Numeric, Paint, Path, Point, Size, Spec, Transform,
+ Align, Em, Length, Numeric, Paint, Path, Point, Size, Spec, Stroke, Transform,
};
use crate::image::ImageId;
@@ -223,22 +223,6 @@ pub struct Shape {
pub stroke: Option<Stroke>,
}
-impl Shape {
- /// Create a filled shape without a stroke.
- pub fn filled(geometry: Geometry, fill: Paint) -> Self {
- Self { geometry, fill: Some(fill), stroke: None }
- }
-
- /// Create a stroked shape without a fill.
- pub fn stroked(geometry: Geometry, stroke: Stroke) -> Self {
- Self {
- geometry,
- fill: None,
- stroke: Some(stroke),
- }
- }
-}
-
/// A shape's geometry.
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Geometry {
@@ -252,11 +236,22 @@ pub enum Geometry {
Path(Path),
}
-/// A stroke of a geometric shape.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub struct Stroke {
- /// The stroke's paint.
- pub paint: Paint,
- /// The stroke's thickness.
- pub thickness: Length,
+impl Geometry {
+ /// Fill the geometry without a stroke.
+ pub fn filled(self, fill: Paint) -> Shape {
+ Shape {
+ geometry: self,
+ fill: Some(fill),
+ stroke: None,
+ }
+ }
+
+ /// Stroke the geometry without a fill.
+ pub fn stroked(self, stroke: Stroke) -> Shape {
+ Shape {
+ geometry: self,
+ fill: None,
+ stroke: Some(stroke),
+ }
+ }
}
diff --git a/src/geom/paint.rs b/src/geom/paint.rs
index 3660d528..351ef443 100644
--- a/src/geom/paint.rs
+++ b/src/geom/paint.rs
@@ -5,7 +5,7 @@ use syntect::highlighting::Color as SynColor;
use super::*;
/// How a fill or stroke should be painted.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub enum Paint {
/// A solid color.
Solid(Color),
@@ -20,6 +20,14 @@ where
}
}
+impl Debug for Paint {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ match self {
+ Self::Solid(color) => color.fmt(f),
+ }
+ }
+}
+
/// A color in a dynamic format.
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub enum Color {
@@ -234,6 +242,24 @@ impl From<CmykColor> for Color {
}
}
+/// A stroke of a geometric shape.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct Stroke {
+ /// The stroke's paint.
+ pub paint: Paint,
+ /// The stroke's thickness.
+ pub thickness: Length,
+}
+
+impl Default for Stroke {
+ fn default() -> Self {
+ Self {
+ paint: Paint::Solid(Color::BLACK.into()),
+ thickness: Length::pt(1.0),
+ }
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/src/library/graphics/line.rs b/src/library/graphics/line.rs
index 1dd138e6..de2e4aa1 100644
--- a/src/library/graphics/line.rs
+++ b/src/library/graphics/line.rs
@@ -12,10 +12,8 @@ pub struct LineNode {
#[node]
impl LineNode {
/// How to stroke the line.
- pub const STROKE: Paint = Color::BLACK.into();
- /// The line's thickness.
- #[property(resolve)]
- pub const THICKNESS: RawLength = Length::pt(1.0).into();
+ #[property(resolve, fold)]
+ pub const STROKE: RawStroke = RawStroke::default();
fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> {
let origin = args.named("origin")?.unwrap_or_default();
@@ -46,11 +44,7 @@ impl Layout for LineNode {
regions: &Regions,
styles: StyleChain,
) -> TypResult<Vec<Arc<Frame>>> {
- let thickness = styles.get(Self::THICKNESS);
- let stroke = Some(Stroke {
- paint: styles.get(Self::STROKE),
- thickness,
- });
+ let stroke = styles.get(Self::STROKE).unwrap_or_default();
let origin = self
.origin
@@ -64,11 +58,10 @@ impl Layout for LineNode {
.zip(regions.base)
.map(|(l, b)| l.relative_to(b));
- let geometry = Geometry::Line(delta.to_point());
- let shape = Shape { geometry, fill: None, stroke };
-
let target = regions.expand.select(regions.first, Size::zero());
let mut frame = Frame::new(target);
+
+ let shape = Geometry::Line(delta.to_point()).stroked(stroke);
frame.push(origin.to_point(), Element::Shape(shape));
Ok(vec![Arc::new(frame)])
diff --git a/src/library/graphics/shape.rs b/src/library/graphics/shape.rs
index ec6f735b..a159a3af 100644
--- a/src/library/graphics/shape.rs
+++ b/src/library/graphics/shape.rs
@@ -24,10 +24,8 @@ impl<const S: ShapeKind> ShapeNode<S> {
/// How to fill the shape.
pub const FILL: Option<Paint> = None;
/// How to stroke the shape.
- pub const STROKE: Smart<Option<Paint>> = Smart::Auto;
- /// The stroke's thickness.
- #[property(resolve)]
- pub const THICKNESS: RawLength = Length::pt(1.0).into();
+ #[property(resolve, fold)]
+ pub const STROKE: Smart<Option<RawStroke>> = Smart::Auto;
/// How much to pad the shape's content.
pub const PADDING: Relative<RawLength> = Relative::zero();
@@ -115,11 +113,10 @@ impl<const S: ShapeKind> Layout for ShapeNode<S> {
// Add fill and/or stroke.
let fill = styles.get(Self::FILL);
- let thickness = styles.get(Self::THICKNESS);
- let stroke = styles
- .get(Self::STROKE)
- .unwrap_or(fill.is_none().then(|| Color::BLACK.into()))
- .map(|paint| Stroke { paint, thickness });
+ let stroke = match styles.get(Self::STROKE) {
+ Smart::Auto => fill.is_none().then(Stroke::default),
+ Smart::Custom(stroke) => stroke.map(RawStroke::unwrap_or_default),
+ };
if fill.is_some() || stroke.is_some() {
let geometry = if is_round(S) {
diff --git a/src/library/mod.rs b/src/library/mod.rs
index a5f0b50c..0034b581 100644
--- a/src/library/mod.rs
+++ b/src/library/mod.rs
@@ -124,65 +124,3 @@ pub fn new() -> Scope {
std
}
-
-dynamic! {
- Dir: "direction",
-}
-
-dynamic! {
- RawAlign: "alignment",
-}
-
-dynamic! {
- Spec<RawAlign>: "2d alignment",
-}
-
-castable! {
- Spec<Option<RawAlign>>,
- Expected: "1d or 2d alignment",
- @align: RawAlign => {
- let mut aligns = Spec::default();
- aligns.set(align.axis(), Some(*align));
- aligns
- },
- @aligns: Spec<RawAlign> => aligns.map(Some),
-}
-
-castable! {
- usize,
- Expected: "non-negative integer",
- Value::Int(int) => int.try_into().map_err(|_| {
- if int < 0 {
- "must be at least zero"
- } else {
- "number too large"
- }
- })?,
-}
-
-castable! {
- NonZeroUsize,
- Expected: "positive integer",
- Value::Int(int) => Value::Int(int)
- .cast::<usize>()?
- .try_into()
- .map_err(|_| "must be positive")?,
-}
-
-castable! {
- Paint,
- Expected: "color",
- Value::Color(color) => Paint::Solid(color),
-}
-
-castable! {
- String,
- Expected: "string",
- Value::Str(string) => string.into(),
-}
-
-castable! {
- LayoutNode,
- Expected: "content",
- Value::Content(content) => content.pack(),
-}
diff --git a/src/library/prelude.rs b/src/library/prelude.rs
index d74a5d85..a1ebe6ef 100644
--- a/src/library/prelude.rs
+++ b/src/library/prelude.rs
@@ -10,7 +10,7 @@ pub use typst_macros::node;
pub use crate::diag::{with_alternative, At, Error, StrResult, TypError, TypResult};
pub use crate::eval::{
Arg, Args, Array, Cast, Content, Dict, Fold, Func, Key, Layout, LayoutNode, Merge,
- Node, RawAlign, RawLength, Regions, Resolve, Scope, Show, ShowNode, Smart,
+ Node, RawAlign, RawLength, RawStroke, Regions, Resolve, Scope, Show, ShowNode, Smart,
StyleChain, StyleMap, StyleVec, Value,
};
pub use crate::frame::*;
diff --git a/src/library/structure/table.rs b/src/library/structure/table.rs
index d0ab0716..40f25749 100644
--- a/src/library/structure/table.rs
+++ b/src/library/structure/table.rs
@@ -19,10 +19,8 @@ impl TableNode {
/// The secondary cell fill color.
pub const SECONDARY: Option<Paint> = None;
/// How to stroke the cells.
- pub const STROKE: Option<Paint> = Some(Color::BLACK.into());
- /// The stroke's thickness.
- #[property(resolve)]
- pub const THICKNESS: RawLength = Length::pt(1.0).into();
+ #[property(resolve, fold)]
+ pub const STROKE: Option<RawStroke> = Some(RawStroke::default());
/// How much to pad the cells's content.
pub const PADDING: Relative<RawLength> = Length::pt(5.0).into();
@@ -48,7 +46,6 @@ impl TableNode {
styles.set_opt(Self::PRIMARY, args.named("primary")?.or(fill));
styles.set_opt(Self::SECONDARY, args.named("secondary")?.or(fill));
styles.set_opt(Self::STROKE, args.named("stroke")?);
- styles.set_opt(Self::THICKNESS, args.named("thickness")?);
styles.set_opt(Self::PADDING, args.named("padding")?);
Ok(styles)
}
@@ -63,8 +60,7 @@ impl Show for TableNode {
let primary = styles.get(Self::PRIMARY);
let secondary = styles.get(Self::SECONDARY);
- let thickness = styles.get(Self::THICKNESS);
- let stroke = styles.get(Self::STROKE).map(|paint| Stroke { paint, thickness });
+ let stroke = styles.get(Self::STROKE).map(RawStroke::unwrap_or_default);
let padding = styles.get(Self::PADDING);
let cols = self.tracks.x.len().max(1);
diff --git a/src/library/text/deco.rs b/src/library/text/deco.rs
index f5ed4744..b8a0b3cb 100644
--- a/src/library/text/deco.rs
+++ b/src/library/text/deco.rs
@@ -20,12 +20,10 @@ pub type OverlineNode = DecoNode<OVERLINE>;
#[node(showable)]
impl<const L: DecoLine> DecoNode<L> {
- /// Stroke color of the line, defaults to the text color if `None`.
- #[property(shorthand)]
- pub const STROKE: Option<Paint> = None;
- /// Thickness of the line's strokes, read from the font tables if `auto`.
- #[property(shorthand, resolve)]
- pub const THICKNESS: Smart<RawLength> = Smart::Auto;
+ /// How to stroke the line. The text color and thickness read from the font
+ /// tables if `auto`.
+ #[property(shorthand, resolve, fold)]
+ pub const STROKE: Smart<RawStroke> = Smart::Auto;
/// Position of the line relative to the baseline, read from the font tables
/// if `auto`.
#[property(resolve)]
@@ -49,8 +47,7 @@ impl<const L: DecoLine> Show for DecoNode<L> {
.unwrap_or_else(|| {
self.0.clone().styled(TextNode::DECO, Decoration {
line: L,
- stroke: styles.get(Self::STROKE),
- thickness: styles.get(Self::THICKNESS),
+ stroke: styles.get(Self::STROKE).unwrap_or_default(),
offset: styles.get(Self::OFFSET),
extent: styles.get(Self::EXTENT),
evade: styles.get(Self::EVADE),
@@ -65,8 +62,7 @@ impl<const L: DecoLine> Show for DecoNode<L> {
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Decoration {
pub line: DecoLine,
- pub stroke: Option<Paint>,
- pub thickness: Smart<Length>,
+ pub stroke: RawStroke<Length>,
pub offset: Smart<Length>,
pub extent: Length,
pub evade: bool,
@@ -103,11 +99,10 @@ pub fn decorate(
let evade = deco.evade && deco.line != STRIKETHROUGH;
let offset = deco.offset.unwrap_or(-metrics.position.at(text.size));
-
- let stroke = Stroke {
- paint: deco.stroke.unwrap_or(text.fill),
- thickness: deco.thickness.unwrap_or(metrics.thickness.at(text.size)),
- };
+ let stroke = deco.stroke.unwrap_or(Stroke {
+ paint: text.fill,
+ thickness: metrics.thickness.at(text.size),
+ });
let gap_padding = 0.08 * text.size;
let min_width = 0.162 * text.size;
@@ -120,7 +115,7 @@ pub fn decorate(
let target = Point::new(to - from, Length::zero());
if target.x >= min_width || !evade {
- let shape = Shape::stroked(Geometry::Line(target), stroke);
+ let shape = Geometry::Line(target).stroked(stroke);
frame.push(origin, Element::Shape(shape));
}
};