1use crate::{Color, Point};
4use std::fmt;
5
6#[derive(Debug, Clone, Copy, PartialEq)]
8pub struct ColorStop {
9 pub offset: f64,
11 pub color: Color,
13}
14
15impl ColorStop {
16 #[must_use = "this returns a new value without modifying anything"]
18 pub fn new(offset: f64, color: Color) -> Self {
19 Self {
20 offset: offset.clamp(0.0, 1.0),
21 color,
22 }
23 }
24}
25
26#[derive(Debug, Clone, PartialEq)]
28pub enum Gradient {
29 Linear {
31 start_point: Point,
33 end_point: Point,
35 color_stops: Vec<ColorStop>,
37 },
38 Radial {
40 center: Point,
42 radius: f64,
44 color_stops: Vec<ColorStop>,
46 },
47}
48
49impl Gradient {
50 #[must_use = "this returns a new value without modifying anything"]
52 pub fn linear(start_point: Point, end_point: Point, color_stops: Vec<ColorStop>) -> Self {
53 Self::Linear {
54 start_point,
55 end_point,
56 color_stops,
57 }
58 }
59
60 #[must_use = "this returns a new value without modifying anything"]
62 pub fn radial(center: Point, radius: f64, color_stops: Vec<ColorStop>) -> Self {
63 Self::Radial {
64 center,
65 radius: radius.max(0.0),
66 color_stops,
67 }
68 }
69
70 #[must_use = "this returns a new value without modifying anything"]
74 pub fn linear_from_angle(angle_deg: f64, color_stops: Vec<ColorStop>) -> Self {
75 use std::f64::consts::PI;
76
77 let angle_rad = (angle_deg - 90.0) * PI / 180.0;
81
82 let cos_a = angle_rad.cos();
84 let sin_a = angle_rad.sin();
85
86 let start_x = 0.5 - cos_a * 0.5;
88 let start_y = 0.5 - sin_a * 0.5;
89 let end_x = 0.5 + cos_a * 0.5;
90 let end_y = 0.5 + sin_a * 0.5;
91
92 use crate::Length;
93 Self::Linear {
94 start_point: Point::new(
95 Length::from_pt(start_x * 100.0),
96 Length::from_pt(start_y * 100.0),
97 ),
98 end_point: Point::new(
99 Length::from_pt(end_x * 100.0),
100 Length::from_pt(end_y * 100.0),
101 ),
102 color_stops,
103 }
104 }
105}
106
107impl fmt::Display for ColorStop {
108 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109 write!(f, "{} {}%", self.color, self.offset * 100.0)
110 }
111}
112
113impl fmt::Display for Gradient {
114 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115 match self {
116 Gradient::Linear {
117 start_point,
118 end_point,
119 color_stops,
120 } => {
121 write!(f, "linear-gradient({} to {}", start_point, end_point)?;
122 for stop in color_stops {
123 write!(f, ", {}", stop)?;
124 }
125 write!(f, ")")
126 }
127 Gradient::Radial {
128 center,
129 radius,
130 color_stops,
131 } => {
132 write!(f, "radial-gradient(circle at {}, radius {}", center, radius)?;
133 for stop in color_stops {
134 write!(f, ", {}", stop)?;
135 }
136 write!(f, ")")
137 }
138 }
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use crate::Length;
146
147 #[test]
148 fn test_color_stop() {
149 let stop = ColorStop::new(0.5, Color::RED);
150 assert_eq!(stop.offset, 0.5);
151 assert_eq!(stop.color, Color::RED);
152
153 let stop_clamped = ColorStop::new(1.5, Color::BLUE);
155 assert_eq!(stop_clamped.offset, 1.0);
156 }
157
158 #[test]
159 fn test_linear_gradient() {
160 let stops = vec![
161 ColorStop::new(0.0, Color::RED),
162 ColorStop::new(1.0, Color::BLUE),
163 ];
164
165 let gradient = Gradient::linear(
166 Point::new(Length::ZERO, Length::ZERO),
167 Point::new(Length::from_pt(100.0), Length::from_pt(100.0)),
168 stops,
169 );
170
171 match gradient {
172 Gradient::Linear { color_stops, .. } => {
173 assert_eq!(color_stops.len(), 2);
174 }
175 _ => panic!("Expected linear gradient"),
176 }
177 }
178
179 #[test]
180 fn test_radial_gradient() {
181 let stops = vec![
182 ColorStop::new(0.0, Color::WHITE),
183 ColorStop::new(1.0, Color::BLACK),
184 ];
185
186 let gradient = Gradient::radial(
187 Point::new(Length::from_pt(50.0), Length::from_pt(50.0)),
188 0.5,
189 stops,
190 );
191
192 match gradient {
193 Gradient::Radial {
194 radius,
195 color_stops,
196 ..
197 } => {
198 assert_eq!(radius, 0.5);
199 assert_eq!(color_stops.len(), 2);
200 }
201 _ => panic!("Expected radial gradient"),
202 }
203 }
204
205 #[test]
206 fn test_linear_from_angle() {
207 let stops = vec![
208 ColorStop::new(0.0, Color::RED),
209 ColorStop::new(1.0, Color::BLUE),
210 ];
211
212 let gradient = Gradient::linear_from_angle(0.0, stops.clone());
214 assert!(matches!(gradient, Gradient::Linear { .. }));
215
216 let gradient = Gradient::linear_from_angle(90.0, stops);
218 assert!(matches!(gradient, Gradient::Linear { .. }));
219 }
220
221 #[test]
222 fn test_color_stop_display() {
223 let stop = ColorStop::new(0.5, Color::RED);
224 assert_eq!(format!("{}", stop), "#FF0000 50%");
225
226 let stop_zero = ColorStop::new(0.0, Color::BLACK);
227 assert_eq!(format!("{}", stop_zero), "#000000 0%");
228
229 let stop_full = ColorStop::new(1.0, Color::WHITE);
230 assert_eq!(format!("{}", stop_full), "#FFFFFF 100%");
231 }
232
233 #[test]
234 fn test_linear_gradient_display() {
235 let stops = vec![
236 ColorStop::new(0.0, Color::RED),
237 ColorStop::new(1.0, Color::BLUE),
238 ];
239
240 let gradient = Gradient::linear(
241 Point::new(Length::ZERO, Length::ZERO),
242 Point::new(Length::from_pt(100.0), Length::from_pt(100.0)),
243 stops,
244 );
245
246 let display = format!("{}", gradient);
247 assert!(display.starts_with("linear-gradient("));
248 assert!(display.contains("#FF0000 0%"));
249 assert!(display.contains("#0000FF 100%"));
250 }
251
252 #[test]
253 fn test_radial_gradient_display() {
254 let stops = vec![
255 ColorStop::new(0.0, Color::WHITE),
256 ColorStop::new(1.0, Color::BLACK),
257 ];
258
259 let gradient = Gradient::radial(
260 Point::new(Length::from_pt(50.0), Length::from_pt(50.0)),
261 0.5,
262 stops,
263 );
264
265 let display = format!("{}", gradient);
266 assert!(display.starts_with("radial-gradient("));
267 assert!(display.contains("#FFFFFF 0%"));
268 assert!(display.contains("#000000 100%"));
269 }
270}
271
272#[cfg(test)]
273mod gradient_extra_tests {
274 use super::*;
275 use crate::Length;
276
277 #[test]
280 fn test_color_stop_at_zero() {
281 let s = ColorStop::new(0.0, Color::BLACK);
282 assert_eq!(s.offset, 0.0);
283 }
284
285 #[test]
286 fn test_color_stop_at_one() {
287 let s = ColorStop::new(1.0, Color::WHITE);
288 assert_eq!(s.offset, 1.0);
289 }
290
291 #[test]
292 fn test_color_stop_clamp_above_one() {
293 let s = ColorStop::new(1.5, Color::RED);
294 assert_eq!(s.offset, 1.0);
295 }
296
297 #[test]
298 fn test_color_stop_clamp_below_zero() {
299 let s = ColorStop::new(-0.5, Color::BLUE);
300 assert_eq!(s.offset, 0.0);
301 }
302
303 #[test]
304 fn test_color_stop_preserves_color() {
305 let s = ColorStop::new(0.5, Color::GREEN);
306 assert_eq!(s.color, Color::GREEN);
307 }
308
309 #[test]
310 fn test_color_stop_display_50_pct() {
311 let s = ColorStop::new(0.5, Color::RED);
312 let display = format!("{}", s);
313 assert!(display.contains("50%"));
314 assert!(display.contains("#FF0000"));
315 }
316
317 #[test]
318 fn test_color_stop_display_0_pct() {
319 let s = ColorStop::new(0.0, Color::BLACK);
320 assert!(format!("{}", s).contains("0%"));
321 }
322
323 #[test]
324 fn test_color_stop_display_100_pct() {
325 let s = ColorStop::new(1.0, Color::WHITE);
326 assert!(format!("{}", s).contains("100%"));
327 }
328
329 #[test]
332 fn test_linear_gradient_has_correct_stops() {
333 let stops = vec![
334 ColorStop::new(0.0, Color::RED),
335 ColorStop::new(0.5, Color::GREEN),
336 ColorStop::new(1.0, Color::BLUE),
337 ];
338 let g = Gradient::linear(
339 Point::new(Length::ZERO, Length::ZERO),
340 Point::new(Length::from_pt(100.0), Length::ZERO),
341 stops,
342 );
343 match g {
344 Gradient::Linear { color_stops, .. } => {
345 assert_eq!(color_stops.len(), 3);
346 assert_eq!(color_stops[1].color, Color::GREEN);
347 }
348 _ => panic!("Expected linear"),
349 }
350 }
351
352 #[test]
353 fn test_linear_gradient_single_stop() {
354 let stops = vec![ColorStop::new(0.0, Color::RED)];
355 let g = Gradient::linear(
356 Point::new(Length::ZERO, Length::ZERO),
357 Point::new(Length::from_pt(10.0), Length::ZERO),
358 stops,
359 );
360 match g {
361 Gradient::Linear { color_stops, .. } => {
362 assert_eq!(color_stops.len(), 1);
363 }
364 _ => panic!("Expected linear"),
365 }
366 }
367
368 #[test]
369 fn test_linear_gradient_empty_stops() {
370 let g = Gradient::linear(
371 Point::new(Length::ZERO, Length::ZERO),
372 Point::new(Length::from_pt(10.0), Length::ZERO),
373 vec![],
374 );
375 match g {
376 Gradient::Linear { color_stops, .. } => {
377 assert!(color_stops.is_empty());
378 }
379 _ => panic!("Expected linear"),
380 }
381 }
382
383 #[test]
386 fn test_radial_gradient_radius_clamped_non_negative() {
387 let g = Gradient::radial(
388 Point::new(Length::from_pt(50.0), Length::from_pt(50.0)),
389 -1.0, vec![],
391 );
392 match g {
393 Gradient::Radial { radius, .. } => {
394 assert!(radius >= 0.0);
395 }
396 _ => panic!("Expected radial"),
397 }
398 }
399
400 #[test]
401 fn test_radial_gradient_center() {
402 let center = Point::new(Length::from_pt(30.0), Length::from_pt(40.0));
403 let g = Gradient::radial(center, 0.7, vec![]);
404 match g {
405 Gradient::Radial { center: c, .. } => {
406 assert_eq!(c, center);
407 }
408 _ => panic!("Expected radial"),
409 }
410 }
411
412 #[test]
413 fn test_radial_gradient_stops_count() {
414 let stops = vec![
415 ColorStop::new(0.0, Color::WHITE),
416 ColorStop::new(0.5, Color::rgb(128, 128, 128)),
417 ColorStop::new(1.0, Color::BLACK),
418 ];
419 let g = Gradient::radial(
420 Point::new(Length::from_pt(50.0), Length::from_pt(50.0)),
421 0.5,
422 stops,
423 );
424 match g {
425 Gradient::Radial { color_stops, .. } => {
426 assert_eq!(color_stops.len(), 3);
427 }
428 _ => panic!("Expected radial"),
429 }
430 }
431
432 #[test]
435 fn test_linear_from_angle_0_is_linear() {
436 let g = Gradient::linear_from_angle(
437 0.0,
438 vec![
439 ColorStop::new(0.0, Color::RED),
440 ColorStop::new(1.0, Color::BLUE),
441 ],
442 );
443 assert!(matches!(g, Gradient::Linear { .. }));
444 }
445
446 #[test]
447 fn test_linear_from_angle_90_is_linear() {
448 let g = Gradient::linear_from_angle(
449 90.0,
450 vec![
451 ColorStop::new(0.0, Color::RED),
452 ColorStop::new(1.0, Color::BLUE),
453 ],
454 );
455 assert!(matches!(g, Gradient::Linear { .. }));
456 }
457
458 #[test]
459 fn test_linear_from_angle_180_is_linear() {
460 let g = Gradient::linear_from_angle(180.0, vec![]);
461 assert!(matches!(g, Gradient::Linear { .. }));
462 }
463
464 #[test]
465 fn test_linear_from_angle_360_is_linear() {
466 let g = Gradient::linear_from_angle(360.0, vec![]);
467 assert!(matches!(g, Gradient::Linear { .. }));
468 }
469
470 #[test]
473 fn test_linear_gradient_display_contains_linear() {
474 let g = Gradient::linear(
475 Point::new(Length::ZERO, Length::ZERO),
476 Point::new(Length::from_pt(100.0), Length::ZERO),
477 vec![
478 ColorStop::new(0.0, Color::RED),
479 ColorStop::new(1.0, Color::BLUE),
480 ],
481 );
482 let s = format!("{}", g);
483 assert!(s.starts_with("linear-gradient("));
484 }
485
486 #[test]
487 fn test_radial_gradient_display_contains_radial() {
488 let g = Gradient::radial(
489 Point::new(Length::from_pt(50.0), Length::from_pt(50.0)),
490 0.5,
491 vec![
492 ColorStop::new(0.0, Color::WHITE),
493 ColorStop::new(1.0, Color::BLACK),
494 ],
495 );
496 let s = format!("{}", g);
497 assert!(s.starts_with("radial-gradient("));
498 }
499
500 #[test]
501 fn test_gradient_display_contains_stop_colors() {
502 let g = Gradient::linear(
503 Point::new(Length::ZERO, Length::ZERO),
504 Point::new(Length::from_pt(10.0), Length::ZERO),
505 vec![
506 ColorStop::new(0.0, Color::RED),
507 ColorStop::new(1.0, Color::BLUE),
508 ],
509 );
510 let s = format!("{}", g);
511 assert!(s.contains("#FF0000"));
512 assert!(s.contains("#0000FF"));
513 }
514}