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 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",
));
}

View File

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

View File

@ -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()))
};
}
}
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::<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::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;

View File

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

View File

@ -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::*;

View File

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

View File

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

View File

@ -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),
}
}
}

View File

@ -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::*;

View File

@ -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)])

View File

@ -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) {

View File

@ -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(),
}

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::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::*;

View File

@ -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);

View File

@ -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));
}
};

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.
---
// 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}

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.

View File

@ -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!]
)

View File

@ -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?
]

View File

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

View File

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

View File

@ -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()

View File

@ -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!]))