Sum color and length into stroke

This commit is contained in:
Laurenz 2022-04-08 17:08:30 +02:00
parent 712c00ecb7
commit 29eb13ca62
28 changed files with 399 additions and 230 deletions

View File

@ -155,6 +155,8 @@ fn process_const(
let value_ty = &item.ty; let value_ty = &item.ty;
let output_ty = if property.referenced { let output_ty = if property.referenced {
parse_quote!(&'a #value_ty) 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 { } else if property.fold {
parse_quote!(<#value_ty as eval::Fold>::Output) parse_quote!(<#value_ty as eval::Fold>::Output)
} else if property.resolve { } else if property.resolve {
@ -190,10 +192,13 @@ fn process_const(
&*LAZY &*LAZY
}) })
}; };
} else if property.fold { } else if property.resolve && property.fold {
get = quote! { get = quote! {
match values.next().cloned() { 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, None => #default,
} }
}; };
@ -202,6 +207,13 @@ fn process_const(
let value = values.next().cloned().unwrap_or(#default); let value = values.next().cloned().unwrap_or(#default);
eval::Resolve::resolve(value, chain) 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 { } else {
get = quote! { get = quote! {
values.next().copied().unwrap_or(#default) values.next().copied().unwrap_or(#default)
@ -267,8 +279,8 @@ struct Property {
referenced: bool, referenced: bool,
shorthand: bool, shorthand: bool,
variadic: bool, variadic: bool,
fold: bool,
resolve: bool, resolve: bool,
fold: bool,
} }
/// Parse a style property attribute. /// Parse a style property attribute.
@ -279,8 +291,8 @@ fn parse_property(item: &mut syn::ImplItemConst) -> Result<Property> {
referenced: false, referenced: false,
shorthand: false, shorthand: false,
variadic: false, variadic: false,
fold: false,
resolve: false, resolve: false,
fold: false,
}; };
if let Some(idx) = item if let Some(idx) = item
@ -296,8 +308,8 @@ fn parse_property(item: &mut syn::ImplItemConst) -> Result<Property> {
"shorthand" => property.shorthand = true, "shorthand" => property.shorthand = true,
"referenced" => property.referenced = true, "referenced" => property.referenced = true,
"variadic" => property.variadic = true, "variadic" => property.variadic = true,
"fold" => property.fold = true,
"resolve" => property.resolve = true, "resolve" => property.resolve = true,
"fold" => property.fold = true,
_ => return Err(Error::new(ident.span(), "invalid attribute")), _ => return Err(Error::new(ident.span(), "invalid attribute")),
}, },
TokenTree::Punct(_) => {} 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( return Err(Error::new(
span, span,
"referenced, fold and resolve are mutually exclusive", "referenced is mutually exclusive with fold and resolve",
)); ));
} }

View File

@ -7,8 +7,8 @@ use std::sync::Arc;
use super::{Barrier, RawAlign, RawLength, Resolve, 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};
use crate::geom::{Align, Length, Paint, Point, Relative, Sides, Size, Spec}; use crate::geom::{Align, Length, Paint, Point, Relative, Sides, Size, Spec, Stroke};
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;
@ -349,7 +349,7 @@ impl Layout for FillNode {
) -> TypResult<Vec<Arc<Frame>>> { ) -> TypResult<Vec<Arc<Frame>>> {
let mut frames = self.child.layout(ctx, regions, styles)?; let mut frames = self.child.layout(ctx, regions, styles)?;
for frame in &mut frames { 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)); Arc::make_mut(frame).prepend(Point::zero(), Element::Shape(shape));
} }
Ok(frames) Ok(frames)
@ -374,7 +374,7 @@ impl Layout for StrokeNode {
) -> TypResult<Vec<Arc<Frame>>> { ) -> TypResult<Vec<Arc<Frame>>> {
let mut frames = self.child.layout(ctx, regions, styles)?; let mut frames = self.child.layout(ctx, regions, styles)?;
for frame in &mut frames { 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)); Arc::make_mut(frame).prepend(Point::zero(), Element::Shape(shape));
} }
Ok(frames) Ok(frames)

View File

