1use core::fmt;
20
21use crate::to_css::ToCss;
22
23use super::{Angle, Color, LengthPercentage, Percentage};
24
25#[derive(Clone, Debug, PartialEq)]
27pub enum Gradient {
28 Linear {
30 direction: LinearDirection,
32 stops: Vec<ColorStop>,
34 },
35 Radial {
37 shape: RadialShape,
39 stops: Vec<ColorStop>,
41 },
42 Conic {
44 from: Option<Angle>,
46 at: Option<(LengthPercentage, LengthPercentage)>,
49 stops: Vec<ColorStop>,
51 },
52}
53
54impl Gradient {
55 pub fn linear_to_bottom(stops: impl IntoIterator<Item = ColorStop>) -> Self {
58 Self::Linear {
59 direction: LinearDirection::ToBottom,
60 stops: stops.into_iter().collect(),
61 }
62 }
63
64 pub fn linear_to_right(stops: impl IntoIterator<Item = ColorStop>) -> Self {
67 Self::Linear {
68 direction: LinearDirection::ToRight,
69 stops: stops.into_iter().collect(),
70 }
71 }
72}
73
74#[derive(Copy, Clone, Debug, PartialEq)]
76pub enum LinearDirection {
77 ToTop,
79 ToRight,
81 ToBottom,
83 ToLeft,
85 ToTopRight,
87 ToTopLeft,
89 ToBottomRight,
91 ToBottomLeft,
93 Angle(Angle),
96}
97
98impl ToCss for LinearDirection {
99 fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
100 match self {
101 LinearDirection::ToTop => dest.write_str("to top"),
102 LinearDirection::ToRight => dest.write_str("to right"),
103 LinearDirection::ToBottom => dest.write_str("to bottom"),
104 LinearDirection::ToLeft => dest.write_str("to left"),
105 LinearDirection::ToTopRight => dest.write_str("to top right"),
106 LinearDirection::ToTopLeft => dest.write_str("to top left"),
107 LinearDirection::ToBottomRight => dest.write_str("to bottom right"),
108 LinearDirection::ToBottomLeft => dest.write_str("to bottom left"),
109 LinearDirection::Angle(a) => a.to_css(dest),
110 }
111 }
112}
113
114#[derive(Clone, Debug, PartialEq)]
116pub enum RadialShape {
117 Circle,
119 Ellipse,
121 CircleSized(LengthPercentage),
123 EllipseSized(LengthPercentage, LengthPercentage),
126}
127
128impl ToCss for RadialShape {
129 fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
130 match self {
131 RadialShape::Circle => dest.write_str("circle"),
132 RadialShape::Ellipse => dest.write_str("ellipse"),
133 RadialShape::CircleSized(r) => {
134 dest.write_str("circle ")?;
135 r.to_css(dest)
136 }
137 RadialShape::EllipseSized(rx, ry) => {
138 dest.write_str("ellipse ")?;
139 rx.to_css(dest)?;
140 dest.write_char(' ')?;
141 ry.to_css(dest)
142 }
143 }
144 }
145}
146
147#[derive(Clone, Debug, PartialEq)]
155pub struct ColorStop {
156 pub color: Color,
158 pub position: Option<StopPosition>,
160}
161
162impl ColorStop {
163 pub fn new(color: Color) -> Self {
165 Self {
166 color,
167 position: None,
168 }
169 }
170
171 pub fn at(color: Color, position: impl Into<StopPosition>) -> Self {
173 Self {
174 color,
175 position: Some(position.into()),
176 }
177 }
178}
179
180impl ToCss for ColorStop {
181 fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
182 self.color.to_css(dest)?;
183 if let Some(p) = &self.position {
184 dest.write_char(' ')?;
185 p.to_css(dest)?;
186 }
187 Ok(())
188 }
189}
190
191#[derive(Clone, Debug, PartialEq)]
193pub enum StopPosition {
194 LengthPercentage(LengthPercentage),
196 Number(f32),
199}
200
201impl From<Percentage> for StopPosition {
202 fn from(p: Percentage) -> Self {
203 Self::LengthPercentage(p.into())
204 }
205}
206
207impl From<LengthPercentage> for StopPosition {
208 fn from(lp: LengthPercentage) -> Self {
209 Self::LengthPercentage(lp)
210 }
211}
212
213impl ToCss for StopPosition {
214 fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
215 match self {
216 StopPosition::LengthPercentage(lp) => lp.to_css(dest),
217 StopPosition::Number(n) => crate::to_css::write_number(dest, *n),
218 }
219 }
220}
221
222fn write_stops(dest: &mut dyn fmt::Write, stops: &[ColorStop]) -> fmt::Result {
223 let mut first = true;
224 for s in stops {
225 if !first {
226 dest.write_str(", ")?;
227 }
228 s.to_css(dest)?;
229 first = false;
230 }
231 Ok(())
232}
233
234impl ToCss for Gradient {
235 fn to_css(&self, dest: &mut dyn fmt::Write) -> fmt::Result {
236 match self {
237 Gradient::Linear { direction, stops } => {
238 dest.write_str("linear-gradient(")?;
239 direction.to_css(dest)?;
240 dest.write_str(", ")?;
241 write_stops(dest, stops)?;
242 dest.write_char(')')
243 }
244 Gradient::Radial { shape, stops } => {
245 dest.write_str("radial-gradient(")?;
246 shape.to_css(dest)?;
247 dest.write_str(", ")?;
248 write_stops(dest, stops)?;
249 dest.write_char(')')
250 }
251 Gradient::Conic { from, at, stops } => {
252 dest.write_str("conic-gradient(")?;
253 let mut wrote_header = false;
254 if let Some(a) = from {
255 dest.write_str("from ")?;
256 a.to_css(dest)?;
257 wrote_header = true;
258 }
259 if let Some((x, y)) = at {
260 if wrote_header {
261 dest.write_char(' ')?;
262 }
263 dest.write_str("at ")?;
264 x.to_css(dest)?;
265 dest.write_char(' ')?;
266 y.to_css(dest)?;
267 wrote_header = true;
268 }
269 if wrote_header {
270 dest.write_str(", ")?;
271 }
272 write_stops(dest, stops)?;
273 dest.write_char(')')
274 }
275 }
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282 use crate::data_type::{Length, NamedColor};
283
284 fn red() -> Color {
285 Color::Named(NamedColor::Red)
286 }
287 fn blue() -> Color {
288 Color::Named(NamedColor::Blue)
289 }
290
291 #[test]
292 fn linear_to_bottom_two_stops() {
293 let g = Gradient::linear_to_bottom([ColorStop::new(red()), ColorStop::new(blue())]);
294 assert_eq!(g.to_css_string(), "linear-gradient(to bottom, red, blue)");
295 }
296
297 #[test]
298 fn linear_with_angle_and_positions() {
299 let g = Gradient::Linear {
300 direction: LinearDirection::Angle(Angle::Deg(45.0)),
301 stops: vec![
302 ColorStop::at(red(), Percentage(0.0)),
303 ColorStop::at(blue(), Percentage(100.0)),
304 ],
305 };
306 assert_eq!(
307 g.to_css_string(),
308 "linear-gradient(45deg, red 0%, blue 100%)"
309 );
310 }
311
312 #[test]
313 fn linear_all_keyword_directions() {
314 let cases = [
315 (LinearDirection::ToTop, "to top"),
316 (LinearDirection::ToRight, "to right"),
317 (LinearDirection::ToBottom, "to bottom"),
318 (LinearDirection::ToLeft, "to left"),
319 (LinearDirection::ToTopRight, "to top right"),
320 (LinearDirection::ToTopLeft, "to top left"),
321 (LinearDirection::ToBottomRight, "to bottom right"),
322 (LinearDirection::ToBottomLeft, "to bottom left"),
323 ];
324 for (d, expected) in cases {
325 let g = Gradient::Linear {
326 direction: d,
327 stops: vec![ColorStop::new(red())],
328 };
329 assert!(g.to_css_string().contains(expected));
330 }
331 }
332
333 #[test]
334 fn radial_circle_default() {
335 let g = Gradient::Radial {
336 shape: RadialShape::Circle,
337 stops: vec![ColorStop::new(red()), ColorStop::new(blue())],
338 };
339 assert_eq!(g.to_css_string(), "radial-gradient(circle, red, blue)");
340 }
341
342 #[test]
343 fn radial_ellipse_sized() {
344 let g = Gradient::Radial {
345 shape: RadialShape::EllipseSized(Length::Px(100.0).into(), Percentage(50.0).into()),
346 stops: vec![ColorStop::new(red())],
347 };
348 assert_eq!(g.to_css_string(), "radial-gradient(ellipse 100px 50%, red)");
349 }
350
351 #[test]
352 fn radial_circle_sized() {
353 let g = Gradient::Radial {
354 shape: RadialShape::CircleSized(Length::Px(50.0).into()),
355 stops: vec![ColorStop::new(red())],
356 };
357 assert_eq!(g.to_css_string(), "radial-gradient(circle 50px, red)");
358 }
359
360 #[test]
361 fn radial_ellipse_keyword() {
362 let g = Gradient::Radial {
363 shape: RadialShape::Ellipse,
364 stops: vec![ColorStop::new(red())],
365 };
366 assert_eq!(g.to_css_string(), "radial-gradient(ellipse, red)");
367 }
368
369 #[test]
370 fn conic_bare() {
371 let g = Gradient::Conic {
372 from: None,
373 at: None,
374 stops: vec![ColorStop::new(red()), ColorStop::new(blue())],
375 };
376 assert_eq!(g.to_css_string(), "conic-gradient(red, blue)");
377 }
378
379 #[test]
380 fn conic_from_and_at() {
381 let g = Gradient::Conic {
382 from: Some(Angle::Deg(90.0)),
383 at: Some((Percentage(50.0).into(), Percentage(50.0).into())),
384 stops: vec![ColorStop::new(red())],
385 };
386 assert_eq!(
387 g.to_css_string(),
388 "conic-gradient(from 90deg at 50% 50%, red)"
389 );
390 }
391
392 #[test]
393 fn conic_at_only() {
394 let g = Gradient::Conic {
395 from: None,
396 at: Some((Percentage(0.0).into(), Percentage(100.0).into())),
397 stops: vec![ColorStop::new(red())],
398 };
399 assert_eq!(g.to_css_string(), "conic-gradient(at 0% 100%, red)");
400 }
401
402 #[test]
403 fn conic_from_only() {
404 let g = Gradient::Conic {
405 from: Some(Angle::Turn(0.25)),
406 at: None,
407 stops: vec![ColorStop::new(red())],
408 };
409 assert_eq!(g.to_css_string(), "conic-gradient(from 0.25turn, red)");
410 }
411
412 #[test]
413 fn stop_with_number_position() {
414 let stop = ColorStop {
415 color: red(),
416 position: Some(StopPosition::Number(0.5)),
417 };
418 assert_eq!(stop.to_css_string(), "red 0.5");
419 }
420
421 #[test]
422 fn stop_position_from_impls() {
423 let p: StopPosition = Percentage(25.0).into();
424 let lp: StopPosition = LengthPercentage::Length(Length::Px(8.0)).into();
425 assert_eq!(p.to_css_string(), "25%");
426 assert_eq!(lp.to_css_string(), "8px");
427 }
428
429 #[test]
430 fn linear_to_right_helper() {
431 let g = Gradient::linear_to_right([ColorStop::new(red()), ColorStop::new(blue())]);
432 assert_eq!(g.to_css_string(), "linear-gradient(to right, red, blue)");
433 }
434}