1use ecow::EcoString;
2use typst_utils::{Numeric, Scalar};
3
4use crate::diag::{HintedStrResult, SourceResult};
5use crate::foundations::{
6 cast, dict, func, scope, ty, Args, Cast, Dict, Fold, FromValue, NoneValue, Repr,
7 Resolve, Smart, StyleChain, Value,
8};
9use crate::layout::{Abs, Length};
10use crate::visualize::{Color, Gradient, Paint, Tiling};
11
12#[ty(scope, cast)]
52#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
53pub struct Stroke<T: Numeric = Length> {
54 pub paint: Smart<Paint>,
56 pub thickness: Smart<T>,
58 pub cap: Smart<LineCap>,
60 pub join: Smart<LineJoin>,
62 pub dash: Smart<Option<DashPattern<T>>>,
64 pub miter_limit: Smart<Scalar>,
66}
67
68impl Stroke {
69 pub fn from_pair(paint: impl Into<Paint>, thickness: Length) -> Self {
71 Self {
72 paint: Smart::Custom(paint.into()),
73 thickness: Smart::Custom(thickness),
74 ..Default::default()
75 }
76 }
77}
78
79#[scope]
80impl Stroke {
81 #[func(constructor)]
99 pub fn construct(
100 args: &mut Args,
101
102 #[external]
106 paint: Smart<Paint>,
107
108 #[external]
112 thickness: Smart<Length>,
113
114 #[external]
118 cap: Smart<LineCap>,
119
120 #[external]
124 join: Smart<LineJoin>,
125
126 #[external]
158 dash: Smart<Option<DashPattern>>,
159
160 #[external]
188 miter_limit: Smart<f64>,
189 ) -> SourceResult<Stroke> {
190 if let Some(stroke) = args.eat::<Stroke>()? {
191 return Ok(stroke);
192 }
193
194 fn take<T: FromValue>(args: &mut Args, arg: &str) -> SourceResult<Smart<T>> {
195 Ok(args.named::<Smart<T>>(arg)?.unwrap_or(Smart::Auto))
196 }
197
198 let paint = take::<Paint>(args, "paint")?;
199 let thickness = take::<Length>(args, "thickness")?;
200 let cap = take::<LineCap>(args, "cap")?;
201 let join = take::<LineJoin>(args, "join")?;
202 let dash = take::<Option<DashPattern>>(args, "dash")?;
203 let miter_limit = take::<f64>(args, "miter-limit")?.map(Scalar::new);
204
205 Ok(Self { paint, thickness, cap, join, dash, miter_limit })
206 }
207}
208
209impl<T: Numeric> Stroke<T> {
210 pub fn map<F, U: Numeric>(self, f: F) -> Stroke<U>
212 where
213 F: Fn(T) -> U,
214 {
215 Stroke {
216 paint: self.paint,
217 thickness: self.thickness.map(&f),
218 cap: self.cap,
219 join: self.join,
220 dash: self.dash.map(|dash| {
221 dash.map(|dash| DashPattern {
222 array: dash
223 .array
224 .into_iter()
225 .map(|l| match l {
226 DashLength::Length(v) => DashLength::Length(f(v)),
227 DashLength::LineWidth => DashLength::LineWidth,
228 })
229 .collect(),
230 phase: f(dash.phase),
231 })
232 }),
233 miter_limit: self.miter_limit,
234 }
235 }
236}
237
238impl Stroke<Abs> {
239 pub fn unwrap_or(self, default: FixedStroke) -> FixedStroke {
241 let thickness = self.thickness.unwrap_or(default.thickness);
242 let dash = self
243 .dash
244 .map(|dash| {
245 dash.map(|dash| DashPattern {
246 array: dash.array.into_iter().map(|l| l.finish(thickness)).collect(),
247 phase: dash.phase,
248 })
249 })
250 .unwrap_or(default.dash);
251
252 FixedStroke {
253 paint: self.paint.unwrap_or(default.paint),
254 thickness,
255 cap: self.cap.unwrap_or(default.cap),
256 join: self.join.unwrap_or(default.join),
257 dash,
258 miter_limit: self.miter_limit.unwrap_or(default.miter_limit),
259 }
260 }
261
262 pub fn unwrap_or_default(self) -> FixedStroke {
264 #[allow(clippy::unwrap_or_default)]
266 self.unwrap_or(FixedStroke::default())
267 }
268}
269
270impl<T: Numeric + Repr> Repr for Stroke<T> {
271 fn repr(&self) -> EcoString {
272 let mut r = EcoString::new();
273 let Self { paint, thickness, cap, join, dash, miter_limit } = &self;
274 if cap.is_auto() && join.is_auto() && dash.is_auto() && miter_limit.is_auto() {
275 match (&self.paint, &self.thickness) {
276 (Smart::Custom(paint), Smart::Custom(thickness)) => {
277 r.push_str(&thickness.repr());
278 r.push_str(" + ");
279 r.push_str(&paint.repr());
280 }
281 (Smart::Custom(paint), Smart::Auto) => r.push_str(&paint.repr()),
282 (Smart::Auto, Smart::Custom(thickness)) => r.push_str(&thickness.repr()),
283 (Smart::Auto, Smart::Auto) => r.push_str("1pt + black"),
284 }
285 } else {
286 r.push('(');
287 let mut sep = "";
288 if let Smart::Custom(paint) = &paint {
289 r.push_str(sep);
290 r.push_str("paint: ");
291 r.push_str(&paint.repr());
292 sep = ", ";
293 }
294 if let Smart::Custom(thickness) = &thickness {
295 r.push_str(sep);
296 r.push_str("thickness: ");
297 r.push_str(&thickness.repr());
298 sep = ", ";
299 }
300 if let Smart::Custom(cap) = &cap {
301 r.push_str(sep);
302 r.push_str("cap: ");
303 r.push_str(&cap.repr());
304 sep = ", ";
305 }
306 if let Smart::Custom(join) = &join {
307 r.push_str(sep);
308 r.push_str("join: ");
309 r.push_str(&join.repr());
310 sep = ", ";
311 }
312 if let Smart::Custom(dash) = &dash {
313 r.push_str(sep);
314 r.push_str("dash: ");
315 if let Some(dash) = dash {
316 r.push_str(&dash.repr());
317 } else {
318 r.push_str(&NoneValue.repr());
319 }
320 sep = ", ";
321 }
322 if let Smart::Custom(miter_limit) = &miter_limit {
323 r.push_str(sep);
324 r.push_str("miter-limit: ");
325 r.push_str(&miter_limit.get().repr());
326 }
327 r.push(')');
328 }
329 r
330 }
331}
332
333impl<T: Numeric + Fold> Fold for Stroke<T> {
334 fn fold(self, outer: Self) -> Self {
335 Self {
336 paint: self.paint.or(outer.paint),
337 thickness: self.thickness.or(outer.thickness),
338 cap: self.cap.or(outer.cap),
339 join: self.join.or(outer.join),
340 dash: self.dash.or(outer.dash),
341 miter_limit: self.miter_limit.or(outer.miter_limit),
342 }
343 }
344}
345
346impl Resolve for Stroke {
347 type Output = Stroke<Abs>;
348
349 fn resolve(self, styles: StyleChain) -> Self::Output {
350 Stroke {
351 paint: self.paint,
352 thickness: self.thickness.resolve(styles),
353 cap: self.cap,
354 join: self.join,
355 dash: self.dash.resolve(styles),
356 miter_limit: self.miter_limit,
357 }
358 }
359}
360
361cast! {
362 type Stroke,
363 thickness: Length => Self {
364 thickness: Smart::Custom(thickness),
365 ..Default::default()
366 },
367 color: Color => Self {
368 paint: Smart::Custom(color.into()),
369 ..Default::default()
370 },
371 gradient: Gradient => Self {
372 paint: Smart::Custom(gradient.into()),
373 ..Default::default()
374 },
375 tiling: Tiling => Self {
376 paint: Smart::Custom(tiling.into()),
377 ..Default::default()
378 },
379 mut dict: Dict => {
380 fn take<T: FromValue>(dict: &mut Dict, key: &str) -> HintedStrResult<Smart<T>> {
382 Ok(dict.take(key).ok().map(Smart::<T>::from_value)
383 .transpose()?.unwrap_or(Smart::Auto))
384 }
385
386 let paint = take::<Paint>(&mut dict, "paint")?;
387 let thickness = take::<Length>(&mut dict, "thickness")?;
388 let cap = take::<LineCap>(&mut dict, "cap")?;
389 let join = take::<LineJoin>(&mut dict, "join")?;
390 let dash = take::<Option<DashPattern>>(&mut dict, "dash")?;
391 let miter_limit = take::<f64>(&mut dict, "miter-limit")?;
392 dict.finish(&["paint", "thickness", "cap", "join", "dash", "miter-limit"])?;
393
394 Self {
395 paint,
396 thickness,
397 cap,
398 join,
399 dash,
400 miter_limit: miter_limit.map(Scalar::new),
401 }
402 },
403}
404
405cast! {
406 Stroke<Abs>,
407 self => self.map(Length::from).into_value(),
408}
409
410#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
412pub enum LineCap {
413 Butt,
415 Round,
417 Square,
419}
420
421impl Repr for LineCap {
422 fn repr(&self) -> EcoString {
423 match self {
424 Self::Butt => "butt".repr(),
425 Self::Round => "round".repr(),
426 Self::Square => "square".repr(),
427 }
428 }
429}
430
431#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
433pub enum LineJoin {
434 Miter,
437 Round,
439 Bevel,
442}
443
444impl Repr for LineJoin {
445 fn repr(&self) -> EcoString {
446 match self {
447 Self::Miter => "miter".repr(),
448 Self::Round => "round".repr(),
449 Self::Bevel => "bevel".repr(),
450 }
451 }
452}
453
454#[derive(Debug, Clone, Eq, PartialEq, Hash)]
456pub struct DashPattern<T: Numeric = Length, DT = DashLength<T>> {
457 pub array: Vec<DT>,
459 pub phase: T,
461}
462
463impl<T: Numeric + Repr, DT: Repr> Repr for DashPattern<T, DT> {
464 fn repr(&self) -> EcoString {
465 let mut r = EcoString::from("(array: (");
466 for (i, elem) in self.array.iter().enumerate() {
467 if i != 0 {
468 r.push_str(", ")
469 }
470 r.push_str(&elem.repr())
471 }
472 r.push_str("), phase: ");
473 r.push_str(&self.phase.repr());
474 r.push(')');
475 r
476 }
477}
478
479impl<T: Numeric + Default> From<Vec<DashLength<T>>> for DashPattern<T> {
480 fn from(array: Vec<DashLength<T>>) -> Self {
481 Self { array, phase: T::default() }
482 }
483}
484
485impl Resolve for DashPattern {
486 type Output = DashPattern<Abs>;
487
488 fn resolve(self, styles: StyleChain) -> Self::Output {
489 DashPattern {
490 array: self.array.into_iter().map(|l| l.resolve(styles)).collect(),
491 phase: self.phase.resolve(styles),
492 }
493 }
494}
495
496cast! {
499 DashPattern,
500 self => dict! { "array" => self.array, "phase" => self.phase }.into_value(),
501
502 "solid" => Vec::new().into(),
503 "dotted" => vec![DashLength::LineWidth, Abs::pt(2.0).into()].into(),
504 "densely-dotted" => vec![DashLength::LineWidth, Abs::pt(1.0).into()].into(),
505 "loosely-dotted" => vec![DashLength::LineWidth, Abs::pt(4.0).into()].into(),
506 "dashed" => vec![Abs::pt(3.0).into(), Abs::pt(3.0).into()].into(),
507 "densely-dashed" => vec![Abs::pt(3.0).into(), Abs::pt(2.0).into()].into(),
508 "loosely-dashed" => vec![Abs::pt(3.0).into(), Abs::pt(6.0).into()].into(),
509 "dash-dotted" => vec![Abs::pt(3.0).into(), Abs::pt(2.0).into(), DashLength::LineWidth, Abs::pt(2.0).into()].into(),
510 "densely-dash-dotted" => vec![Abs::pt(3.0).into(), Abs::pt(1.0).into(), DashLength::LineWidth, Abs::pt(1.0).into()].into(),
511 "loosely-dash-dotted" => vec![Abs::pt(3.0).into(), Abs::pt(4.0).into(), DashLength::LineWidth, Abs::pt(4.0).into()].into(),
512
513 array: Vec<DashLength> => Self { array, phase: Length::zero() },
514 mut dict: Dict => {
515 let array: Vec<DashLength> = dict.take("array")?.cast()?;
516 let phase = dict.take("phase").ok().map(Value::cast)
517 .transpose()?.unwrap_or(Length::zero());
518 dict.finish(&["array", "phase"])?;
519 Self {
520 array,
521 phase,
522 }
523 },
524}
525
526#[derive(Debug, Clone, Eq, PartialEq, Hash)]
528pub enum DashLength<T: Numeric = Length> {
529 LineWidth,
530 Length(T),
531}
532
533impl<T: Numeric> DashLength<T> {
534 fn finish(self, line_width: T) -> T {
535 match self {
536 Self::LineWidth => line_width,
537 Self::Length(l) => l,
538 }
539 }
540}
541
542impl<T: Numeric + Repr> Repr for DashLength<T> {
543 fn repr(&self) -> EcoString {
544 match self {
545 Self::LineWidth => "dot".repr(),
546 Self::Length(v) => v.repr(),
547 }
548 }
549}
550
551impl Resolve for DashLength {
552 type Output = DashLength<Abs>;
553
554 fn resolve(self, styles: StyleChain) -> Self::Output {
555 match self {
556 Self::LineWidth => DashLength::LineWidth,
557 Self::Length(v) => DashLength::Length(v.resolve(styles)),
558 }
559 }
560}
561
562impl From<Abs> for DashLength {
563 fn from(l: Abs) -> Self {
564 DashLength::Length(l.into())
565 }
566}
567
568cast! {
569 DashLength,
570 self => match self {
571 Self::LineWidth => "dot".into_value(),
572 Self::Length(v) => v.into_value(),
573 },
574 "dot" => Self::LineWidth,
575 v: Length => Self::Length(v),
576}
577
578#[derive(Debug, Clone, Eq, PartialEq, Hash)]
580pub struct FixedStroke {
581 pub paint: Paint,
583 pub thickness: Abs,
585 pub cap: LineCap,
587 pub join: LineJoin,
589 pub dash: Option<DashPattern<Abs, Abs>>,
591 pub miter_limit: Scalar,
593}
594
595impl FixedStroke {
596 pub fn from_pair(paint: impl Into<Paint>, thickness: Abs) -> Self {
598 Self {
599 paint: paint.into(),
600 thickness,
601 ..Default::default()
602 }
603 }
604}
605
606impl Default for FixedStroke {
607 fn default() -> Self {
608 Self {
609 paint: Paint::Solid(Color::BLACK),
610 thickness: Abs::pt(1.0),
611 cap: LineCap::Butt,
612 join: LineJoin::Miter,
613 dash: None,
614 miter_limit: Scalar::new(4.0),
615 }
616 }
617}