Sum color and length into stroke
@ -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",
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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),
|
||||
})
|
||||
}
|
||||
|
||||
|
134
src/eval/raw.rs
@ -1,8 +1,11 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
use std::ops::{Add, Div, Mul, Neg};
|
||||
|
||||
use super::{Resolve, StyleChain};
|
||||
use crate::geom::{Align, Em, Length, Numeric, Relative, SpecAxis};
|
||||
use super::{Fold, Resolve, Smart, StyleChain, Value};
|
||||
use crate::geom::{
|
||||
Align, Em, Get, Length, Numeric, Paint, Relative, Spec, SpecAxis, Stroke,
|
||||
};
|
||||
use crate::library::text::{ParNode, TextNode};
|
||||
|
||||
/// The unresolved alignment representation.
|
||||
@ -49,6 +52,101 @@ impl Debug for RawAlign {
|
||||
}
|
||||
}
|
||||
|
||||
dynamic! {
|
||||
RawAlign: "alignment",
|
||||
}
|
||||
|
||||
dynamic! {
|
||||
Spec<RawAlign>: "2d alignment",
|
||||
}
|
||||
|
||||
castable! {
|
||||
Spec<Option<RawAlign>>,
|
||||
Expected: "1d or 2d alignment",
|
||||
@align: RawAlign => {
|
||||
let mut aligns = Spec::default();
|
||||
aligns.set(align.axis(), Some(*align));
|
||||
aligns
|
||||
},
|
||||
@aligns: Spec<RawAlign> => aligns.map(Some),
|
||||
}
|
||||
|
||||
/// The unresolved stroke representation.
|
||||
///
|
||||
/// In this representation, both fields are optional so that you can pass either
|
||||
/// just a paint (`red`), just a thickness (`0.1em`) or both (`2pt + red`) where
|
||||
/// this is expected.
|
||||
#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct RawStroke<T = RawLength> {
|
||||
/// The stroke's paint.
|
||||
pub paint: Smart<Paint>,
|
||||
/// The stroke's thickness.
|
||||
pub thickness: Smart<T>,
|
||||
}
|
||||
|
||||
impl RawStroke<Length> {
|
||||
/// Unpack the stroke, filling missing fields with `default`.
|
||||
pub fn unwrap_or(self, default: Stroke) -> Stroke {
|
||||
Stroke {
|
||||
paint: self.paint.unwrap_or(default.paint),
|
||||
thickness: self.thickness.unwrap_or(default.thickness),
|
||||
}
|
||||
}
|
||||
|
||||
/// Unpack the stroke, filling missing fields with the default values.
|
||||
pub fn unwrap_or_default(self) -> Stroke {
|
||||
self.unwrap_or(Stroke::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve for RawStroke {
|
||||
type Output = RawStroke<Length>;
|
||||
|
||||
fn resolve(self, styles: StyleChain) -> Self::Output {
|
||||
RawStroke {
|
||||
paint: self.paint,
|
||||
thickness: self.thickness.resolve(styles),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This faciliates RawStroke => Stroke.
|
||||
impl Fold for RawStroke<Length> {
|
||||
type Output = Self;
|
||||
|
||||
fn fold(self, outer: Self::Output) -> Self::Output {
|
||||
Self {
|
||||
paint: self.paint.or(outer.paint),
|
||||
thickness: self.thickness.or(outer.thickness),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Debug> Debug for RawStroke<T> {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
match (self.paint, &self.thickness) {
|
||||
(Smart::Custom(paint), Smart::Custom(thickness)) => {
|
||||
write!(f, "{thickness:?} + {paint:?}")
|
||||
}
|
||||
(Smart::Custom(paint), Smart::Auto) => paint.fmt(f),
|
||||
(Smart::Auto, Smart::Custom(thickness)) => thickness.fmt(f),
|
||||
(Smart::Auto, Smart::Auto) => f.pad("<stroke>"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dynamic! {
|
||||
RawStroke: "stroke",
|
||||
Value::Length(thickness) => Self {
|
||||
paint: Smart::Auto,
|
||||
thickness: Smart::Custom(thickness),
|
||||
},
|
||||
Value::Color(color) => Self {
|
||||
paint: Smart::Custom(color.into()),
|
||||
thickness: Smart::Auto,
|
||||
},
|
||||
}
|
||||
|
||||
/// The unresolved length representation.
|
||||
///
|
||||
/// Currently supports absolute and em units, but support could quite easily be
|
||||
@ -56,7 +154,7 @@ impl Debug for RawAlign {
|
||||
/// Probably, it would be a good idea to then move to an enum representation
|
||||
/// that has a small footprint and allocates for the rare case that units are
|
||||
/// mixed.
|
||||
#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct RawLength {
|
||||
/// The absolute part.
|
||||
pub length: Length,
|
||||
@ -101,6 +199,26 @@ impl Resolve for RawLength {
|
||||
}
|
||||
}
|
||||
|
||||
impl Numeric for RawLength {
|
||||
fn zero() -> Self {
|
||||
Self::zero()
|
||||
}
|
||||
|
||||
fn is_finite(self) -> bool {
|
||||
self.length.is_finite() && self.em.is_finite()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for RawLength {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
if self.em.is_zero() && other.em.is_zero() {
|
||||
self.length.partial_cmp(&other.length)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Length> for RawLength {
|
||||
fn from(length: Length) -> Self {
|
||||
Self { length, em: Em::zero() }
|
||||
@ -119,16 +237,6 @@ impl From<Length> for Relative<RawLength> {
|
||||
}
|
||||
}
|
||||
|
||||
impl Numeric for RawLength {
|
||||
fn zero() -> Self {
|
||||
Self::zero()
|
||||
}
|
||||
|
||||
fn is_finite(self) -> bool {
|
||||
self.length.is_finite() && self.em.is_finite()
|
||||
}
|
||||
}
|
||||
|
||||
impl Neg for RawLength {
|
||||
type Output = Self;
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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::*;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
43
src/frame.rs
@ -5,7 +5,7 @@ use std::sync::Arc;
|
||||
|
||||
use crate::font::FaceId;
|
||||
use crate::geom::{
|
||||
Align, Em, Length, Numeric, Paint, Path, Point, Size, Spec, Transform,
|
||||
Align, Em, Length, Numeric, Paint, Path, Point, Size, Spec, Stroke, Transform,
|
||||
};
|
||||
use crate::image::ImageId;
|
||||
|
||||
@ -223,22 +223,6 @@ pub struct Shape {
|
||||
pub stroke: Option<Stroke>,
|
||||
}
|
||||
|
||||
impl Shape {
|
||||
/// Create a filled shape without a stroke.
|
||||
pub fn filled(geometry: Geometry, fill: Paint) -> Self {
|
||||
Self { geometry, fill: Some(fill), stroke: None }
|
||||
}
|
||||
|
||||
/// Create a stroked shape without a fill.
|
||||
pub fn stroked(geometry: Geometry, stroke: Stroke) -> Self {
|
||||
Self {
|
||||
geometry,
|
||||
fill: None,
|
||||
stroke: Some(stroke),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A shape's geometry.
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum Geometry {
|
||||
@ -252,11 +236,22 @@ pub enum Geometry {
|
||||
Path(Path),
|
||||
}
|
||||
|
||||
/// A stroke of a geometric shape.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Stroke {
|
||||
/// The stroke's paint.
|
||||
pub paint: Paint,
|
||||
/// The stroke's thickness.
|
||||
pub thickness: Length,
|
||||
impl Geometry {
|
||||
/// Fill the geometry without a stroke.
|
||||
pub fn filled(self, fill: Paint) -> Shape {
|
||||
Shape {
|
||||
geometry: self,
|
||||
fill: Some(fill),
|
||||
stroke: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Stroke the geometry without a fill.
|
||||
pub fn stroked(self, stroke: Stroke) -> Shape {
|
||||
Shape {
|
||||
geometry: self,
|
||||
fill: None,
|
||||
stroke: Some(stroke),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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::*;
|
||||
|
@ -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)])
|
||||
|
@ -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) {
|
||||
|
@ -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(),
|
||||
}
|
||||
|
@ -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::*;
|
||||
|
@ -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);
|
||||
|
@ -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));
|
||||
}
|
||||
};
|
||||
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 31 KiB |
@ -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}
|
||||
|
@ -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.
|
||||
|
@ -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!]
|
||||
)
|
||||
|
||||
|
@ -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?
|
||||
]
|
||||
|
@ -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)
|
||||
|
@ -14,8 +14,7 @@
|
||||
#block(rect(
|
||||
height: 15pt,
|
||||
fill: rgb("46b3c2"),
|
||||
stroke: rgb("234994"),
|
||||
thickness: 2pt,
|
||||
stroke: 2pt + rgb("234994"),
|
||||
))
|
||||
|
||||
// Fixed width, text height.
|
||||
|
@ -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()
|
||||
|
@ -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!]))
|
||||
|