Skip to main content

oxml_layout/
paint.rs

1//! Backend-neutral fill and stroke paint.
2
3use crate::{Color, MediaId, Point, Rect, Transform};
4
5/// One color stop in a gradient.
6#[derive(Debug, Clone, Copy, PartialEq)]
7pub struct GradientStop {
8    pub offset: f64,
9    pub color: Color,
10}
11
12/// Fill or stroke paint independent of a rendering backend.
13#[derive(Debug, Clone, PartialEq)]
14pub enum Paint {
15    Solid(Color),
16    Linear {
17        start: Point,
18        end: Point,
19        stops: Vec<GradientStop>,
20        extend: (bool, bool),
21    },
22    Radial {
23        center: Point,
24        radius: f64,
25        focal: Point,
26        stops: Vec<GradientStop>,
27        extend: (bool, bool),
28    },
29    Tile {
30        image: MediaId,
31        tile: Rect,
32        transform: Transform,
33    },
34}
35
36impl Paint {
37    /// Construct a linear gradient, degrading one stop to a solid paint.
38    pub fn linear(
39        start: Point,
40        end: Point,
41        stops: Vec<GradientStop>,
42        extend: (bool, bool),
43    ) -> Self {
44        if let [stop] = stops.as_slice() {
45            Self::Solid(stop.color)
46        } else {
47            Self::Linear {
48                start,
49                end,
50                stops,
51                extend,
52            }
53        }
54    }
55
56    /// Construct a radial gradient, degrading one stop to a solid paint.
57    pub fn radial(
58        center: Point,
59        radius: f64,
60        focal: Point,
61        stops: Vec<GradientStop>,
62        extend: (bool, bool),
63    ) -> Self {
64        if let [stop] = stops.as_slice() {
65            Self::Solid(stop.color)
66        } else {
67            Self::Radial {
68                center,
69                radius,
70                focal,
71                stops,
72                extend,
73            }
74        }
75    }
76}
77
78/// Stroke endpoint style.
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
80pub enum LineCap {
81    #[default]
82    Butt,
83    Round,
84    Square,
85}
86
87/// Stroke segment join style.
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
89pub enum LineJoin {
90    #[default]
91    Miter,
92    Round,
93    Bevel,
94}
95
96/// Paint and geometry used to stroke a path.
97#[derive(Debug, Clone, PartialEq)]
98pub struct Stroke {
99    pub paint: Paint,
100    pub width: f64,
101    pub cap: LineCap,
102    pub join: LineJoin,
103    pub dash: Option<Vec<f64>>,
104}
105
106impl Stroke {
107    /// Construct a stroke with butt caps, miter joins, and no dash.
108    pub fn new(paint: Paint, width: f64) -> Self {
109        Self {
110            paint,
111            width,
112            cap: LineCap::Butt,
113            join: LineJoin::Miter,
114            dash: None,
115        }
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::{GradientStop, LineCap, LineJoin, Paint, Stroke};
122    use crate::{Color, MediaId, Point, Rect, Transform};
123
124    fn red() -> Color {
125        Color {
126            r: 1.0,
127            g: 0.0,
128            b: 0.0,
129            a: 1.0,
130        }
131    }
132
133    #[test]
134    fn single_stop_gradients_degrade_to_solid_at_construction() {
135        let stops = vec![GradientStop {
136            offset: 0.25,
137            color: red(),
138        }];
139        assert_eq!(
140            Paint::linear(
141                Point { x: 0.0, y: 0.0 },
142                Point { x: 1.0, y: 1.0 },
143                stops.clone(),
144                (false, false),
145            ),
146            Paint::Solid(red())
147        );
148        assert_eq!(
149            Paint::radial(
150                Point { x: 0.5, y: 0.5 },
151                1.0,
152                Point { x: 0.5, y: 0.5 },
153                stops,
154                (false, false),
155            ),
156            Paint::Solid(red())
157        );
158    }
159
160    #[test]
161    fn multiple_stop_gradients_preserve_their_geometry_and_stops() {
162        let stops = vec![
163            GradientStop {
164                offset: 0.0,
165                color: Color::BLACK,
166            },
167            GradientStop {
168                offset: 1.0,
169                color: Color::WHITE,
170            },
171        ];
172        let paint = Paint::linear(
173            Point { x: 0.0, y: 0.0 },
174            Point { x: 1.0, y: 1.0 },
175            stops.clone(),
176            (true, false),
177        );
178        assert!(matches!(
179            paint,
180            Paint::Linear {
181                stops: actual,
182                extend: (true, false),
183                ..
184            } if actual == stops
185        ));
186    }
187
188    #[test]
189    fn stroke_new_uses_pdf_defaults() {
190        let stroke = Stroke::new(Paint::Solid(red()), 2.0);
191        assert_eq!(stroke.cap, LineCap::Butt);
192        assert_eq!(stroke.join, LineJoin::Miter);
193        assert_eq!(stroke.dash, None);
194    }
195
196    #[test]
197    fn tile_paint_uses_a_media_id() {
198        let media_id = MediaId::from_bytes(b"tile");
199        let paint = Paint::Tile {
200            image: media_id,
201            tile: Rect {
202                x: 0.0,
203                y: 0.0,
204                width: 10.0,
205                height: 20.0,
206            },
207            transform: Transform::IDENTITY,
208        };
209        assert!(matches!(paint, Paint::Tile { image, .. } if image == media_id));
210    }
211}