diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 429c5b09b..dbea1f831 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 { 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 { "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 { )); } - 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 f92a31f5f..117c269a7 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>> { 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>> { 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 898029495..4796e042e 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 { (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::(), b.downcast::()) - { - 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())) - }; - } - } - - mismatch!("cannot add {} and {}", a, b); + (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::(), b.downcast::()) + { + 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); + } + } + + (a, b) => mismatch!("cannot add {} and {}", a, b), }) } diff --git a/src/eval/raw.rs b/src/eval/raw.rs index 622a0562c..b0f46fc94 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: "2d alignment", +} + +castable! { + Spec>, + Expected: "1d or 2d alignment", + @align: RawAlign => { + let mut aligns = Spec::default(); + aligns.set(align.axis(), Some(*align)); + aligns + }, + @aligns: Spec => 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 { + /// The stroke's paint. + pub paint: Smart, + /// The stroke's thickness. + pub thickness: Smart, +} + +impl RawStroke { + /// 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; + + fn resolve(self, styles: StyleChain) -> Self::Output { + RawStroke { + paint: self.paint, + thickness: self.thickness.resolve(styles), + } + } +} + +// This faciliates RawStroke => Stroke. +impl Fold for RawStroke { + 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 Debug for RawStroke { + 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(""), + } + } +} + +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 { + if self.em.is_zero() && other.em.is_zero() { + self.length.partial_cmp(&other.length) + } else { + None + } + } +} + impl From for RawLength { fn from(length: Length) -> Self { Self { length, em: Em::zero() } @@ -119,16 +237,6 @@ impl From for Relative { } } -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 575518f59..71293f40d 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 Fold for Option +where + T: Fold, + T::Output: Default, +{ + type Output = Option; + + fn fold(self, outer: Self::Output) -> Self::Output { + self.map(|inner| inner.fold(outer.unwrap_or_default())) + } +} + +impl Fold for Smart +where + T: Fold, + T::Output: Default, +{ + type Output = Smart; + + 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>(self, key: K) -> K::Output { K::get(self, self.values(key)) diff --git a/src/eval/value.rs b/src/eval/value.rs index 1851cf281..cc312c5a6 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 Smart { } } + /// Keeps `self` if it contains a custom value, otherwise returns `other`. + pub fn or(self, other: Smart) -> 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 Smart { 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 Default for Smart { @@ -678,6 +699,49 @@ impl Cast for Smart { } } +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::()? + .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 95d20c518..067eb2775 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 d6f82121d..c3b92d315 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 a104c0695..9613e485d 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, } -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 3660d5282..351ef443d 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 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 1dd138e68..de2e4aa1d 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 { let origin = args.named("origin")?.unwrap_or_default(); @@ -46,11 +44,7 @@ impl Layout for LineNode { regions: &Regions, styles: StyleChain, ) -> TypResult>> { - 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 ec6f735bf..a159a3af3 100644 --- a/src/library/graphics/shape.rs +++ b/src/library/graphics/shape.rs @@ -24,10 +24,8 @@ impl ShapeNode { /// How to fill the shape. pub const FILL: Option = None; /// How to stroke the shape. - pub const STROKE: Smart> = Smart::Auto; - /// The stroke's thickness. - #[property(resolve)] - pub const THICKNESS: RawLength = Length::pt(1.0).into(); + #[property(resolve, fold)] + pub const STROKE: Smart> = Smart::Auto; /// How much to pad the shape's content. pub const PADDING: Relative = Relative::zero(); @@ -115,11 +113,10 @@ impl Layout for ShapeNode { // 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 a5f0b50c4..0034b5815 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: "2d alignment", -} - -castable! { - Spec>, - Expected: "1d or 2d alignment", - @align: RawAlign => { - let mut aligns = Spec::default(); - aligns.set(align.axis(), Some(*align)); - aligns - }, - @aligns: Spec => 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::()? - .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 d74a5d852..a1ebe6eff 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 d0ab0716e..40f25749b 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 = None; /// How to stroke the cells. - pub const STROKE: Option = 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 = Some(RawStroke::default()); /// How much to pad the cells's content. pub const PADDING: Relative = 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 f5ed47445..b8a0b3cbf 100644 --- a/src/library/text/deco.rs +++ b/src/library/text/deco.rs @@ -20,12 +20,10 @@ pub type OverlineNode = DecoNode; #[node(showable)] impl DecoNode { - /// Stroke color of the line, defaults to the text color if `None`. - #[property(shorthand)] - pub const STROKE: Option = None; - /// Thickness of the line's strokes, read from the font tables if `auto`. - #[property(shorthand, resolve)] - pub const THICKNESS: Smart = 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 = Smart::Auto; /// Position of the line relative to the baseline, read from the font tables /// if `auto`. #[property(resolve)] @@ -49,8 +47,7 @@ impl Show for DecoNode { .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 Show for DecoNode { #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct Decoration { pub line: DecoLine, - pub stroke: Option, - pub thickness: Smart, + pub stroke: RawStroke, pub offset: Smart, 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 822b096d3..4474149a6 100644 Binary files a/tests/ref/code/repr.png and b/tests/ref/code/repr.png differ diff --git a/tests/ref/graphics/shape-fill-stroke.png b/tests/ref/graphics/shape-fill-stroke.png index 12fcbd55b..f2278c887 100644 Binary files a/tests/ref/graphics/shape-fill-stroke.png and b/tests/ref/graphics/shape-fill-stroke.png differ diff --git a/tests/ref/structure/table.png b/tests/ref/structure/table.png index bc70d5485..f50f613b8 100644 Binary files a/tests/ref/structure/table.png and b/tests/ref/structure/table.png differ diff --git a/tests/ref/text/deco.png b/tests/ref/text/deco.png index 684532a1b..94bd8a388 100644 Binary files a/tests/ref/text/deco.png and b/tests/ref/text/deco.png differ diff --git a/tests/typ/code/repr.typ b/tests/typ/code/repr.typ index f766ee7ea..8742f4139 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 050ce05c9..97dcb5cf7 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 4b978e866..dc1e3f242 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 154144c4a..995eabb9b 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 dd5b9ee8b..c09cb065c 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 add39b808..e035fc91d 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 0372951cb..57b71ede7 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 071208ac6..5e9de5b33 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!]))