Skip to main content

servo_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 euclid::Angle;
9use euclid::approxeq::ApproxEq;
10use euclid::default::{Point2D, Rect, Size2D, Transform2D};
11use fonts_traits::{FontDataAndIndex, FontIdentifier};
12use kurbo::{BezPath, ParamCurveNearest as _, PathEl, Point, Shape, Triangle};
13use malloc_size_of::MallocSizeOf;
14use malloc_size_of_derive::MallocSizeOf;
15use pixels::SharedSnapshot;
16use serde::{Deserialize, Serialize};
17use servo_base::Epoch;
18use servo_base::generic_channel::GenericSender;
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
440pub type CanvasMsg = (CanvasId, CanvasCommand);
441
442#[derive(Debug, Deserialize, Serialize, Display)]
443pub enum CanvasCommand {
444    /// This is used for resizing (when size is provided) or just clearing the canvas (when size is `None`).
445    Recreate(Option<Size2D<u64>>),
446    /// Destroy the canvas (its id will be invalidated).
447    Destroy,
448    SetImageKey(ImageKey),
449    DrawImage(
450        SharedSnapshot,
451        Rect<f64>,
452        Rect<f64>,
453        bool,
454        ShadowOptions,
455        CompositionOptions,
456        Transform2D<f64>,
457    ),
458    DrawEmptyImage(
459        Size2D<u32>,
460        Rect<f64>,
461        Rect<f64>,
462        ShadowOptions,
463        CompositionOptions,
464        Transform2D<f64>,
465    ),
466    DrawImageInOther(
467        CanvasId,
468        Rect<f64>,
469        Rect<f64>,
470        bool,
471        ShadowOptions,
472        CompositionOptions,
473        Transform2D<f64>,
474    ),
475    ClearRect(Rect<f32>, Transform2D<f64>),
476    ClipPath(Path, FillRule, Transform2D<f64>),
477    PopClips(usize),
478    FillPath(
479        FillOrStrokeStyle,
480        Path,
481        FillRule,
482        ShadowOptions,
483        CompositionOptions,
484        Transform2D<f64>,
485    ),
486    FillText(
487        Rect<f64>,
488        Vec<TextRun>,
489        FillOrStrokeStyle,
490        ShadowOptions,
491        CompositionOptions,
492        Transform2D<f64>,
493    ),
494    StrokeText(
495        Rect<f64>,
496        Vec<TextRun>,
497        FillOrStrokeStyle,
498        LineOptions,
499        ShadowOptions,
500        CompositionOptions,
501        Transform2D<f64>,
502    ),
503    FillRect(
504        Rect<f32>,
505        FillOrStrokeStyle,
506        ShadowOptions,
507        CompositionOptions,
508        Transform2D<f64>,
509    ),
510    GetImageData(Option<Rect<u32>>, GenericSender<SharedSnapshot>),
511    PutImageData(Rect<u32>, SharedSnapshot),
512    StrokeRect(
513        Rect<f32>,
514        FillOrStrokeStyle,
515        LineOptions,
516        ShadowOptions,
517        CompositionOptions,
518        Transform2D<f64>,
519    ),
520    StrokePath(
521        Path,
522        FillOrStrokeStyle,
523        LineOptions,
524        ShadowOptions,
525        CompositionOptions,
526        Transform2D<f64>,
527    ),
528    UpdateImage(Option<Epoch>),
529    ProcessBatchMessages(Vec<CanvasCommand>),
530}
531
532#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)]
533pub struct CanvasGradientStop {
534    pub offset: f64,
535    pub color: AbsoluteColor,
536}
537
538#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)]
539pub struct LinearGradientStyle {
540    pub x0: f64,
541    pub y0: f64,
542    pub x1: f64,
543    pub y1: f64,
544    pub stops: Vec<CanvasGradientStop>,
545}
546
547impl LinearGradientStyle {
548    pub fn new(
549        x0: f64,
550        y0: f64,
551        x1: f64,
552        y1: f64,
553        stops: Vec<CanvasGradientStop>,
554    ) -> LinearGradientStyle {
555        LinearGradientStyle {
556            x0,
557            y0,
558            x1,
559            y1,
560            stops,
561        }
562    }
563}
564
565#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)]
566pub struct RadialGradientStyle {
567    pub x0: f64,
568    pub y0: f64,
569    pub r0: f64,
570    pub x1: f64,
571    pub y1: f64,
572    pub r1: f64,
573    pub stops: Vec<CanvasGradientStop>,
574}
575
576impl RadialGradientStyle {
577    pub fn new(
578        x0: f64,
579        y0: f64,
580        r0: f64,
581        x1: f64,
582        y1: f64,
583        r1: f64,
584        stops: Vec<CanvasGradientStop>,
585    ) -> RadialGradientStyle {
586        RadialGradientStyle {
587            x0,
588            y0,
589            r0,
590            x1,
591            y1,
592            r1,
593            stops,
594        }
595    }
596}
597
598#[derive(Clone, Debug, Deserialize, Serialize)]
599pub struct SurfaceStyle {
600    pub surface_data: SharedSnapshot,
601    pub surface_size: Size2D<u32>,
602    pub repeat_x: bool,
603    pub repeat_y: bool,
604    pub transform: Transform2D<f32>,
605}
606
607impl SurfaceStyle {
608    pub fn new(
609        surface_data: SharedSnapshot,
610        surface_size: Size2D<u32>,
611        repeat_x: bool,
612        repeat_y: bool,
613        transform: Transform2D<f32>,
614    ) -> Self {
615        Self {
616            surface_data,
617            surface_size,
618            repeat_x,
619            repeat_y,
620            transform,
621        }
622    }
623}
624
625#[derive(Clone, Debug, Deserialize, Serialize)]
626pub enum FillOrStrokeStyle {
627    Color(AbsoluteColor),
628    LinearGradient(LinearGradientStyle),
629    RadialGradient(RadialGradientStyle),
630    Surface(SurfaceStyle),
631}
632
633impl FillOrStrokeStyle {
634    pub fn is_zero_size_gradient(&self) -> bool {
635        match self {
636            Self::RadialGradient(pattern) => {
637                let centers_equal = (pattern.x0, pattern.y0) == (pattern.x1, pattern.y1);
638                let radii_equal = pattern.r0 == pattern.r1;
639                (centers_equal && radii_equal) || pattern.stops.is_empty()
640            },
641            Self::LinearGradient(pattern) => {
642                (pattern.x0, pattern.y0) == (pattern.x1, pattern.y1) || pattern.stops.is_empty()
643            },
644            Self::Color(..) | Self::Surface(..) => false,
645        }
646    }
647
648    pub fn x_bound(&self) -> Option<u32> {
649        match self {
650            Self::Surface(pattern) => {
651                if pattern.repeat_x {
652                    None
653                } else {
654                    Some(pattern.surface_size.width)
655                }
656            },
657            Self::Color(..) | Self::LinearGradient(..) | Self::RadialGradient(..) => None,
658        }
659    }
660
661    pub fn y_bound(&self) -> Option<u32> {
662        match self {
663            Self::Surface(pattern) => {
664                if pattern.repeat_y {
665                    None
666                } else {
667                    Some(pattern.surface_size.height)
668                }
669            },
670            Self::Color(..) | Self::LinearGradient(..) | Self::RadialGradient(..) => None,
671        }
672    }
673}
674
675#[derive(
676    Clone, Copy, Debug, Display, Deserialize, EnumString, MallocSizeOf, PartialEq, Serialize,
677)]
678pub enum LineCapStyle {
679    Butt = 0,
680    Round = 1,
681    Square = 2,
682}
683
684#[derive(
685    Clone, Copy, Debug, Deserialize, Display, EnumString, MallocSizeOf, PartialEq, Serialize,
686)]
687pub enum LineJoinStyle {
688    Round = 0,
689    Bevel = 1,
690    Miter = 2,
691}
692
693#[derive(Clone, Copy, Debug, Deserialize, Display, EnumString, PartialEq, Serialize)]
694#[strum(serialize_all = "kebab-case")]
695pub enum RepetitionStyle {
696    Repeat,
697    RepeatX,
698    RepeatY,
699    NoRepeat,
700}
701
702/// <https://drafts.fxtf.org/compositing/#compositemode>
703#[derive(
704    Clone, Copy, Debug, Deserialize, Display, EnumString, MallocSizeOf, PartialEq, Serialize,
705)]
706#[strum(serialize_all = "kebab-case")]
707pub enum CompositionStyle {
708    Clear,
709    Copy,
710    SourceOver,
711    DestinationOver,
712    SourceIn,
713    DestinationIn,
714    SourceOut,
715    DestinationOut,
716    SourceAtop,
717    DestinationAtop,
718    Xor,
719    Lighter,
720    // PlusDarker,
721    // PlusLighter,
722}
723
724/// <https://drafts.fxtf.org/compositing/#ltblendmodegt>
725#[derive(
726    Clone, Copy, Debug, Deserialize, Display, EnumString, MallocSizeOf, PartialEq, Serialize,
727)]
728#[strum(serialize_all = "kebab-case")]
729pub enum BlendingStyle {
730    // Normal,
731    Multiply,
732    Screen,
733    Overlay,
734    Darken,
735    Lighten,
736    ColorDodge,
737    ColorBurn,
738    HardLight,
739    SoftLight,
740    Difference,
741    Exclusion,
742    Hue,
743    Saturation,
744    Color,
745    Luminosity,
746}
747
748#[derive(Clone, Copy, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize)]
749pub enum CompositionOrBlending {
750    Composition(CompositionStyle),
751    Blending(BlendingStyle),
752}
753
754impl Default for CompositionOrBlending {
755    fn default() -> CompositionOrBlending {
756        CompositionOrBlending::Composition(CompositionStyle::SourceOver)
757    }
758}
759
760impl FromStr for CompositionOrBlending {
761    type Err = ();
762
763    fn from_str(string: &str) -> Result<CompositionOrBlending, ()> {
764        if let Ok(op) = CompositionStyle::from_str(string) {
765            return Ok(CompositionOrBlending::Composition(op));
766        }
767
768        if let Ok(op) = BlendingStyle::from_str(string) {
769            return Ok(CompositionOrBlending::Blending(op));
770        }
771
772        Err(())
773    }
774}
775
776#[derive(Debug, Deserialize, Serialize)]
777pub struct GlyphAndPosition {
778    pub id: u32,
779    pub point: Point2D<f32>,
780}
781
782#[derive(Deserialize, Serialize)]
783pub struct CanvasFont {
784    /// A [`FontIdentifier`] for this [`CanvasFont`], maybe either `Local` or `Web`.
785    pub identifier: FontIdentifier,
786    /// If this font is a web font, this field contains the data for the font. If
787    /// the font is a local font, it will be `None`.
788    pub data: Option<FontDataAndIndex>,
789}
790
791impl CanvasFont {
792    pub fn font_data_and_index(&self) -> Option<FontDataAndIndex> {
793        self.data.clone().or_else(|| match &self.identifier {
794            FontIdentifier::Local(local_font_identifier) => {
795                local_font_identifier.font_data_and_index()
796            },
797            FontIdentifier::Web(_) | FontIdentifier::ArrayBuffer(_) => None,
798        })
799    }
800}
801
802#[derive(Deserialize, Serialize)]
803pub struct TextRun {
804    pub font: CanvasFont,
805    pub pt_size: f32,
806    pub glyphs_and_positions: Vec<GlyphAndPosition>,
807    pub advance: f32,
808    pub bounds: Rect<f64>,
809}
810
811impl std::fmt::Debug for TextRun {
812    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
813        f.debug_struct("TextRun")
814            .field("glyphs_and_positions", &self.glyphs_and_positions)
815            .field("size", &self.bounds)
816            .finish()
817    }
818}