summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2022-04-08 17:08:30 +0200
committerLaurenz <laurmaedje@gmail.com>2022-04-09 12:02:35 +0200
commit29eb13ca6214461a4b0deb63d589cd39ad6d41c2 (patch)
treec86797d440cfcc801c87a3c64f479e39f2c068b1
parent712c00ecb72b67da2c0788e5d3eb4dcc6366b2a7 (diff)
Sum color and length into stroke
-rw-r--r--macros/src/lib.rs26
-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
-rw-r--r--tests/ref/code/repr.pngbin28698 -> 32209 bytes
-rw-r--r--tests/ref/graphics/shape-fill-stroke.pngbin2893 -> 2997 bytes
-rw-r--r--tests/ref/structure/table.pngbin1566 -> 4295 bytes
-rw-r--r--tests/ref/text/deco.pngbin29421 -> 31937 bytes
-rw-r--r--tests/typ/code/repr.typ34
-rw-r--r--tests/typ/graphics/line.typ2
-rw-r--r--tests/typ/graphics/shape-circle.typ2
-rw-r--r--tests/typ/graphics/shape-ellipse.typ2
-rw-r--r--tests/typ/graphics/shape-fill-stroke.typ24
-rw-r--r--tests/typ/graphics/shape-rect.typ3
-rw-r--r--tests/typ/structure/table.typ9
-rw-r--r--tests/typ/text/deco.typ12
28 files changed, 397 insertions, 228 deletions
diff --git a/macros/src/lib.rs b/macros/src/lib.rs
index 429c5b09..dbea1f83 100644
--- a/macros/src/lib.rs
+++ b/macros/src/lib.rs
@@ -155,6 +155,8 @@ fn process_const(
let value_ty = &item.ty;
let output_ty = if property.referenced {
parse_quote!(&'a #value_ty)
+ } else if property.fold && property.resolve {
+ parse_quote!(<<#value_ty as eval::Resolve>::Output as eval::Fold>::Output)
} else if property.fold {
parse_quote!(<#value_ty as eval::Fold>::Output)
} else if property.resolve {
@@ -190,10 +192,13 @@ fn process_const(
&*LAZY
})
};
- } else if property.fold {
+ } else if property.resolve && property.fold {
get = quote! {
match values.next().cloned() {
- Some(inner) => eval::Fold::fold(inner, Self::get(chain, values)),
+ Some(value) => eval::Fold::fold(
+ eval::Resolve::resolve(value, chain),
+ Self::get(chain, values),
+ ),
None => #default,
}
};
@@ -202,6 +207,13 @@ fn process_const(
let value = values.next().cloned().unwrap_or(#default);
eval::Resolve::resolve(value, chain)
};
+ } else if property.fold {
+ get = quote! {
+ match values.next().cloned() {
+ Some(value) => eval::Fold::fold(value, Self::get(chain, values)),
+ None => #default,
+ }
+ };
} else {
get = quote! {
values.next().copied().unwrap_or(#default)
@@ -267,8 +279,8 @@ struct Property {
referenced: bool,
shorthand: bool,
variadic: bool,
- fold: bool,
resolve: bool,
+ fold: bool,
}
/// Parse a style property attribute.
@@ -279,8 +291,8 @@ fn parse_property(item: &mut syn::ImplItemConst) -> Result<Property> {
referenced: false,
shorthand: false,
variadic: false,
- fold: false,
resolve: false,
+ fold: false,
};
if let Some(idx) = item
@@ -296,8 +308,8 @@ fn parse_property(item: &mut syn::ImplItemConst) -> Result<Property> {
"shorthand" => property.shorthand = true,
"referenced" => property.referenced = true,
"variadic" => property.variadic = true,
- "fold" => property.fold = true,
"resolve" => property.resolve = true,
+ "fold" => property.fold = true,
_ => return Err(Error::new(ident.span(), "invalid attribute")),
},
TokenTree::Punct(_) => {}
@@ -314,10 +326,10 @@ fn parse_property(item: &mut syn::ImplItemConst) -> Result<Property> {
));
}
- if property.referenced as u8 + property.fold as u8 + property.resolve as u8 > 1 {
+ if property.referenced && (property.fold || property.resolve) {
return Err(Error::new(
span,
- "referenced, fold and resolve are mutually exclusive",
+ "referenced is mutually exclusive with fold and resolve",
));
}
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));
}
};
diff --git a/tests/ref/code/repr.png b/tests/ref/code/repr.png
index 822b096d..4474149a 100644
--- a/tests/ref/code/repr.png
+++ b/tests/ref/code/repr.png
Binary files differ
diff --git a/tests/ref/graphics/shape-fill-stroke.png b/tests/ref/graphics/shape-fill-stroke.png
index 12fcbd55..f2278c88 100644
--- a/tests/ref/graphics/shape-fill-stroke.png
+++ b/tests/ref/graphics/shape-fill-stroke.png
Binary files differ
diff --git a/tests/ref/structure/table.png b/tests/ref/structure/table.png
index bc70d548..f50f613b 100644
--- a/tests/ref/structure/table.png
+++ b/tests/ref/structure/table.png
Binary files differ
diff --git a/tests/ref/text/deco.png b/tests/ref/text/deco.png
index 684532a1..94bd8a38 100644
--- a/tests/ref/text/deco.png
+++ b/tests/ref/text/deco.png
Binary files differ
diff --git a/tests/typ/code/repr.typ b/tests/typ/code/repr.typ
index f766ee7e..8742f413 100644
--- a/tests/typ/code/repr.typ
+++ b/tests/typ/code/repr.typ
@@ -1,17 +1,8 @@
// Test representation of values in the document.
---
-// Variables.
-#let name = "Typst"
-#let ke-bab = "Kebab!"
-#let α = "Alpha"
-
-{name} \
-{ke-bab} \
-{α}
-
----
// Literal values.
+{auto} \
{none} (empty) \
{true} \
{false}
@@ -27,29 +18,30 @@
{4.5cm} \
{12e1pt} \
{2.5rad} \
-{45deg}
+{45deg} \
+{1.7em} \
+{1cm + 0em} \
+{2em + 10pt} \
+{2.3fr}
---
// Colors.
-#rgb("f7a20500")
+#rgb("f7a20500") \
+{2pt + rgb("f7a20500")}
---
// Strings and escaping.
-{"hi"} \
-{"a\n[]\"\u{1F680}string"}
+#repr("hi") \
+#repr("a\n[]\"\u{1F680}string")
---
// Content.
-{[*{"H" + "i"} there*]}
+#repr[*{"H" + "i"} there*]
---
// Functions
#let f(x) = x
-{rect} \
+{() => none} \
{f} \
-{() => none}
-
----
-// When using the `repr` function it's not in monospace.
-#repr(23deg)
+{rect}
diff --git a/tests/typ/graphics/line.typ b/tests/typ/graphics/line.typ
index 050ce05c..97dcb5cf 100644
--- a/tests/typ/graphics/line.typ
+++ b/tests/typ/graphics/line.typ
@@ -37,7 +37,7 @@
]
]
-#align(center, grid(columns: (1fr,) * 3, ..((star(20pt, thickness: .5pt),) * 9)))
+#align(center, grid(columns: (1fr,) * 3, ..((star(20pt, stroke: 0.5pt),) * 9)))
---
// Test errors.
diff --git a/tests/typ/graphics/shape-circle.typ b/tests/typ/graphics/shape-circle.typ
index 4b978e86..dc1e3f24 100644
--- a/tests/typ/graphics/shape-circle.typ
+++ b/tests/typ/graphics/shape-circle.typ
@@ -9,7 +9,7 @@
// Test auto sizing.
Auto-sized circle. \
-#circle(fill: rgb("eb5278"), stroke: black, thickness: 2pt,
+#circle(fill: rgb("eb5278"), stroke: 2pt + black,
align(center + horizon)[But, soft!]
)
diff --git a/tests/typ/graphics/shape-ellipse.typ b/tests/typ/graphics/shape-ellipse.typ
index 154144c4..995eabb9 100644
--- a/tests/typ/graphics/shape-ellipse.typ
+++ b/tests/typ/graphics/shape-ellipse.typ
@@ -17,7 +17,7 @@ Rect in ellipse in fixed rect. \
)
Auto-sized ellipse. \
-#ellipse(fill: conifer, stroke: forest, thickness: 3pt, padding: 3pt)[
+#ellipse(fill: conifer, stroke: 3pt + forest, padding: 3pt)[
#set text(8pt)
But, soft! what light through yonder window breaks?
]
diff --git a/tests/typ/graphics/shape-fill-stroke.typ b/tests/typ/graphics/shape-fill-stroke.typ
index dd5b9ee8..c09cb065 100644
--- a/tests/typ/graphics/shape-fill-stroke.typ
+++ b/tests/typ/graphics/shape-fill-stroke.typ
@@ -6,15 +6,15 @@
variant(stroke: none),
variant(),
variant(fill: none),
- variant(thickness: 2pt),
+ variant(stroke: 2pt),
variant(stroke: eastern),
- variant(stroke: eastern, thickness: 2pt),
+ variant(stroke: eastern + 2pt),
variant(fill: eastern),
variant(fill: eastern, stroke: none),
- variant(fill: forest, stroke: none, thickness: 2pt),
+ variant(fill: forest, stroke: none),
variant(fill: forest, stroke: conifer),
- variant(fill: forest, stroke: black, thickness: 2pt),
- variant(fill: forest, stroke: conifer, thickness: 2pt),
+ variant(fill: forest, stroke: black + 2pt),
+ variant(fill: forest, stroke: conifer + 2pt),
) {
(align(horizon)[{i + 1}.], item, [])
}
@@ -24,3 +24,17 @@
gutter: 5pt,
..items,
)
+
+---
+// Test stroke folding.
+#let sq = square.with(size: 10pt)
+
+#set square(stroke: none)
+#sq()
+#set square(stroke: auto)
+#sq()
+#sq(fill: teal)
+#sq(stroke: 2pt)
+#sq(stroke: blue)
+#sq(fill: teal, stroke: blue)
+#sq(fill: teal, stroke: 2pt + blue)
diff --git a/tests/typ/graphics/shape-rect.typ b/tests/typ/graphics/shape-rect.typ
index add39b80..e035fc91 100644
--- a/tests/typ/graphics/shape-rect.typ
+++ b/tests/typ/graphics/shape-rect.typ
@@ -14,8 +14,7 @@
#block(rect(
height: 15pt,
fill: rgb("46b3c2"),
- stroke: rgb("234994"),
- thickness: 2pt,
+ stroke: 2pt + rgb("234994"),
))
// Fixed width, text height.
diff --git a/tests/typ/structure/table.typ b/tests/typ/structure/table.typ
index 0372951c..57b71ede 100644
--- a/tests/typ/structure/table.typ
+++ b/tests/typ/structure/table.typ
@@ -1,13 +1,18 @@
+// Test tables.
+
+---
#set page(height: 70pt)
#set table(primary: rgb("aaa"), secondary: none)
#table(
columns: (1fr,) * 3,
- stroke: rgb("333"),
- thickness: 2pt,
+ stroke: 2pt + rgb("333"),
[A], [B], [C], [], [], [D \ E \ F \ \ \ G], [H],
)
---
+#table(columns: 3, stroke: none, fill: green, [A], [B], [C])
+
+---
// Ref: false
#table()
diff --git a/tests/typ/text/deco.typ b/tests/typ/text/deco.typ
index 071208ac..5e9de5b3 100644
--- a/tests/typ/text/deco.typ
+++ b/tests/typ/text/deco.typ
@@ -20,12 +20,14 @@
---
#let redact = strike.with(10pt, extent: 0.05em)
-#let highlight = strike.with(
- stroke: rgb("abcdef88"),
- thickness: 10pt,
- extent: 0.05em,
-)
+#let highlight = strike.with(stroke: 10pt + rgb("abcdef88"), extent: 0.05em)
// Abuse thickness and transparency for redacting and highlighting stuff.
Sometimes, we work #redact[in secret].
There might be #highlight[redacted] things.
+ underline()
+
+---
+// Test stroke folding.
+#set underline(stroke: 2pt, offset: 2pt)
+#underline(text(red, [DANGER!]))