1use ecow::EcoString;
2use typst_utils::Numeric;
3
4use crate::diag::{HintedStrResult, SourceResult};
5use crate::foundations::{
6 Args, Cast, Dict, Fold, FromValue, NoneValue, Repr, Resolve, Smart, StyleChain,
7 Value, cast, dict, func, scope, ty,
8};
9use crate::layout::{Abs, Length, Ratio};
10use crate::visualize::{Color, Gradient, Paint, Tiling};
11
12#[ty(scope, cast)]
53#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
54pub struct Stroke<T: Numeric = Length> {
55 pub paint: Smart<Paint>,
57 pub thickness: Smart<T>,
59 pub cap: Smart<LineCap>,
61 pub join: Smart<LineJoin>,
63 pub dash: Smart<Option<DashPattern<T>>>,
65 pub miter_limit: Smart<Ratio>,
67}
68
69impl Stroke {
70 pub fn from_pair(paint: impl Into<Paint>, thickness: Length) -> Self {
72 Self {
73 paint: Smart::Custom(paint.into()),
74 thickness: Smart::Custom(thickness),
75 ..Default::default()
76 }
77 }
78}
79
80#[scope]
81impl Stroke {
82 #[func(constructor)]
100 pub fn construct(
101 args: &mut Args,
102
103 #[external]
107 paint: Smart<Paint>,
108
109 #[external]
113 thickness: Smart<Length>,
114
115 #[external]
120 cap: Smart<LineCap>,
121
122 #[external]
127 join: Smart<LineJoin>,
128
129 #[external]
196 dash: Smart<Option<DashPattern>>,
197
198 #[external]
226 miter_limit: Smart<f64>,
227 ) -> SourceResult<Stroke> {
228 if let Some(stroke) = args.eat::<Stroke>()? {
229 return Ok(stroke);
230 }
231
232 fn take<T: FromValue>(args: &mut Args, arg: &str) -> SourceResult<Smart<T>> {
233 Ok(args.named::<Smart<T>>(arg)?.unwrap_or(Smart::Auto))
234 }
235
236 let paint = take::<Paint>(args, "paint")?;
237 let thickness = take::<Length>(args, "thickness")?;
238 let cap = take::<LineCap>(args, "cap")?;
239 let join = take::<LineJoin>(args, "join")?;
240 let dash = take::<Option<DashPattern>>(args, "dash")?;
241 let miter_limit = take::<f64>(args, "miter-limit")?.map(Ratio::new);
242
243 Ok(Self { paint, thickness, cap, join, dash, miter_limit })
244 }
245}
246
247impl<T: Numeric> Stroke<T> {
248 pub fn map<F, U: Numeric>(self, f: F) -> Stroke<U>
250 where
251 F: Fn(T) -> U,
252 {
253 Stroke {
254 paint: self.paint,
255 thickness: self.thickness.map(&f),
256 cap: self.cap,
257 join: self.join,
258 dash: self.dash.map(|dash| {
259 dash.map(|dash| DashPattern {
260 array: dash
261 .array
262 .into_iter()
263 .map(|l| match l {
264 DashLength::Length(v) => DashLength::Length(f(v)),
265 DashLength::LineWidth => DashLength::LineWidth,
266 })
267 .collect(),
268 phase: f(dash.phase),
269 })
270 }),
271 miter_limit: self.miter_limit,
272 }
273 }
274}
275
276impl Stroke<Abs> {
277 pub fn unwrap_or(self, default: FixedStroke) -> FixedStroke {
279 let thickness = self.thickness.unwrap_or(default.thickness);
280 let dash = self
281 .dash
282 .map(|dash| {
283 dash.map(|dash| DashPattern {
284 array: dash.array.into_iter().map(|l| l.finish(thickness)).collect(),
285 phase: dash.phase,
286 })
287 })
288 .unwrap_or(default.dash);
289
290 FixedStroke {
291 paint: self.paint.unwrap_or(default.paint),
292 thickness,
293 cap: self.cap.unwrap_or(default.cap),
294 join: self.join.unwrap_or(default.join),
295 dash,
296 miter_limit: self.miter_limit.unwrap_or(default.miter_limit),
297 }
298 }
299
300 pub fn unwrap_or_default(self) -> FixedStroke {
302 #[allow(clippy::unwrap_or_default)]
304 self.unwrap_or(FixedStroke::default())
305 }
306}
307
308impl<T: Numeric + Repr> Repr for Stroke<T> {
309 fn repr(&self) -> EcoString {
310 let mut r = EcoString::new();
311 let Self { paint, thickness, cap, join, dash, miter_limit } = &self;
312 if cap.is_auto() && join.is_auto() && dash.is_auto() && miter_limit.is_auto() {
313 match (&self.paint, &self.thickness) {
314 (Smart::Custom(paint), Smart::Custom(thickness)) => {
315 r.push_str(&thickness.repr());
316 r.push_str(" + ");
317 r.push_str(&paint.repr());
318 }
319 (Smart::Custom(paint), Smart::Auto) => r.push_str(&paint.repr()),
320 (Smart::Auto, Smart::Custom(thickness)) => r.push_str(&thickness.repr()),
321 (Smart::Auto, Smart::Auto) => r.push_str("1pt + black"),
322 }
323 } else {
324 r.push('(');
325 let mut sep = "";
326 if let Smart::Custom(paint) = &paint {
327 r.push_str(sep);
328 r.push_str("paint: ");
329 r.push_str(&paint.repr());
330 sep = ", ";
331 }
332 if let Smart::Custom(thickness) = &thickness {
333 r.push_str(sep);
334 r.push_str("thickness: ");
335 r.push_str(&thickness.repr());
336 sep = ", ";
337 }
338 if let Smart::Custom(cap) = &cap {
339 r.push_str(sep);
340 r.push_str("cap: ");
341 r.push_str(&cap.repr());
342 sep = ", ";
343 }
344 if let Smart::Custom(join) = &join {
345 r.push_str(sep);
346 r.push_str("join: ");
347 r.push_str(&join.repr());
348 sep = ", ";
349 }
350 if let Smart::Custom(dash) = &dash {
351 r.push_str(sep);
352 r.push_str("dash: ");
353 if let Some(dash) = dash {
354 r.push_str(&dash.repr());
355 } else {
356 r.push_str(&NoneValue.repr());
357 }
358 sep = ", ";
359 }
360 if let Smart::Custom(miter_limit) = &miter_limit {
361 r.push_str(sep);
362 r.push_str("miter-limit: ");
363 r.push_str(&miter_limit.get().repr());
364 }
365 r.push(')');
366 }
367 r
368 }
369}
370
371impl<T: Numeric + Fold> Fold for Stroke<T> {
372 fn fold(self, outer: Self) -> Self {
373 Self {
374 paint: self.paint.or(outer.paint),
375 thickness: self.thickness.or(outer.thickness),
376 cap: self.cap.or(outer.cap),
377 join: self.join.or(outer.join),
378 dash: self.dash.or(outer.dash),
379 miter_limit: self.miter_limit.or(outer.miter_limit),
380 }
381 }
382}
383
384impl Resolve for Stroke {
385 type Output = Stroke<Abs>;
386
387 fn resolve(self, styles: StyleChain) -> Self::Output {
388 Stroke {
389 paint: self.paint,
390 thickness: self.thickness.resolve(styles),
391 cap: self.cap,
392 join: self.join,
393 dash: self.dash.resolve(styles),
394 miter_limit: self.miter_limit,
395 }
396 }
397}
398
399cast! {
400 type Stroke,
401 thickness: Length => Self {
402 thickness: Smart::Custom(thickness),
403 ..Default::default()
404 },
405 color: Color => Self {
406 paint: Smart::Custom(color.into()),
407 ..Default::default()
408 },
409 gradient: Gradient => Self {
410 paint: Smart::Custom(gradient.into()),
411 ..Default::default()
412 },
413 tiling: Tiling => Self {
414 paint: Smart::Custom(tiling.into()),
415 ..Default::default()
416 },
417 mut dict: Dict => {
418 fn take<T: FromValue>(dict: &mut Dict, key: &str) -> HintedStrResult<Smart<T>> {
420 Ok(dict.take(key).ok().map(Smart::<T>::from_value)
421 .transpose()?.unwrap_or(Smart::Auto))
422 }
423
424 let paint = take::<Paint>(&mut dict, "paint")?;
425 let thickness = take::<Length>(&mut dict, "thickness")?;
426 let cap = take::<LineCap>(&mut dict, "cap")?;
427 let join = take::<LineJoin>(&mut dict, "join")?;
428 let dash = take::<Option<DashPattern>>(&mut dict, "dash")?;
429 let miter_limit = take::<f64>(&mut dict, "miter-limit")?;
430 dict.finish(&["paint", "thickness", "cap", "join", "dash", "miter-limit"])?;
431
432 Self {
433 paint,
434 thickness,
435 cap,
436 join,
437 dash,
438 miter_limit: miter_limit.map(Ratio::new),
439 }
440 },
441}
442
443cast! {
444 Stroke<Abs>,
445 self => self.map(Length::from).into_value(),
446}
447
448#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
450pub enum LineCap {
451 Butt,
453 Round,
455 Square,
457}
458
459impl Repr for LineCap {
460 fn repr(&self) -> EcoString {
461 match self {
462 Self::Butt => "butt".repr(),
463 Self::Round => "round".repr(),
464 Self::Square => "square".repr(),
465 }
466 }
467}
468
469#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
471pub enum LineJoin {
472 Miter,
475 Round,
477 Bevel,
480}
481
482impl Repr for LineJoin {
483 fn repr(&self) -> EcoString {
484 match self {
485 Self::Miter => "miter".repr(),
486 Self::Round => "round".repr(),
487 Self::Bevel => "bevel".repr(),
488 }
489 }
490}
491
492#[derive(Debug, Clone, Eq, PartialEq, Hash)]
494pub struct DashPattern<T: Numeric = Length, DT = DashLength<T>> {
495 pub array: Vec<DT>,
497 pub phase: T,
499}
500
501impl<T: Numeric + Repr, DT: Repr> Repr for DashPattern<T, DT> {
502 fn repr(&self) -> EcoString {
503 let mut r = EcoString::from("(array: (");
504 for (i, elem) in self.array.iter().enumerate() {
505 if i != 0 {
506 r.push_str(", ")
507 }
508 r.push_str(&elem.repr())
509 }
510 r.push_str("), phase: ");
511 r.push_str(&self.phase.repr());
512 r.push(')');
513 r
514 }
515}
516
517impl<T: Numeric + Default> From<Vec<DashLength<T>>> for DashPattern<T> {
518 fn from(array: Vec<DashLength<T>>) -> Self {
519 Self { array, phase: T::default() }
520 }
521}
522
523impl Resolve for DashPattern {
524 type Output = DashPattern<Abs>;
525
526 fn resolve(self, styles: StyleChain) -> Self::Output {
527 DashPattern {
528 array: self.array.into_iter().map(|l| l.resolve(styles)).collect(),
529 phase: self.phase.resolve(styles),
530 }
531 }
532}
533
534cast! {
537 DashPattern,
538 self => dict! { "array" => self.array, "phase" => self.phase }.into_value(),
539
540 "solid" => Vec::new().into(),
542 "dotted" => vec![DashLength::LineWidth, Abs::pt(2.0).into()].into(),
544 "densely-dotted" => vec![DashLength::LineWidth, Abs::pt(1.0).into()].into(),
546 "loosely-dotted" => vec![DashLength::LineWidth, Abs::pt(4.0).into()].into(),
548 "dashed" => vec![Abs::pt(3.0).into(), Abs::pt(3.0).into()].into(),
550 "densely-dashed" => vec![Abs::pt(3.0).into(), Abs::pt(2.0).into()].into(),
552 "loosely-dashed" => vec![Abs::pt(3.0).into(), Abs::pt(6.0).into()].into(),
554 "dash-dotted" => vec![Abs::pt(3.0).into(), Abs::pt(2.0).into(), DashLength::LineWidth, Abs::pt(2.0).into()].into(),
556 "densely-dash-dotted" => vec![Abs::pt(3.0).into(), Abs::pt(1.0).into(), DashLength::LineWidth, Abs::pt(1.0).into()].into(),
558 "loosely-dash-dotted" => vec![Abs::pt(3.0).into(), Abs::pt(4.0).into(), DashLength::LineWidth, Abs::pt(4.0).into()].into(),
560
561 array: Vec<DashLength> => Self { array, phase: Length::zero() },
562 mut dict: Dict => {
563 let array: Vec<DashLength> = dict.take("array")?.cast()?;
564 let phase = dict.take("phase").ok().map(Value::cast)
565 .transpose()?.unwrap_or(Length::zero());
566 dict.finish(&["array", "phase"])?;
567 Self {
568 array,
569 phase,
570 }
571 },
572}
573
574#[derive(Debug, Clone, Eq, PartialEq, Hash)]
576pub enum DashLength<T: Numeric = Length> {
577 LineWidth,
578 Length(T),
579}
580
581impl<T: Numeric> DashLength<T> {
582 fn finish(self, line_width: T) -> T {
583 match self {
584 Self::LineWidth => line_width,
585 Self::Length(l) => l,
586 }
587 }
588}
589
590impl<T: Numeric + Repr> Repr for DashLength<T> {
591 fn repr(&self) -> EcoString {
592 match self {
593 Self::LineWidth => "dot".repr(),
594 Self::Length(v) => v.repr(),
595 }
596 }
597}
598
599impl Resolve for DashLength {
600 type Output = DashLength<Abs>;
601
602 fn resolve(self, styles: StyleChain) -> Self::Output {
603 match self {
604 Self::LineWidth => DashLength::LineWidth,
605 Self::Length(v) => DashLength::Length(v.resolve(styles)),
606 }
607 }
608}
609
610impl From<Abs> for DashLength {
611 fn from(l: Abs) -> Self {
612 DashLength::Length(l.into())
613 }
614}
615
616cast! {
617 DashLength,
618 self => match self {
619 Self::LineWidth => "dot".into_value(),
620 Self::Length(v) => v.into_value(),
621 },
622 "dot" => Self::LineWidth,
623 v: Length => Self::Length(v),
624}
625
626#[derive(Debug, Clone, Eq, PartialEq, Hash)]
628pub struct FixedStroke {
629 pub paint: Paint,
631 pub thickness: Abs,
633 pub cap: LineCap,
635 pub join: LineJoin,
637 pub dash: Option<DashPattern<Abs, Abs>>,
639 pub miter_limit: Ratio,
641}
642
643impl FixedStroke {
644 pub fn from_pair(paint: impl Into<Paint>, thickness: Abs) -> Self {
646 Self {
647 paint: paint.into(),
648 thickness,
649 ..Default::default()
650 }
651 }
652}
653
654impl Default for FixedStroke {
655 fn default() -> Self {
656 Self {
657 paint: Paint::Solid(Color::BLACK),
658 thickness: Abs::pt(1.0),
659 cap: LineCap::Butt,
660 join: LineJoin::Miter,
661 dash: None,
662 miter_limit: Ratio::new(4.0),
663 }
664 }
665}