@ -1,6 +1,6 @@
use std::cmp::Ordering; use std::cmp::Ordering;
use super::{Dynamic, RawAlign, StrExt, Value}; use super::{Dynamic, RawAlign, RawStroke, Smart, StrExt, Value};
use crate::diag::StrResult; use crate::diag::StrResult;
use crate::geom::{Numeric, Spec, SpecAxis}; use crate::geom::{Numeric, Spec, SpecAxis};
use Value::*; use Value::*;
@ -90,25 +90,32 @@ pub fn add(lhs: Value, rhs: Value) -> StrResult<Value> {
(Array(a), Array(b)) => Array(a + b), (Array(a), Array(b)) => Array(a + b),
(Dict(a), Dict(b)) => Dict(a + b), (Dict(a), Dict(b)) => Dict(a + b),
(a, b) => { (Color(color), Length(thickness)) | (Length(thickness), Color(color)) => {
if let (Dyn(a), Dyn(b)) = (&a, &b) { Dyn(Dynamic::new(RawStroke {
// 1D alignments can be summed into 2D alignments. paint: Smart::Custom(color.into()),
if let (Some(&a), Some(&b)) = thickness: Smart::Custom(thickness),
(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()))
};
}
}
mismatch!("cannot add {} and {}", a, b);
} }
(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);
}
}
(a, b) => mismatch!("cannot add {} and {}", a, b),
}) })
} }

View File

