canvas_traits/
canvas.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::default::Default;
6use std::str::FromStr;
7
8use base::Epoch;
9use base::generic_channel::GenericSender;
10use euclid::Angle;
11use euclid::approxeq::ApproxEq;
12use euclid::default::{Point2D, Rect, Size2D, Transform2D};
13use fonts_traits::{FontDataAndIndex, FontIdentifier};
14use kurbo::{BezPath, ParamCurveNearest as _, PathEl, Point, Shape, Triangle};
15use malloc_size_of::MallocSizeOf;
16use malloc_size_of_derive::MallocSizeOf;
17use pixels::SharedSnapshot;
18use serde::{Deserialize, Serialize};
19use strum::{Display, EnumString};
20use style::color::AbsoluteColor;
21use webrender_api::ImageKey;
22
23#[derive(Clone, Debug, Default, Deserialize, Serialize)]
24pub struct Path(pub BezPath);
25
26impl MallocSizeOf for Path {
27    fn size_of(&self, _ops: &mut malloc_size_of::MallocSizeOfOps) -> usize {
28        std::mem::size_of_val(self.0.elements())
29    }
30}
31
32pub struct IndexSizeError;
33
34impl Path {
35    pub fn new() -> Self {
36        Self(BezPath::new())
37    }
38
39    pub fn from_svg(s: &str) -> Self {
40        Self(BezPath::from_svg(s).unwrap_or_default())
41    }
42
43    pub fn transform(&mut self, transform: Transform2D<f64>) {
44        self.0.apply_affine(transform.into());
45    }
46
47    /// <https://html.spec.whatwg.org/multipage/#ensure-there-is-a-subpath>
48    pub fn ensure_there_is_a_subpath(&mut self, x: f64, y: f64) {
49        // The user agent must check to see if the path has its need new subpath flag set.
50        if self.0.elements().is_empty() {
51            // If it does, then the user agent must create a new subpath with the point (x, y)
52            // as its first (and only) point,
53            // as if the moveTo() method had been called,
54            // and must then unset the path's need new subpath flag.
55            self.0.move_to((x, y));
56        }
57    }
58
59    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-closepath>
60    pub fn close_path(&mut self) {
61        // must do nothing if the object's path has no subpaths
62        if matches!(self.0.elements().last(), None | Some(PathEl::ClosePath)) {
63            return;
64        }
65        // Otherwise, it must mark the last subpath as closed,
66        // create a new subpath whose first point is the same as the previous subpath's first point,
67        // and finally add this new subpath to the path.
68        self.0.close_path();
69    }
70
71    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-moveto>
72    pub fn move_to(&mut self, x: f64, y: f64) {
73        // Step 1. If either of the arguments are infinite or NaN, then return.
74        if !(x.is_finite() && y.is_finite()) {
75            return;
76        }
77
78        // Step 2. Create a new subpath with the specified point as its first (and only) point.
79        self.0.move_to((x, y));
80    }
81
82    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-lineto>
83    pub fn line_to(&mut self, x: f64, y: f64) {
84        // Step 1. If either of the arguments are infinite or NaN, then return.
85        if !(x.is_finite() && y.is_finite()) {
86            return;
87        }
88
89        // Step 2. If the object's path has no subpaths, then ensure there is a subpath for (x, y).
90        self.ensure_there_is_a_subpath(x, y);
91
92        // Step 3. Otherwise, connect the last point in the subpath to the given point (x, y) using a straight line,
93        // and then add the given point (x, y) to the subpath.
94        self.0.line_to((x, y));
95    }
96
97    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-quadraticcurveto>
98    pub fn quadratic_curve_to(&mut self, cpx: f64, cpy: f64, x: f64, y: f64) {
99        // Step 1. If any of the arguments are infinite or NaN, then return.
100        if !(cpx.is_finite() && cpy.is_finite() && x.is_finite() && y.is_finite()) {
101            return;
102        }
103
104        // Step 2. Ensure there is a subpath for (cpx, cpy).
105        self.ensure_there_is_a_subpath(cpx, cpy);
106
107        // 3. Connect the last point in the subpath to the given point (x, y)
108        // using a quadratic Bézier curve with control point (cpx, cpy). [BEZIER]
109        // 4. Add the given point (x, y) to the subpath.
110        self.0.quad_to((cpx, cpy), (x, y));
111    }
112
113    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-beziercurveto>
114    pub fn bezier_curve_to(&mut self, cp1x: f64, cp1y: f64, cp2x: f64, cp2y: f64, x: f64, y: f64) {
115        // Step 1. If any of the arguments are infinite or NaN, then return.
116        if !(cp1x.is_finite() &&
117            cp1y.is_finite() &&
118            cp2x.is_finite() &&
119            cp2y.is_finite() &&
120            x.is_finite() &&
121            y.is_finite())
122        {
123            return;
124        }
125
126        // Step 2. Ensure there is a subpath for (cp1x, cp1y).
127        self.ensure_there_is_a_subpath(cp1x, cp1y);
128
129        // Step 3. Connect the last point in the subpath to the given point (x, y)
130        // using a cubic Bézier curve with control points (cp1x, cp1y) and (cp2x, cp2y). [BEZIER]
131        // Step 4. Add the point (x, y) to the subpath.
132        self.0.curve_to((cp1x, cp1y), (cp2x, cp2y), (x, y));
133    }
134
135    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-arcto>
136    pub fn arc_to(
137        &mut self,
138        x1: f64,
139        y1: f64,
140        x2: f64,
141        y2: f64,
142        radius: f64,
143    ) -> Result<(), IndexSizeError> {
144        // Step 1. If any of the arguments are infinite or NaN, then return.
145        if !(x1.is_finite() &&
146            y1.is_finite() &&
147            x2.is_finite() &&
148            y2.is_finite() &&
149            radius.is_finite())
150        {
151            return Ok(());
152        }
153
154        // Step 2. Ensure there is a subpath for (x1, y1).
155        self.ensure_there_is_a_subpath(x1, y1);
156
157        // Step 3. If either radius is negative, then throw an "IndexSizeError" DOMException.
158        if radius < 0.0 {
159            return Err(IndexSizeError);
160        }
161
162        // Step 4. Let the point (x0, y0) be the last point in the subpath.
163        let Point { x: x0, y: y0 } = self.last_point().unwrap();
164
165        // Step 5. If the point (x0, y0) is equal to the point (x1, y1),
166        // or if the point (x1, y1) is equal to the point (x2, y2),
167        // or if radius is zero, then add the point (x1, y1) to the subpath,
168        // and connect that point to the previous point (x0, y0) by a straight line.
169        if ((x0, y0) == (x1, y1)) || ((x1, y1) == (x2, y2)) || radius.approx_eq(&0.0) {
170            self.0.line_to((x1, y1));
171            return Ok(());
172        }
173
174        // Step 6. Otherwise, if the points (x0, y0), (x1, y1), and (x2, y2)
175        // all lie on a single straight line, then add the point (x1, y1) to the subpath,
176        // and connect that point to the previous point (x0, y0) by a straight line.
177        let direction = Triangle::from_coords((x0, y0), (x1, y1), (x2, y2)).area();
178        if direction.approx_eq(&0.0) {
179            self.0.line_to((x1, y1));
180            return Ok(());
181        }
182
183        // Step 7. Otherwise, let The Arc be the shortest arc given by circumference of the circle
184        // that has radius radius, and that has one point tangent to the half-infinite line
185        // that crosses the point (x0, y0) and ends at the point (x1, y1),
186        // and that has a different point tangent to the half-infinite line that ends at the point (x1, y1)
187        // and crosses the point (x2, y2).
188        // The points at which this circle touches these two lines are called the start
189        // and end tangent points respectively.
190        // Connect the point (x0, y0) to the start tangent point by a straight line,
191        // adding the start tangent point to the subpath,
192        // and then connect the start tangent point to the end tangent point by The Arc,
193        // adding the end tangent point to the subpath.
194
195        let a2 = (x0 - x1).powi(2) + (y0 - y1).powi(2);
196        let b2 = (x1 - x2).powi(2) + (y1 - y2).powi(2);
197        let d = {
198            let c2 = (x0 - x2).powi(2) + (y0 - y2).powi(2);
199            let cosx = (a2 + b2 - c2) / (2.0 * (a2 * b2).sqrt());
200            let sinx = (1.0 - cosx.powi(2)).sqrt();
201            radius / ((1.0 - cosx) / sinx)
202        };
203
204        // first tangent point
205        let anx = (x1 - x0) / a2.sqrt();
206        let any = (y1 - y0) / a2.sqrt();
207        let tp1 = Point2D::new(x1 - anx * d, y1 - any * d);
208
209        // second tangent point
210        let bnx = (x1 - x2) / b2.sqrt();
211        let bny = (y1 - y2) / b2.sqrt();
212        let tp2 = Point2D::new(x1 - bnx * d, y1 - bny * d);
213
214        // arc center and angles
215        let anticlockwise = direction < 0.0;
216        let cx = tp1.x + any * radius * if anticlockwise { 1.0 } else { -1.0 };
217        let cy = tp1.y - anx * radius * if anticlockwise { 1.0 } else { -1.0 };
218        let angle_start = (tp1.y - cy).atan2(tp1.x - cx);
219        let angle_end = (tp2.y - cy).atan2(tp2.x - cx);
220
221        self.0.line_to((tp1.x, tp1.y));
222
223        self.arc(cx, cy, radius, angle_start, angle_end, anticlockwise)
224    }
225
226    pub fn last_point(&mut self) -> Option<Point> {
227        self.0.current_position()
228    }
229
230    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-arc>
231    pub fn arc(
232        &mut self,
233        x: f64,
234        y: f64,
235        radius: f64,
236        start_angle: f64,
237        end_angle: f64,
238        counterclockwise: bool,
239    ) -> Result<(), IndexSizeError> {
240        // ellipse() with both radii are equal and rotation is 0.
241        self.ellipse(
242            x,
243            y,
244            radius,
245            radius,
246            0.,
247            start_angle,
248            end_angle,
249            counterclockwise,
250        )
251    }
252
253    #[expect(clippy::too_many_arguments)]
254    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-ellipse>
255    pub fn ellipse(
256        &mut self,
257        x: f64,
258        y: f64,
259        radius_x: f64,
260        radius_y: f64,
261        rotation_angle: f64,
262        start_angle: f64,
263        end_angle: f64,
264        counterclockwise: bool,
265    ) -> Result<(), IndexSizeError> {
266        // Step 1. If any of the arguments are infinite or NaN, then return.
267        if !(x.is_finite() &&
268            y.is_finite() &&
269            radius_x.is_finite() &&
270            radius_y.is_finite() &&
271            rotation_angle.is_finite() &&
272            start_angle.is_finite() &&
273            end_angle.is_finite())
274        {
275            return Ok(());
276        }
277
278        // Step 2. If either radiusX or radiusY are negative, then throw an "IndexSizeError" DOMException.
279        if radius_x < 0.0 || radius_y < 0.0 {
280            return Err(IndexSizeError);
281        }
282
283        let mut start = Angle::radians(start_angle);
284        let mut end = Angle::radians(end_angle);
285
286        // Wrap angles mod 2 * PI if necessary
287        if !counterclockwise && start > end + Angle::two_pi() ||
288            counterclockwise && end > start + Angle::two_pi()
289        {
290            start = start.positive();
291            end = end.positive();
292        }
293
294        // Calculate the total arc we're going to sweep.
295        let sweep = match counterclockwise {
296            true => {
297                if end - start == Angle::two_pi() {
298                    -Angle::two_pi()
299                } else if end > start {
300                    -(Angle::two_pi() - (end - start))
301                } else {
302                    -(start - end)
303                }
304            },
305            false => {
306                if start - end == Angle::two_pi() {
307                    Angle::two_pi()
308                } else if start > end {
309                    Angle::two_pi() - (start - end)
310                } else {
311                    end - start
312                }
313            },
314        };
315
316        let arc = kurbo::Arc::new(
317            (x, y),
318            (radius_x, radius_y),
319            start.radians,
320            sweep.radians,
321            rotation_angle,
322        );
323
324        let mut iter = arc.path_elements(0.01);
325        let kurbo::PathEl::MoveTo(start_point) = iter.next().unwrap() else {
326            unreachable!()
327        };
328
329        self.line_to(start_point.x, start_point.y);
330
331        if sweep.radians.abs() > 1e-3 {
332            self.0.extend(iter);
333        }
334
335        Ok(())
336    }
337
338    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-rect>
339    pub fn rect(&mut self, x: f64, y: f64, w: f64, h: f64) {
340        // Step 1. If any of the arguments are infinite or NaN, then return.
341        if !(x.is_finite() && y.is_finite() && w.is_finite() && h.is_finite()) {
342            return;
343        }
344
345        // Step 2. Create a new subpath containing just the four points
346        // (x, y), (x+w, y), (x+w, y+h), (x, y+h), in that order,
347        // with those four points connected by straight lines.
348        self.0.move_to((x, y));
349        self.0.line_to((x + w, y));
350        self.0.line_to((x + w, y + h));
351        self.0.line_to((x, y + h));
352
353        // Step 3. Mark the subpath as closed.
354        self.0.close_path();
355
356        // Step 4. Create a new subpath with the point (x, y) as the only point in the subpath.
357        self.0.move_to((x, y));
358    }
359
360    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-ispointinpath>
361    pub fn is_point_in_path(&self, x: f64, y: f64, fill_rule: FillRule) -> bool {
362        let p = Point::new(x, y);
363        // Step 1. If x or y are infinite or NaN, then return false.
364        if !p.is_finite() {
365            return false;
366        }
367
368        // Step 2. If the point given by the x and y coordinates,
369        // when treated as coordinates in the canvas coordinate space unaffected by the current transformation,
370        // is inside the intended path for path as determined by the fill rule indicated by fillRule,
371        // then return true.
372        // Open subpaths must be implicitly closed when computing the area inside the path,
373        // without affecting the actual subpaths.
374        let mut path = self.clone();
375        path.close_path();
376        let winding = path.0.winding(p);
377        let is_inside = match fill_rule {
378            FillRule::Nonzero => winding != 0,
379            FillRule::Evenodd => (winding % 2) != 0,
380        };
381        if is_inside {
382            return true;
383        }
384        // Points on the path itself must be considered to be inside the path.
385        path.0
386            .segments()
387            .any(|seg| seg.nearest(p, 0.00001).distance_sq < 0.00001)
388    }
389
390    pub fn bounding_box(&self) -> Rect<f64> {
391        self.0.control_box().into()
392    }
393}
394
395#[derive(Clone, Debug, Deserialize, Serialize)]
396pub enum FillRule {
397    Nonzero,
398    Evenodd,
399}
400
401#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize)]
402pub struct CanvasId(pub u64);
403
404#[derive(Clone, Copy, Debug, Deserialize, MallocSizeOf, Serialize)]
405pub struct CompositionOptions {
406    pub alpha: f64,
407    pub composition_operation: CompositionOrBlending,
408}
409
410#[derive(Debug, Deserialize, MallocSizeOf, Serialize)]
411pub struct ShadowOptions {
412    pub offset_x: f64,
413    pub offset_y: f64,
414    pub blur: f64,
415    pub color: AbsoluteColor,
416}
417
418impl ShadowOptions {
419    /// <https://html.spec.whatwg.org/multipage/#when-shadows-are-drawn>
420    pub fn need_to_draw_shadow(&self) -> bool {
421        // Shadows are only drawn if the opacity component of the alpha component of the shadow color is nonzero
422        self.color.alpha != 0.0 &&
423        // and either the shadowBlur is nonzero, or the shadowOffsetX is nonzero, or the shadowOffsetY is nonzero.
424            (self.offset_x != 0.0 ||
425                self.offset_y != 0.0 ||
426                self.blur != 0.0)
427    }
428}
429
430#[derive(Debug, Deserialize, MallocSizeOf, Serialize)]
431pub struct LineOptions {
432    pub width: f64,
433    pub cap_style: LineCapStyle,
434    pub join_style: LineJoinStyle,
435    pub miter_limit: f64,
436    pub dash: Vec<f32>,
437    pub dash_offset: f64,
438}
439
440#[expect(clippy::large_enum_variant)]
441#[derive(Debug, Deserialize, Serialize)]
442pub enum CanvasMsg {
443    Canvas2d(Canvas2dMsg, CanvasId),
444    Recreate(Option<Size2D<u64>>, CanvasId),
445    Close(CanvasId),
446}
447
448#[derive(Debug, Deserialize, Serialize, Display)]
449pub enum Canvas2dMsg {
450    SetImageKey(ImageKey),
451    DrawImage(
452        SharedSnapshot,
453        Rect<f64>,
454        Rect<f64>,
455        bool,
456        ShadowOptions,
457        CompositionOptions,
458        Transform2D<f64>,
459    ),
460    DrawEmptyImage(
461        Size2D<u32>,
462        Rect<f64>,
463        Rect<f64>,
464        ShadowOptions,
465        CompositionOptions,
466        Transform2D<f64>,
467    ),
468    DrawImageInOther(
469        CanvasId,
470        Rect<f64>,
471        Rect<f64>,
472        bool,
473        ShadowOptions,
474        CompositionOptions,
475        Transform2D<f64>,
476    ),
477    ClearRect(Rect<f32>, Transform2D<f64>),
478    ClipPath(Path, FillRule, Transform2D<f64>),
479    PopClips(usize),
480    FillPath(
481        FillOrStrokeStyle,
482        Path,
483        FillRule,
484        ShadowOptions,
485        CompositionOptions,
486        Transform2D<f64>,
487    ),
488    FillText(
489        Rect<f64>,
490        Vec<TextRun>,
491        FillOrStrokeStyle,
492        ShadowOptions,
493        CompositionOptions,
494        Transform2D<f64>,
495    ),
496    StrokeText(
497        Rect<f64>,
498        Vec<TextRun>,
499        FillOrStrokeStyle,
500        LineOptions,
501        ShadowOptions,
502        CompositionOptions,
503        Transform2D<f64>,
504    ),
505    FillRect(
506        Rect<f32>,
507        FillOrStrokeStyle,
508        ShadowOptions,
509        CompositionOptions,
510        Transform2D<f64>,
511    ),
512    GetImageData(Option<Rect<u32>>, GenericSender<SharedSnapshot>),
513    PutImageData(Rect<u32>, SharedSnapshot),
514    StrokeRect(
515        Rect<f32>,
516        FillOrStrokeStyle,
517        LineOptions,
518        ShadowOptions,
519        CompositionOptions,
520        Transform2D<f64>,
521    ),
522    StrokePath(
523        Path,
524        FillOrStrokeStyle,
525        LineOptions,
526        ShadowOptions,
527        CompositionOptions,
528        Transform2D<f64>,
529    ),
530    UpdateImage(Option<Epoch>),
531}
532
533#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)]
534pub struct CanvasGradientStop {
535    pub offset: f64,
536    pub color: AbsoluteColor,
537}
538
539#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)]
540pub struct LinearGradientStyle {
541    pub x0: f64,
542    pub y0: f64,
543    pub x1: f64,
544    pub y1: f64,
545    pub stops: Vec<CanvasGradientStop>,
546}
547
548impl LinearGradientStyle {
549    pub fn new(
550        x0: f64,
551        y0: f64,
552        x1: f64,
553        y1: f64,
554        stops: Vec<CanvasGradientStop>,
555    ) -> LinearGradientStyle {
556        LinearGradientStyle {
557            x0,
558            y0,
559            x1,
560            y1,
561            stops,
562        }
563    }
564}
565
566#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)]
567pub struct RadialGradientStyle {
568    pub x0: f64,
569    pub y0: f64,
570    pub r0: f64,
571    pub x1: f64,
572    pub y1: f64,
573    pub r1: f64,
574    pub stops: Vec<CanvasGradientStop>,
575}
576
577impl RadialGradientStyle {
578    pub fn new(
579        x0: f64,
580        y0: f64,
581        r0: f64,
582        x1: f64,
583        y1: f64,
584        r1: f64,
585        stops: Vec<CanvasGradientStop>,
586    ) -> RadialGradientStyle {
587        RadialGradientStyle {
588            x0,
589            y0,
590            r0,
591            x1,
592            y1,
593            r1,
594            stops,
595        }
596    }
597}
598
599#[derive(Clone, Debug, Deserialize, Serialize)]
600pub struct SurfaceStyle {
601    pub surface_data: SharedSnapshot,
602    pub surface_size: Size2D<u32>,
603    pub repeat_x: bool,
604    pub repeat_y: bool,
605    pub transform: Transform2D<f32>,
606}
607
608impl SurfaceStyle {
609    pub fn new(
610        surface_data: SharedSnapshot,
611        surface_size: Size2D<u32>,
612        repeat_x: bool,
613        repeat_y: bool,
614        transform: Transform2D<f32>,
615    ) -> Self {
616        Self {
617            surface_data,
618            surface_size,
619            repeat_x,
620            repeat_y,
621            transform,
622        }
623    }
624}
625
626#[derive(Clone, Debug, Deserialize, Serialize)]
627pub enum FillOrStrokeStyle {
628    Color(AbsoluteColor),
629    LinearGradient(LinearGradientStyle),
630    RadialGradient(RadialGradientStyle),
631    Surface(SurfaceStyle),
632}
633
634impl FillOrStrokeStyle {
635    pub fn is_zero_size_gradient(&self) -> bool {
636        match self {
637            Self::RadialGradient(pattern) => {
638                let centers_equal = (pattern.x0, pattern.y0) == (pattern.x1, pattern.y1);
639                let radii_equal = pattern.r0 == pattern.r1;
640                (centers_equal && radii_equal) || pattern.stops.is_empty()
641            },
642            Self::LinearGradient(pattern) => {
643                (pattern.x0, pattern.y0) == (pattern.x1, pattern.y1) || pattern.stops.is_empty()
644            },
645            Self::Color(..) | Self::Surface(..) => false,
646        }
647    }
648
649    pub fn x_bound(&self) -> Option<u32> {
650        match self {
651            Self::Surface(pattern) => {
652                if pattern.repeat_x {
653                    None
654                } else {
655                    Some(pattern.surface_size.width)
656                }
657            },
658            Self::Color(..) | Self::LinearGradient(..) | Self::RadialGradient(..) => None,
659        }
660    }
661
662    pub fn y_bound(&self) -> Option<u32> {
663        match self {
664            Self::Surface(pattern) => {
665                if pattern.repeat_y {
666                    None
667                } else {
668                    Some(pattern.surface_size.height)
669                }
670            },
671            Self::Color(..) | Self::LinearGradient(..) | Self::RadialGradient(..) => None,
672        }
673    }
674}
675
676#[derive(
677    Clone, Copy, Debug, Display, Deserialize, EnumString, MallocSizeOf, PartialEq, Serialize,
678)]
679pub enum LineCapStyle {
680    Butt = 0,
681    Round = 1,
682    Square = 2,
683}
684
685#[derive(
686    Clone, Copy, Debug, Deserialize, Display, EnumString, MallocSizeOf, PartialEq, Serialize,
687)]
688pub enum LineJoinStyle {
689    Round = 0,
690    Bevel = 1,
691    Miter = 2,
692}
693
694#[derive(Clone, Copy, Debug, Deserialize, Display, EnumString, PartialEq, Serialize)]
695#[strum(serialize_all = "kebab-case")]
696pub enum RepetitionStyle {
697    Repeat,
698    RepeatX,
699    RepeatY,
700    NoRepeat,
701}
702
703/// <https://drafts.fxtf.org/compositing/#compositemode>
704#[derive(
705    Clone, Copy, Debug, Deserialize, Display, EnumString, MallocSizeOf, PartialEq, Serialize,
706)]
707#[strum(serialize_all = "kebab-case")]
708pub enum CompositionStyle {
709    Clear,
710    Copy,
711    SourceOver,
712    DestinationOver,
713    SourceIn,
714    DestinationIn,
715    SourceOut,
716    DestinationOut,
717    SourceAtop,
718    DestinationAtop,
719    Xor,
720    Lighter,
721    // PlusDarker,
722    // PlusLighter,
723}
724
725/// <https://drafts.fxtf.org/compositing/#ltblendmodegt>
726#[derive(
727    Clone, Copy, Debug, Deserialize, Display, EnumString, MallocSizeOf, PartialEq, Serialize,
728)]
729#[strum(serialize_all = "kebab-case")]
730pub enum BlendingStyle {
731    // Normal,
732    Multiply,
733    Screen,
734    Overlay,
735    Darken,
736    Lighten,
737    ColorDodge,
738    ColorBurn,
739    HardLight,
740    SoftLight,
741    Difference,
742    Exclusion,
743    Hue,
744    Saturation,
745    Color,
746    Luminosity,
747}
748
749#[derive(Clone, Copy, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize)]
750pub enum CompositionOrBlending {
751    Composition(CompositionStyle),
752    Blending(BlendingStyle),
753}
754
755impl Default for CompositionOrBlending {
756    fn default() -> CompositionOrBlending {
757        CompositionOrBlending::Composition(CompositionStyle::SourceOver)
758    }
759}
760
761impl FromStr for CompositionOrBlending {
762    type Err = ();
763
764    fn from_str(string: &str) -> Result<CompositionOrBlending, ()> {
765        if let Ok(op) = CompositionStyle::from_str(string) {
766            return Ok(CompositionOrBlending::Composition(op));
767        }
768
769        if let Ok(op) = BlendingStyle::from_str(string) {
770            return Ok(CompositionOrBlending::Blending(op));
771        }
772
773        Err(())
774    }
775}
776
777#[derive(Debug, Deserialize, Serialize)]
778pub struct GlyphAndPosition {
779    pub id: u32,
780    pub point: Point2D<f32>,
781}
782
783#[derive(Deserialize, Serialize)]
784pub struct CanvasFont {
785    /// A [`FontIdentifier`] for this [`CanvasFont`], maybe either `Local` or `Web`.
786    pub identifier: FontIdentifier,
787    /// If this font is a web font, this field contains the data for the font. If
788    /// the font is a local font, it will be `None`.
789    pub data: Option<FontDataAndIndex>,
790}
791
792impl CanvasFont {
793    pub fn font_data_and_index(&self) -> Option<FontDataAndIndex> {
794        self.data.clone().or_else(|| match &self.identifier {
795            FontIdentifier::Local(local_font_identifier) => {
796                local_font_identifier.font_data_and_index()
797            },
798            FontIdentifier::Web(_) => None,
799        })
800    }
801}
802
803#[derive(Deserialize, Serialize)]
804pub struct TextRun {
805    pub font: CanvasFont,
806    pub pt_size: f32,
807    pub glyphs_and_positions: Vec<GlyphAndPosition>,
808    pub advance: f32,
809    pub bounds: Rect<f64>,
810}
811
812impl std::fmt::Debug for TextRun {
813    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
814        f.debug_struct("TextRun")
815            .field("glyphs_and_positions", &self.glyphs_and_positions)
816            .field("size", &self.bounds)
817            .finish()
818    }
819}