@ -1,8 +1,11 @@
use std::cmp::Ordering;
use std::fmt::{self, Debug, Formatter}; use std::fmt::{self, Debug, Formatter};
use std::ops::{Add, Div, Mul, Neg}; use std::ops::{Add, Div, Mul, Neg};
use super::{Resolve, StyleChain}; use super::{Fold, Resolve, Smart, StyleChain, Value};
use crate::geom::{Align, Em, Length, Numeric, Relative, SpecAxis}; use crate::geom::{
Align, Em, Get, Length, Numeric, Paint, Relative, Spec, SpecAxis, Stroke,
};
use crate::library::text::{ParNode, TextNode}; use crate::library::text::{ParNode, TextNode};
/// The unresolved alignment representation. /// 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. /// The unresolved length representation.
/// ///
/// Currently supports absolute and em units, but support could quite easily be /// 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 /// 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 /// that has a small footprint and allocates for the rare case that units are
/// mixed. /// mixed.
#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] #[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
pub struct RawLength { pub struct RawLength {
/// The absolute part. /// The absolute part.
pub length: Length, 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 { impl From<Length> for RawLength {
fn from(length: Length) -> Self { fn from(length: Length) -> Self {
Self { length, em: Em::zero() } 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 { impl Neg for RawLength {
type Output = Self; type Output = Self;

View File

@ -287,15 +287,6 @@ pub trait Key<'a>: 'static {
) -> Self::Output; ) -> 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. /// A property that is resolved with other properties from the style chain.
pub trait Resolve { pub trait Resolve {
/// The type of the resolved output. /// 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. /// A show rule recipe.
#[derive(Clone, PartialEq, Hash)] #[derive(Clone, PartialEq, Hash)]
struct Recipe { struct Recipe {
@ -472,7 +496,7 @@ impl<'a> StyleChain<'a> {
/// Get the output value of a style property. /// Get the output value of a style property.
/// ///
/// Returns the property's default value if no map in the chain contains an /// 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. /// references where applicable.
pub fn get<K: Key<'a>>(self, key: K) -> K::Output { pub fn get<K: Key<'a>>(self, key: K) -> K::Output {
K::get(self, self.values(key)) K::get(self, self.values(key))

View File

@ -2,11 +2,16 @@ use std::any::Any;
use std::cmp::Ordering; use std::cmp::Ordering;
use std::fmt::{self, Debug, Formatter}; use std::fmt::{self, Debug, Formatter};
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use std::num::NonZeroUsize;
use std::sync::Arc; 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::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::library::text::RawNode;
use crate::syntax::{Span, Spanned}; use crate::syntax::{Span, Spanned};
use crate::util::EcoString; use crate::util::EcoString;
@ -526,7 +531,7 @@ macro_rules! castable {
$(@$dyn_in:ident: $dyn_type:ty => $dyn_out:expr,)* $(@$dyn_in:ident: $dyn_type:ty => $dyn_out:expr,)*
) => { ) => {
impl $crate::eval::Cast<$crate::eval::Value> for $type { impl $crate::eval::Cast<$crate::eval::Value> for $type {
fn is(value: &Value) -> bool { fn is(value: &$crate::eval::Value) -> bool {
#[allow(unused_variables)] #[allow(unused_variables)]
match value { match value {
$($pattern => true,)* $($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. /// Returns the contained custom value or a provided default value.
pub fn unwrap_or(self, default: T) -> T { pub fn unwrap_or(self, default: T) -> T {
match self { match self {
@ -655,6 +668,14 @@ impl<T> Smart<T> {
Self::Custom(x) => x, 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> { 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -16,8 +16,10 @@ use ttf_parser::{name_id, GlyphId, Tag};
use super::subset::subset; use super::subset::subset;
use crate::font::{find_name, FaceId, FontStore}; use crate::font::{find_name, FaceId, FontStore};
use crate::frame::{Element, Frame, Geometry, Group, Shape, Stroke, Text}; use crate::frame::{Element, Frame, Geometry, Group, Shape, Text};
use crate::geom::{self, Color, Em, Length, Numeric, Paint, Point, Size, Transform}; use crate::geom::{
self, Color, Em, Length, Numeric, Paint, Point, Size, Stroke, Transform,
};
use crate::image::{Image, ImageId, ImageStore, RasterImage}; use crate::image::{Image, ImageId, ImageStore, RasterImage};
use crate::Context; use crate::Context;

View File

@ -7,8 +7,8 @@ use tiny_skia as sk;
use ttf_parser::{GlyphId, OutlineBuilder}; use ttf_parser::{GlyphId, OutlineBuilder};
use usvg::FitTo; use usvg::FitTo;
use crate::frame::{Element, Frame, Geometry, Group, Shape, Stroke, Text}; use crate::frame::{Element, Frame, Geometry, Group, Shape, Text};
use crate::geom::{self, Length, Paint, PathElement, Size, Transform}; use crate::geom::{self, Length, Paint, PathElement, Size, Stroke, Transform};
use crate::image::{Image, RasterImage, Svg}; use crate::image::{Image, RasterImage, Svg};
use crate::Context; use crate::Context;

View File

@ -5,7 +5,7 @@ use std::sync::Arc;
use crate::font::FaceId; use crate::font::FaceId;
use crate::geom::{ 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; use crate::image::ImageId;
@ -223,22 +223,6 @@ pub struct Shape {
pub stroke: Option<Stroke>, 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. /// A shape's geometry.
#[derive(Debug, Clone, Eq, PartialEq)] #[derive(Debug, Clone, Eq, PartialEq)]
pub enum Geometry { pub enum Geometry {
@ -252,11 +236,22 @@ pub enum Geometry {
Path(Path), Path(Path),
} }
/// A stroke of a geometric shape. impl Geometry {
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] /// Fill the geometry without a stroke.
pub struct Stroke { pub fn filled(self, fill: Paint) -> Shape {
/// The stroke's paint. Shape {
pub paint: Paint, geometry: self,
/// The stroke's thickness. fill: Some(fill),
pub thickness: Length, stroke: None,
}
}
/// Stroke the geometry without a fill.
pub fn stroked(self, stroke: Stroke) -> Shape {
Shape {
geometry: self,
fill: None,
stroke: Some(stroke),
}
}
} }

View File

@ -5,7 +5,7 @@ use syntect::highlighting::Color as SynColor;
use super::*; use super::*;
/// How a fill or stroke should be painted. /// How a fill or stroke should be painted.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub enum Paint { pub enum Paint {
/// A solid color. /// A solid color.
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. /// A color in a dynamic format.
#[derive(Copy, Clone, Eq, PartialEq, Hash)] #[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub enum Color { 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -12,10 +12,8 @@ pub struct LineNode {
#[node] #[node]
impl LineNode { impl LineNode {
/// How to stroke the line. /// How to stroke the line.
pub const STROKE: Paint = Color::BLACK.into(); #[property(resolve, fold)]
/// The line's thickness. pub const STROKE: RawStroke = RawStroke::default();
#[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();
@ -46,11 +44,7 @@ impl Layout for LineNode {
regions: &Regions, regions: &Regions,
styles: StyleChain, styles: StyleChain,
) -> TypResult<Vec<Arc<Frame>>> { ) -> TypResult<Vec<Arc<Frame>>> {
let thickness = styles.get(Self::THICKNESS); let stroke = styles.get(Self::STROKE).unwrap_or_default();
let stroke = Some(Stroke {
paint: styles.get(Self::STROKE),
thickness,
});
let origin = self let origin = self
.origin .origin
@ -64,11 +58,10 @@ impl Layout for LineNode {
.zip(regions.base) .zip(regions.base)
.map(|(l, b)| l.relative_to(b)); .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 target = regions.expand.select(regions.first, Size::zero());
let mut frame = Frame::new(target); let mut frame = Frame::new(target);
let shape = Geometry::Line(delta.to_point()).stroked(stroke);
frame.push(origin.to_point(), Element::Shape(shape)); frame.push(origin.to_point(), Element::Shape(shape));
Ok(vec![Arc::new(frame)]) Ok(vec![Arc::new(frame)])

View File

@ -24,10 +24,8 @@ impl<const S: ShapeKind> ShapeNode<S> {
/// How to fill the shape. /// How to fill the shape.
pub const FILL: Option<Paint> = None; pub const FILL: Option<Paint> = None;
/// How to stroke the shape. /// How to stroke the shape.
pub const STROKE: Smart<Option<Paint>> = Smart::Auto; #[property(resolve, fold)]
/// The stroke's thickness. pub const STROKE: Smart<Option<RawStroke>> = Smart::Auto;
#[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<RawLength> = Relative::zero(); pub const PADDING: Relative<RawLength> = Relative::zero();
@ -115,11 +113,10 @@ impl<const S: ShapeKind> Layout for ShapeNode<S> {
// Add fill and/or stroke. // Add fill and/or stroke.
let fill = styles.get(Self::FILL); let fill = styles.get(Self::FILL);
let thickness = styles.get(Self::THICKNESS); let stroke = match styles.get(Self::STROKE) {
let stroke = styles Smart::Auto => fill.is_none().then(Stroke::default),
.get(Self::STROKE) Smart::Custom(stroke) => stroke.map(RawStroke::unwrap_or_default),
.unwrap_or(fill.is_none().then(|| Color::BLACK.into())) };
.map(|paint| Stroke { paint, thickness });
if fill.is_some() || stroke.is_some() { if fill.is_some() || stroke.is_some() {
let geometry = if is_round(S) { let geometry = if is_round(S) {

View File

@ -124,65 +124,3 @@ pub fn new() -> Scope {
std 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(),
}

View File

@ -10,7 +10,7 @@ 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, RawLength, Regions, Resolve, Scope, Show, ShowNode, Smart, Node, RawAlign, RawLength, RawStroke, Regions, Resolve, Scope, Show, ShowNode, Smart,
StyleChain, StyleMap, StyleVec, Value, StyleChain, StyleMap, StyleVec, Value,
}; };
pub use crate::frame::*; pub use crate::frame::*;

View File

@ -19,10 +19,8 @@ impl TableNode {
/// The secondary cell fill color. /// The secondary cell fill color.
pub const SECONDARY: Option<Paint> = None; pub const SECONDARY: Option<Paint> = None;
/// How to stroke the cells. /// How to stroke the cells.
pub const STROKE: Option<Paint> = Some(Color::BLACK.into()); #[property(resolve, fold)]
/// The stroke's thickness. pub const STROKE: Option<RawStroke> = Some(RawStroke::default());
#[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<RawLength> = Length::pt(5.0).into(); 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::PRIMARY, args.named("primary")?.or(fill));
styles.set_opt(Self::SECONDARY, args.named("secondary")?.or(fill)); styles.set_opt(Self::SECONDARY, args.named("secondary")?.or(fill));
styles.set_opt(Self::STROKE, args.named("stroke")?); styles.set_opt(Self::STROKE, args.named("stroke")?);
styles.set_opt(Self::THICKNESS, args.named("thickness")?);
styles.set_opt(Self::PADDING, args.named("padding")?); styles.set_opt(Self::PADDING, args.named("padding")?);
Ok(styles) Ok(styles)
} }
@ -63,8 +60,7 @@ impl Show for TableNode {
let primary = styles.get(Self::PRIMARY); let primary = styles.get(Self::PRIMARY);
let secondary = styles.get(Self::SECONDARY); let secondary = styles.get(Self::SECONDARY);
let thickness = styles.get(Self::THICKNESS); let stroke = styles.get(Self::STROKE).map(RawStroke::unwrap_or_default);
let stroke = styles.get(Self::STROKE).map(|paint| Stroke { paint, thickness });
let padding = styles.get(Self::PADDING); let padding = styles.get(Self::PADDING);
let cols = self.tracks.x.len().max(1); let cols = self.tracks.x.len().max(1);

View File

@ -20,12 +20,10 @@ pub type OverlineNode = DecoNode<OVERLINE>;
#[node(showable)] #[node(showable)]
impl<const L: DecoLine> DecoNode<L> { impl<const L: DecoLine> DecoNode<L> {
/// Stroke color of the line, defaults to the text color if `None`. /// How to stroke the line. The text color and thickness read from the font
#[property(shorthand)] /// tables if `auto`.
pub const STROKE: Option<Paint> = None; #[property(shorthand, resolve, fold)]
/// Thickness of the line's strokes, read from the font tables if `auto`. pub const STROKE: Smart<RawStroke> = Smart::Auto;
#[property(shorthand, resolve)]
pub const THICKNESS: Smart<RawLength> = Smart::Auto;
/// Position of the line relative to the baseline, read from the font tables /// Position of the line relative to the baseline, read from the font tables
/// if `auto`. /// if `auto`.
#[property(resolve)] #[property(resolve)]
@ -49,8 +47,7 @@ impl<const L: DecoLine> Show for DecoNode<L> {
.unwrap_or_else(|| { .unwrap_or_else(|| {
self.0.clone().styled(TextNode::DECO, Decoration { self.0.clone().styled(TextNode::DECO, Decoration {
line: L, line: L,
stroke: styles.get(Self::STROKE), stroke: styles.get(Self::STROKE).unwrap_or_default(),
thickness: styles.get(Self::THICKNESS),
offset: styles.get(Self::OFFSET), offset: styles.get(Self::OFFSET),
extent: styles.get(Self::EXTENT), extent: styles.get(Self::EXTENT),
evade: styles.get(Self::EVADE), evade: styles.get(Self::EVADE),
@ -65,8 +62,7 @@ impl<const L: DecoLine> Show for DecoNode<L> {
#[derive(Debug, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Decoration { pub struct Decoration {
pub line: DecoLine, pub line: DecoLine,
pub stroke: Option<Paint>, pub stroke: RawStroke<Length>,
pub thickness: Smart<Length>,
pub offset: Smart<Length>, pub offset: Smart<Length>,
pub extent: Length, pub extent: Length,
pub evade: bool, pub evade: bool,
@ -103,11 +99,10 @@ pub fn decorate(
let evade = deco.evade && deco.line != STRIKETHROUGH; let evade = deco.evade && deco.line != STRIKETHROUGH;
let offset = deco.offset.unwrap_or(-metrics.position.at(text.size)); let offset = deco.offset.unwrap_or(-metrics.position.at(text.size));
let stroke = deco.stroke.unwrap_or(Stroke {
let stroke = Stroke { paint: text.fill,
paint: deco.stroke.unwrap_or(text.fill), thickness: metrics.thickness.at(text.size),
thickness: deco.thickness.unwrap_or(metrics.thickness.at(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;
@ -120,7 +115,7 @@ pub fn decorate(
let target = Point::new(to - from, Length::zero()); let target = Point::new(to - from, Length::zero());
if target.x >= min_width || !evade { 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)); frame.push(origin, Element::Shape(shape));
} }
}; };

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -1,17 +1,8 @@
// Test representation of values in the document. // Test representation of values in the document.
---
// Variables.
#let name = "Typst"
#let ke-bab = "Kebab!"
#let α = "Alpha"
{name} \
{ke-bab} \
{α}
--- ---
// Literal values. // Literal values.
{auto} \
{none} (empty) \ {none} (empty) \
{true} \ {true} \
{false} {false}
@ -27,29 +18,30 @@
{4.5cm} \ {4.5cm} \
{12e1pt} \ {12e1pt} \
{2.5rad} \ {2.5rad} \
{45deg} {45deg} \
{1.7em} \
{1cm + 0em} \
{2em + 10pt} \
{2.3fr}
--- ---
// Colors. // Colors.
#rgb("f7a20500") #rgb("f7a20500") \
{2pt + rgb("f7a20500")}
--- ---
// Strings and escaping. // Strings and escaping.
{"hi"} \ #repr("hi") \
{"a\n[]\"\u{1F680}string"} #repr("a\n[]\"\u{1F680}string")
--- ---
// Content. // Content.
{[*{"H" + "i"} there*]} #repr[*{"H" + "i"} there*]
--- ---
// Functions // Functions
#let f(x) = x #let f(x) = x
{rect} \ {() => none} \
{f} \ {f} \
{() => none} {rect}
---
// When using the `repr` function it's not in monospace.
#repr(23deg)

View File

@ -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. // Test errors.

View File

@ -9,7 +9,7 @@
// Test auto sizing. // Test auto sizing.
Auto-sized circle. \ Auto-sized circle. \
#circle(fill: rgb("eb5278"), stroke: black, thickness: 2pt, #circle(fill: rgb("eb5278"), stroke: 2pt + black,
align(center + horizon)[But, soft!] align(center + horizon)[But, soft!]
) )

View File

@ -17,7 +17,7 @@ Rect in ellipse in fixed rect. \
) )
Auto-sized ellipse. \ Auto-sized ellipse. \
#ellipse(fill: conifer, stroke: forest, thickness: 3pt, padding: 3pt)[ #ellipse(fill: conifer, stroke: 3pt + forest, padding: 3pt)[
#set text(8pt) #set text(8pt)
But, soft! what light through yonder window breaks? But, soft! what light through yonder window breaks?
] ]

View File

@ -6,15 +6,15 @@
variant(stroke: none), variant(stroke: none),
variant(), variant(),
variant(fill: none), variant(fill: none),
variant(thickness: 2pt), variant(stroke: 2pt),
variant(stroke: eastern), variant(stroke: eastern),
variant(stroke: eastern, thickness: 2pt), variant(stroke: eastern + 2pt),
variant(fill: eastern), variant(fill: eastern),
variant(fill: eastern, stroke: none), 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: conifer),
variant(fill: forest, stroke: black, thickness: 2pt), variant(fill: forest, stroke: black + 2pt),
variant(fill: forest, stroke: conifer, thickness: 2pt), variant(fill: forest, stroke: conifer + 2pt),
) { ) {
(align(horizon)[{i + 1}.], item, []) (align(horizon)[{i + 1}.], item, [])
} }
@ -24,3 +24,17 @@
gutter: 5pt, gutter: 5pt,
..items, ..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)

View File

@ -14,8 +14,7 @@
#block(rect( #block(rect(
height: 15pt, height: 15pt,
fill: rgb("46b3c2"), fill: rgb("46b3c2"),
stroke: rgb("234994"), stroke: 2pt + rgb("234994"),
thickness: 2pt,
)) ))
// Fixed width, text height. // Fixed width, text height.

View File

@ -1,13 +1,18 @@
// Test tables.
---
#set page(height: 70pt) #set page(height: 70pt)
#set table(primary: rgb("aaa"), secondary: none) #set table(primary: rgb("aaa"), secondary: none)
#table( #table(
columns: (1fr,) * 3, columns: (1fr,) * 3,
stroke: rgb("333"), stroke: 2pt + rgb("333"),
thickness: 2pt,
[A], [B], [C], [], [], [D \ E \ F \ \ \ G], [H], [A], [B], [C], [], [], [D \ E \ F \ \ \ G], [H],
) )
---
#table(columns: 3, stroke: none, fill: green, [A], [B], [C])
--- ---
// Ref: false // Ref: false
#table() #table()

View File

@ -20,12 +20,14 @@
--- ---
#let redact = strike.with(10pt, extent: 0.05em) #let redact = strike.with(10pt, extent: 0.05em)
#let highlight = strike.with( #let highlight = strike.with(stroke: 10pt + rgb("abcdef88"), extent: 0.05em)
stroke: rgb("abcdef88"),
thickness: 10pt,
extent: 0.05em,
)
// Abuse thickness and transparency for redacting and highlighting stuff. // Abuse thickness and transparency for redacting and highlighting stuff.
Sometimes, we work #redact[in secret]. Sometimes, we work #redact[in secret].
There might be #highlight[redacted] things. There might be #highlight[redacted] things.
underline()
---
// Test stroke folding.
#set underline(stroke: 2pt, offset: 2pt)
#underline(text(red, [DANGER!]))