1use alloc::{
4 string::{String, ToString},
5 vec::Vec,
6};
7use core::fmt;
8
9#[cfg(feature = "parser")]
10use crate::props::basic::{
11 color::parse_color_or_system,
12 error::{InvalidValueErr, InvalidValueErrOwned},
13 parse::{
14 parse_image, parse_parentheses, split_string_respect_comma, CssImageParseError,
15 CssImageParseErrorOwned, ParenthesisParseError, ParenthesisParseErrorOwned,
16 },
17};
18use crate::{
19 codegen::format::GetHash,
20 corety::AzString,
21 props::{
22 basic::{
23 angle::{
24 parse_angle_value, AngleValue, CssAngleValueParseError,
25 CssAngleValueParseErrorOwned, OptionAngleValue,
26 },
27 color::{
28 ColorOrSystem, ColorU, CssColorParseError, CssColorParseErrorOwned, SystemColorRef,
29 },
30 direction::{
31 parse_direction, CssDirectionParseError, CssDirectionParseErrorOwned, Direction,
32 },
33 length::{
34 parse_percentage_value, OptionPercentageValue, PercentageParseError,
35 PercentageParseErrorOwned, PercentageValue,
36 },
37 pixel::{
38 parse_pixel_value, CssPixelValueParseError, CssPixelValueParseErrorOwned,
39 PixelValue,
40 },
41 },
42 formatter::PrintAsCssValue,
43 },
44};
45
46#[derive(Debug, Copy, Clone, PartialEq, Ord, PartialOrd, Eq, Hash)]
50#[repr(C)]
51#[derive(Default)]
52pub enum ExtendMode {
53 #[default]
54 Clamp,
55 Repeat,
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
62#[repr(C, u8)]
63pub enum StyleBackgroundContent {
64 LinearGradient(LinearGradient),
65 RadialGradient(RadialGradient),
66 ConicGradient(ConicGradient),
67 Image(AzString),
68 Color(ColorU),
69 SystemColor(SystemColorRef),
73}
74
75impl_option!(
76 StyleBackgroundContent,
77 OptionStyleBackgroundContent,
78 copy = false,
79 [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
80);
81
82impl_vec!(
83 StyleBackgroundContent,
84 StyleBackgroundContentVec,
85 StyleBackgroundContentVecDestructor,
86 StyleBackgroundContentVecDestructorType,
87 StyleBackgroundContentVecSlice,
88 OptionStyleBackgroundContent
89);
90impl_vec_debug!(StyleBackgroundContent, StyleBackgroundContentVec);
91impl_vec_partialord!(StyleBackgroundContent, StyleBackgroundContentVec);
92impl_vec_ord!(StyleBackgroundContent, StyleBackgroundContentVec);
93impl_vec_clone!(
94 StyleBackgroundContent,
95 StyleBackgroundContentVec,
96 StyleBackgroundContentVecDestructor
97);
98impl_vec_partialeq!(StyleBackgroundContent, StyleBackgroundContentVec);
99impl_vec_eq!(StyleBackgroundContent, StyleBackgroundContentVec);
100impl_vec_hash!(StyleBackgroundContent, StyleBackgroundContentVec);
101
102impl Default for StyleBackgroundContent {
103 fn default() -> Self {
104 Self::Color(ColorU::TRANSPARENT)
105 }
106}
107
108impl PrintAsCssValue for StyleBackgroundContent {
109 fn print_as_css_value(&self) -> String {
110 match self {
111 Self::LinearGradient(lg) => {
112 let prefix = if lg.extend_mode == ExtendMode::Repeat {
113 "repeating-linear-gradient"
114 } else {
115 "linear-gradient"
116 };
117 format!("{}({})", prefix, lg.print_as_css_value())
118 }
119 Self::RadialGradient(rg) => {
120 let prefix = if rg.extend_mode == ExtendMode::Repeat {
121 "repeating-radial-gradient"
122 } else {
123 "radial-gradient"
124 };
125 format!("{}({})", prefix, rg.print_as_css_value())
126 }
127 Self::ConicGradient(cg) => {
128 let prefix = if cg.extend_mode == ExtendMode::Repeat {
129 "repeating-conic-gradient"
130 } else {
131 "conic-gradient"
132 };
133 format!("{}({})", prefix, cg.print_as_css_value())
134 }
135 Self::Image(id) => format!("url(\"{}\")", id.as_str()),
136 Self::Color(c) => c.to_hash(),
137 Self::SystemColor(s) => s.as_css_str().to_string(),
138 }
139 }
140}
141
142impl crate::codegen::format::FormatAsRustCode for StyleBackgroundContent {
145 fn format_as_rust_code(&self, _tabs: usize) -> String {
146 format!(
148 "StyleBackgroundContent::from_css(\"{}\")",
149 self.print_as_css_value()
150 )
151 }
152}
153
154impl crate::codegen::format::FormatAsRustCode for StyleBackgroundSizeVec {
155 fn format_as_rust_code(&self, _tabs: usize) -> String {
156 format!(
157 "StyleBackgroundSizeVec::from_const_slice(STYLE_BACKGROUND_SIZE_{}_ITEMS)",
158 self.get_hash()
159 )
160 }
161}
162
163impl crate::codegen::format::FormatAsRustCode for StyleBackgroundRepeatVec {
164 fn format_as_rust_code(&self, _tabs: usize) -> String {
165 format!(
166 "StyleBackgroundRepeatVec::from_const_slice(STYLE_BACKGROUND_REPEAT_{}_ITEMS)",
167 self.get_hash()
168 )
169 }
170}
171
172impl crate::codegen::format::FormatAsRustCode for StyleBackgroundContentVec {
173 fn format_as_rust_code(&self, _tabs: usize) -> String {
174 format!(
175 "StyleBackgroundContentVec::from_const_slice(STYLE_BACKGROUND_CONTENT_{}_ITEMS)",
176 self.get_hash()
177 )
178 }
179}
180
181impl PrintAsCssValue for StyleBackgroundContentVec {
182 fn print_as_css_value(&self) -> String {
183 self.as_ref()
184 .iter()
185 .map(PrintAsCssValue::print_as_css_value)
186 .collect::<Vec<_>>()
187 .join(", ")
188 }
189}
190
191#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
195#[repr(C)]
196pub struct LinearGradient {
197 pub direction: Direction,
198 pub extend_mode: ExtendMode,
199 pub stops: NormalizedLinearColorStopVec,
200}
201impl Default for LinearGradient {
202 fn default() -> Self {
203 Self {
204 direction: Direction::default(),
205 extend_mode: ExtendMode::default(),
206 stops: Vec::new().into(),
207 }
208 }
209}
210impl PrintAsCssValue for LinearGradient {
211 fn print_as_css_value(&self) -> String {
212 let dir_str = self.direction.print_as_css_value();
213 let stops_str = self
214 .stops
215 .iter()
216 .map(PrintAsCssValue::print_as_css_value)
217 .collect::<Vec<_>>()
218 .join(", ");
219 if stops_str.is_empty() {
220 dir_str
221 } else {
222 format!("{dir_str}, {stops_str}")
223 }
224 }
225}
226
227#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
229#[repr(C)]
230pub struct RadialGradient {
231 pub shape: Shape,
232 pub size: RadialGradientSize,
233 pub position: StyleBackgroundPosition,
234 pub extend_mode: ExtendMode,
235 pub stops: NormalizedLinearColorStopVec,
236}
237impl Default for RadialGradient {
238 fn default() -> Self {
239 Self {
240 shape: Shape::default(),
241 size: RadialGradientSize::default(),
242 position: StyleBackgroundPosition::default(),
243 extend_mode: ExtendMode::default(),
244 stops: Vec::new().into(),
245 }
246 }
247}
248impl PrintAsCssValue for RadialGradient {
249 fn print_as_css_value(&self) -> String {
250 let stops_str = self
251 .stops
252 .iter()
253 .map(PrintAsCssValue::print_as_css_value)
254 .collect::<Vec<_>>()
255 .join(", ");
256 format!(
257 "{} {} at {}, {}",
258 self.shape,
259 self.size,
260 self.position.print_as_css_value(),
261 stops_str
262 )
263 }
264}
265
266#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
268#[repr(C)]
269pub struct ConicGradient {
270 pub extend_mode: ExtendMode,
271 pub center: StyleBackgroundPosition,
272 pub angle: AngleValue,
273 pub stops: NormalizedRadialColorStopVec,
274}
275impl Default for ConicGradient {
276 fn default() -> Self {
277 Self {
278 extend_mode: ExtendMode::default(),
279 center: StyleBackgroundPosition {
283 horizontal: BackgroundPositionHorizontal::Center,
284 vertical: BackgroundPositionVertical::Center,
285 },
286 angle: AngleValue::default(),
287 stops: Vec::new().into(),
288 }
289 }
290}
291impl PrintAsCssValue for ConicGradient {
292 fn print_as_css_value(&self) -> String {
293 let stops_str = self
294 .stops
295 .iter()
296 .map(PrintAsCssValue::print_as_css_value)
297 .collect::<Vec<_>>()
298 .join(", ");
299 format!(
300 "from {} at {}, {}",
301 self.angle,
302 self.center.print_as_css_value(),
303 stops_str
304 )
305 }
306}
307
308#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
312#[repr(C)]
313#[derive(Default)]
314pub enum Shape {
315 #[default]
316 Ellipse,
317 Circle,
318}
319impl fmt::Display for Shape {
320 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
321 write!(
322 f,
323 "{}",
324 match self {
325 Self::Ellipse => "ellipse",
326 Self::Circle => "circle",
327 }
328 )
329 }
330}
331
332#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
334#[repr(C)]
335#[derive(Default)]
336pub enum RadialGradientSize {
337 ClosestSide,
338 ClosestCorner,
339 FarthestSide,
340 #[default]
341 FarthestCorner,
342}
343impl fmt::Display for RadialGradientSize {
344 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
345 write!(
346 f,
347 "{}",
348 match self {
349 Self::ClosestSide => "closest-side",
350 Self::ClosestCorner => "closest-corner",
351 Self::FarthestSide => "farthest-side",
352 Self::FarthestCorner => "farthest-corner",
353 }
354 )
355 }
356}
357
358#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
359#[repr(C)]
360pub struct NormalizedLinearColorStop {
361 pub offset: PercentageValue,
362 pub color: ColorOrSystem,
364}
365
366impl NormalizedLinearColorStop {
367 #[must_use]
369 pub const fn new(offset: PercentageValue, color: ColorU) -> Self {
370 Self {
371 offset,
372 color: ColorOrSystem::color(color),
373 }
374 }
375
376 #[must_use]
378 pub fn resolve(&self, system_colors: &crate::system::SystemColors, fallback: ColorU) -> ColorU {
379 self.color.resolve(system_colors, fallback)
380 }
381}
382
383impl_option!(
384 NormalizedLinearColorStop,
385 OptionNormalizedLinearColorStop,
386 copy = false,
387 [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
388);
389impl_vec!(
390 NormalizedLinearColorStop,
391 NormalizedLinearColorStopVec,
392 NormalizedLinearColorStopVecDestructor,
393 NormalizedLinearColorStopVecDestructorType,
394 NormalizedLinearColorStopVecSlice,
395 OptionNormalizedLinearColorStop
396);
397impl_vec_debug!(NormalizedLinearColorStop, NormalizedLinearColorStopVec);
398impl_vec_partialord!(NormalizedLinearColorStop, NormalizedLinearColorStopVec);
399impl_vec_ord!(NormalizedLinearColorStop, NormalizedLinearColorStopVec);
400impl_vec_clone!(
401 NormalizedLinearColorStop,
402 NormalizedLinearColorStopVec,
403 NormalizedLinearColorStopVecDestructor
404);
405impl_vec_partialeq!(NormalizedLinearColorStop, NormalizedLinearColorStopVec);
406impl_vec_eq!(NormalizedLinearColorStop, NormalizedLinearColorStopVec);
407impl_vec_hash!(NormalizedLinearColorStop, NormalizedLinearColorStopVec);
408impl PrintAsCssValue for NormalizedLinearColorStop {
409 fn print_as_css_value(&self) -> String {
410 match &self.color {
411 ColorOrSystem::Color(c) => format!("{} {}", c.to_hash(), self.offset),
412 ColorOrSystem::System(s) => format!("{} {}", s.as_css_str(), self.offset),
413 }
414 }
415}
416
417#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
418#[repr(C)]
419pub struct NormalizedRadialColorStop {
420 pub angle: AngleValue,
421 pub color: ColorOrSystem,
423}
424
425impl NormalizedRadialColorStop {
426 #[must_use]
428 pub const fn new(angle: AngleValue, color: ColorU) -> Self {
429 Self {
430 angle,
431 color: ColorOrSystem::color(color),
432 }
433 }
434
435 #[must_use]
437 pub fn resolve(&self, system_colors: &crate::system::SystemColors, fallback: ColorU) -> ColorU {
438 self.color.resolve(system_colors, fallback)
439 }
440}
441
442impl_option!(
443 NormalizedRadialColorStop,
444 OptionNormalizedRadialColorStop,
445 copy = false,
446 [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
447);
448impl_vec!(
449 NormalizedRadialColorStop,
450 NormalizedRadialColorStopVec,
451 NormalizedRadialColorStopVecDestructor,
452 NormalizedRadialColorStopVecDestructorType,
453 NormalizedRadialColorStopVecSlice,
454 OptionNormalizedRadialColorStop
455);
456impl_vec_debug!(NormalizedRadialColorStop, NormalizedRadialColorStopVec);
457impl_vec_partialord!(NormalizedRadialColorStop, NormalizedRadialColorStopVec);
458impl_vec_ord!(NormalizedRadialColorStop, NormalizedRadialColorStopVec);
459impl_vec_clone!(
460 NormalizedRadialColorStop,
461 NormalizedRadialColorStopVec,
462 NormalizedRadialColorStopVecDestructor
463);
464impl_vec_partialeq!(NormalizedRadialColorStop, NormalizedRadialColorStopVec);
465impl_vec_eq!(NormalizedRadialColorStop, NormalizedRadialColorStopVec);
466impl_vec_hash!(NormalizedRadialColorStop, NormalizedRadialColorStopVec);
467impl PrintAsCssValue for NormalizedRadialColorStop {
468 fn print_as_css_value(&self) -> String {
469 match &self.color {
470 ColorOrSystem::Color(c) => format!("{} {}", c.to_hash(), self.angle),
471 ColorOrSystem::System(s) => format!("{} {}", s.as_css_str(), self.angle),
472 }
473 }
474}
475
476#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
485pub struct LinearColorStop {
486 pub color: ColorOrSystem,
487 pub offset1: OptionPercentageValue,
489 pub offset2: OptionPercentageValue,
492}
493
494#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
503pub struct RadialColorStop {
504 pub color: ColorOrSystem,
505 pub offset1: OptionAngleValue,
507 pub offset2: OptionAngleValue,
510}
511
512#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
516#[repr(C)]
517pub struct StyleBackgroundPosition {
518 pub horizontal: BackgroundPositionHorizontal,
519 pub vertical: BackgroundPositionVertical,
520}
521
522impl_option!(
523 StyleBackgroundPosition,
524 OptionStyleBackgroundPosition,
525 [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
526);
527impl_vec!(
528 StyleBackgroundPosition,
529 StyleBackgroundPositionVec,
530 StyleBackgroundPositionVecDestructor,
531 StyleBackgroundPositionVecDestructorType,
532 StyleBackgroundPositionVecSlice,
533 OptionStyleBackgroundPosition
534);
535impl_vec_debug!(StyleBackgroundPosition, StyleBackgroundPositionVec);
536impl_vec_partialord!(StyleBackgroundPosition, StyleBackgroundPositionVec);
537impl_vec_ord!(StyleBackgroundPosition, StyleBackgroundPositionVec);
538impl_vec_clone!(
539 StyleBackgroundPosition,
540 StyleBackgroundPositionVec,
541 StyleBackgroundPositionVecDestructor
542);
543impl_vec_partialeq!(StyleBackgroundPosition, StyleBackgroundPositionVec);
544impl_vec_eq!(StyleBackgroundPosition, StyleBackgroundPositionVec);
545impl_vec_hash!(StyleBackgroundPosition, StyleBackgroundPositionVec);
546impl Default for StyleBackgroundPosition {
547 fn default() -> Self {
548 Self {
549 horizontal: BackgroundPositionHorizontal::Left,
550 vertical: BackgroundPositionVertical::Top,
551 }
552 }
553}
554
555impl StyleBackgroundPosition {
556 pub fn scale_for_dpi(&mut self, scale_factor: f32) {
557 self.horizontal.scale_for_dpi(scale_factor);
558 self.vertical.scale_for_dpi(scale_factor);
559 }
560}
561
562impl PrintAsCssValue for StyleBackgroundPosition {
563 fn print_as_css_value(&self) -> String {
564 format!(
565 "{} {}",
566 self.horizontal.print_as_css_value(),
567 self.vertical.print_as_css_value()
568 )
569 }
570}
571impl PrintAsCssValue for StyleBackgroundPositionVec {
572 fn print_as_css_value(&self) -> String {
573 self.iter()
574 .map(PrintAsCssValue::print_as_css_value)
575 .collect::<Vec<_>>()
576 .join(", ")
577 }
578}
579
580impl crate::codegen::format::FormatAsRustCode for StyleBackgroundPositionVec {
582 fn format_as_rust_code(&self, _tabs: usize) -> String {
583 format!(
584 "StyleBackgroundPositionVec::from_const_slice(STYLE_BACKGROUND_POSITION_{}_ITEMS)",
585 self.get_hash()
586 )
587 }
588}
589#[allow(variant_size_differences)]
590#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
593#[repr(C, u8)]
594pub enum BackgroundPositionHorizontal {
595 Left,
596 Center,
597 Right,
598 Exact(PixelValue),
599}
600
601impl BackgroundPositionHorizontal {
602 pub fn scale_for_dpi(&mut self, scale_factor: f32) {
603 if let Self::Exact(s) = self {
604 s.scale_for_dpi(scale_factor);
605 }
606 }
607}
608
609impl PrintAsCssValue for BackgroundPositionHorizontal {
610 fn print_as_css_value(&self) -> String {
611 match self {
612 Self::Left => "left".to_string(),
613 Self::Center => "center".to_string(),
614 Self::Right => "right".to_string(),
615 Self::Exact(px) => px.print_as_css_value(),
616 }
617 }
618}
619#[allow(variant_size_differences)]
620#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
623#[repr(C, u8)]
624pub enum BackgroundPositionVertical {
625 Top,
626 Center,
627 Bottom,
628 Exact(PixelValue),
629}
630
631impl BackgroundPositionVertical {
632 pub fn scale_for_dpi(&mut self, scale_factor: f32) {
633 if let Self::Exact(s) = self {
634 s.scale_for_dpi(scale_factor);
635 }
636 }
637}
638
639impl PrintAsCssValue for BackgroundPositionVertical {
640 fn print_as_css_value(&self) -> String {
641 match self {
642 Self::Top => "top".to_string(),
643 Self::Center => "center".to_string(),
644 Self::Bottom => "bottom".to_string(),
645 Self::Exact(px) => px.print_as_css_value(),
646 }
647 }
648}
649#[allow(variant_size_differences)]
650#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
653#[repr(C, u8)]
654#[derive(Default)]
655pub enum StyleBackgroundSize {
656 ExactSize(PixelValueSize),
657 #[default]
658 Contain,
659 Cover,
660}
661
662impl_option!(
663 StyleBackgroundSize,
664 OptionStyleBackgroundSize,
665 copy = false,
666 [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
667);
668
669#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
672#[repr(C)]
673pub struct PixelValueSize {
674 pub width: PixelValue,
675 pub height: PixelValue,
676}
677
678impl_vec!(
679 StyleBackgroundSize,
680 StyleBackgroundSizeVec,
681 StyleBackgroundSizeVecDestructor,
682 StyleBackgroundSizeVecDestructorType,
683 StyleBackgroundSizeVecSlice,
684 OptionStyleBackgroundSize
685);
686impl_vec_debug!(StyleBackgroundSize, StyleBackgroundSizeVec);
687impl_vec_partialord!(StyleBackgroundSize, StyleBackgroundSizeVec);
688impl_vec_ord!(StyleBackgroundSize, StyleBackgroundSizeVec);
689impl_vec_clone!(
690 StyleBackgroundSize,
691 StyleBackgroundSizeVec,
692 StyleBackgroundSizeVecDestructor
693);
694impl_vec_partialeq!(StyleBackgroundSize, StyleBackgroundSizeVec);
695impl_vec_eq!(StyleBackgroundSize, StyleBackgroundSizeVec);
696impl_vec_hash!(StyleBackgroundSize, StyleBackgroundSizeVec);
697
698impl StyleBackgroundSize {
699 pub fn scale_for_dpi(&mut self, scale_factor: f32) {
700 if let Self::ExactSize(size) = self {
701 size.width.scale_for_dpi(scale_factor);
702 size.height.scale_for_dpi(scale_factor);
703 }
704 }
705}
706
707impl PrintAsCssValue for StyleBackgroundSize {
708 fn print_as_css_value(&self) -> String {
709 match self {
710 Self::Contain => "contain".to_string(),
711 Self::Cover => "cover".to_string(),
712 Self::ExactSize(size) => {
713 format!(
714 "{} {}",
715 size.width.print_as_css_value(),
716 size.height.print_as_css_value()
717 )
718 }
719 }
720 }
721}
722impl PrintAsCssValue for StyleBackgroundSizeVec {
723 fn print_as_css_value(&self) -> String {
724 self.iter()
725 .map(PrintAsCssValue::print_as_css_value)
726 .collect::<Vec<_>>()
727 .join(", ")
728 }
729}
730
731#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
733#[repr(C)]
734#[derive(Default)]
735pub enum StyleBackgroundRepeat {
736 NoRepeat,
737 #[default]
738 PatternRepeat,
739 RepeatX,
740 RepeatY,
741}
742
743impl_option!(
744 StyleBackgroundRepeat,
745 OptionStyleBackgroundRepeat,
746 [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
747);
748impl_vec!(
749 StyleBackgroundRepeat,
750 StyleBackgroundRepeatVec,
751 StyleBackgroundRepeatVecDestructor,
752 StyleBackgroundRepeatVecDestructorType,
753 StyleBackgroundRepeatVecSlice,
754 OptionStyleBackgroundRepeat
755);
756impl_vec_debug!(StyleBackgroundRepeat, StyleBackgroundRepeatVec);
757impl_vec_partialord!(StyleBackgroundRepeat, StyleBackgroundRepeatVec);
758impl_vec_ord!(StyleBackgroundRepeat, StyleBackgroundRepeatVec);
759impl_vec_clone!(
760 StyleBackgroundRepeat,
761 StyleBackgroundRepeatVec,
762 StyleBackgroundRepeatVecDestructor
763);
764impl_vec_partialeq!(StyleBackgroundRepeat, StyleBackgroundRepeatVec);
765impl_vec_eq!(StyleBackgroundRepeat, StyleBackgroundRepeatVec);
766impl_vec_hash!(StyleBackgroundRepeat, StyleBackgroundRepeatVec);
767impl PrintAsCssValue for StyleBackgroundRepeat {
768 fn print_as_css_value(&self) -> String {
769 match self {
770 Self::NoRepeat => "no-repeat".to_string(),
771 Self::PatternRepeat => "repeat".to_string(),
772 Self::RepeatX => "repeat-x".to_string(),
773 Self::RepeatY => "repeat-y".to_string(),
774 }
775 }
776}
777impl PrintAsCssValue for StyleBackgroundRepeatVec {
778 fn print_as_css_value(&self) -> String {
779 self.iter()
780 .map(PrintAsCssValue::print_as_css_value)
781 .collect::<Vec<_>>()
782 .join(", ")
783 }
784}
785
786#[derive(Clone, PartialEq)]
789pub enum CssBackgroundParseError<'a> {
790 Error(&'a str),
791 InvalidBackground(ParenthesisParseError<'a>),
792 UnclosedGradient(&'a str),
793 NoDirection(&'a str),
794 TooFewGradientStops(&'a str),
795 DirectionParseError(CssDirectionParseError<'a>),
796 GradientParseError(CssGradientStopParseError<'a>),
797 ConicGradient(CssConicGradientParseError<'a>),
798 ShapeParseError(CssShapeParseError<'a>),
799 ImageParseError(CssImageParseError<'a>),
800 ColorParseError(CssColorParseError<'a>),
801}
802
803impl_debug_as_display!(CssBackgroundParseError<'a>);
804impl_display! { CssBackgroundParseError<'a>, {
805 Error(e) => e,
806 InvalidBackground(val) => format!("Invalid background value: \"{}\"", val),
807 UnclosedGradient(val) => format!("Unclosed gradient: \"{}\"", val),
808 NoDirection(val) => format!("Gradient has no direction: \"{}\"", val),
809 TooFewGradientStops(val) => format!("Failed to parse gradient due to too few gradient steps: \"{}\"", val),
810 DirectionParseError(e) => format!("Failed to parse gradient direction: \"{}\"", e),
811 GradientParseError(e) => format!("Failed to parse gradient: {}", e),
812 ConicGradient(e) => format!("Failed to parse conic gradient: {}", e),
813 ShapeParseError(e) => format!("Failed to parse shape of radial gradient: {}", e),
814 ImageParseError(e) => format!("Failed to parse image() value: {}", e),
815 ColorParseError(e) => format!("Failed to parse color value: {}", e),
816}}
817
818#[cfg(feature = "parser")]
819impl_from!(
820 ParenthesisParseError<'a>,
821 CssBackgroundParseError::InvalidBackground
822);
823#[cfg(feature = "parser")]
824impl_from!(
825 CssDirectionParseError<'a>,
826 CssBackgroundParseError::DirectionParseError
827);
828#[cfg(feature = "parser")]
829impl_from!(
830 CssGradientStopParseError<'a>,
831 CssBackgroundParseError::GradientParseError
832);
833#[cfg(feature = "parser")]
834impl_from!(
835 CssShapeParseError<'a>,
836 CssBackgroundParseError::ShapeParseError
837);
838#[cfg(feature = "parser")]
839impl_from!(
840 CssImageParseError<'a>,
841 CssBackgroundParseError::ImageParseError
842);
843#[cfg(feature = "parser")]
844impl_from!(
845 CssColorParseError<'a>,
846 CssBackgroundParseError::ColorParseError
847);
848#[cfg(feature = "parser")]
849impl_from!(
850 CssConicGradientParseError<'a>,
851 CssBackgroundParseError::ConicGradient
852);
853
854#[derive(Debug, Clone, PartialEq)]
855#[repr(C, u8)]
856pub enum CssBackgroundParseErrorOwned {
857 Error(AzString),
858 InvalidBackground(ParenthesisParseErrorOwned),
859 UnclosedGradient(AzString),
860 NoDirection(AzString),
861 TooFewGradientStops(AzString),
862 DirectionParseError(CssDirectionParseErrorOwned),
863 GradientParseError(CssGradientStopParseErrorOwned),
864 ConicGradient(CssConicGradientParseErrorOwned),
865 ShapeParseError(CssShapeParseErrorOwned),
866 ImageParseError(CssImageParseErrorOwned),
867 ColorParseError(CssColorParseErrorOwned),
868}
869
870impl CssBackgroundParseError<'_> {
871 #[must_use]
872 pub fn to_contained(&self) -> CssBackgroundParseErrorOwned {
873 match self {
874 Self::Error(s) => CssBackgroundParseErrorOwned::Error((*s).to_string().into()),
875 Self::InvalidBackground(e) => {
876 CssBackgroundParseErrorOwned::InvalidBackground(e.to_contained())
877 }
878 Self::UnclosedGradient(s) => {
879 CssBackgroundParseErrorOwned::UnclosedGradient((*s).to_string().into())
880 }
881 Self::NoDirection(s) => {
882 CssBackgroundParseErrorOwned::NoDirection((*s).to_string().into())
883 }
884 Self::TooFewGradientStops(s) => {
885 CssBackgroundParseErrorOwned::TooFewGradientStops((*s).to_string().into())
886 }
887 Self::DirectionParseError(e) => {
888 CssBackgroundParseErrorOwned::DirectionParseError(e.to_contained())
889 }
890 Self::GradientParseError(e) => {
891 CssBackgroundParseErrorOwned::GradientParseError(e.to_contained())
892 }
893 Self::ConicGradient(e) => CssBackgroundParseErrorOwned::ConicGradient(e.to_contained()),
894 Self::ShapeParseError(e) => {
895 CssBackgroundParseErrorOwned::ShapeParseError(e.to_contained())
896 }
897 Self::ImageParseError(e) => {
898 CssBackgroundParseErrorOwned::ImageParseError(e.to_contained())
899 }
900 Self::ColorParseError(e) => {
901 CssBackgroundParseErrorOwned::ColorParseError(e.to_contained())
902 }
903 }
904 }
905}
906
907impl CssBackgroundParseErrorOwned {
908 #[must_use]
909 pub fn to_shared(&self) -> CssBackgroundParseError<'_> {
910 match self {
911 Self::Error(s) => CssBackgroundParseError::Error(s),
912 Self::InvalidBackground(e) => CssBackgroundParseError::InvalidBackground(e.to_shared()),
913 Self::UnclosedGradient(s) => CssBackgroundParseError::UnclosedGradient(s),
914 Self::NoDirection(s) => CssBackgroundParseError::NoDirection(s),
915 Self::TooFewGradientStops(s) => CssBackgroundParseError::TooFewGradientStops(s),
916 Self::DirectionParseError(e) => {
917 CssBackgroundParseError::DirectionParseError(e.to_shared())
918 }
919 Self::GradientParseError(e) => {
920 CssBackgroundParseError::GradientParseError(e.to_shared())
921 }
922 Self::ConicGradient(e) => CssBackgroundParseError::ConicGradient(e.to_shared()),
923 Self::ShapeParseError(e) => CssBackgroundParseError::ShapeParseError(e.to_shared()),
924 Self::ImageParseError(e) => CssBackgroundParseError::ImageParseError(e.to_shared()),
925 Self::ColorParseError(e) => CssBackgroundParseError::ColorParseError(e.to_shared()),
926 }
927 }
928}
929
930#[derive(Clone, PartialEq)]
931pub enum CssGradientStopParseError<'a> {
932 Error(&'a str),
933 Percentage(PercentageParseError),
934 Angle(CssAngleValueParseError<'a>),
935 ColorParseError(CssColorParseError<'a>),
936}
937
938impl_debug_as_display!(CssGradientStopParseError<'a>);
939impl_display! { CssGradientStopParseError<'a>, {
940 Error(e) => e,
941 Percentage(e) => format!("Failed to parse offset percentage: {}", e),
942 Angle(e) => format!("Failed to parse angle: {}", e),
943 ColorParseError(e) => format!("{}", e),
944}}
945#[cfg(feature = "parser")]
946impl_from!(
947 CssColorParseError<'a>,
948 CssGradientStopParseError::ColorParseError
949);
950
951#[derive(Debug, Clone, PartialEq)]
952#[repr(C, u8)]
953pub enum CssGradientStopParseErrorOwned {
954 Error(AzString),
955 Percentage(PercentageParseErrorOwned),
956 Angle(CssAngleValueParseErrorOwned),
957 ColorParseError(CssColorParseErrorOwned),
958}
959
960impl CssGradientStopParseError<'_> {
961 #[must_use]
962 pub fn to_contained(&self) -> CssGradientStopParseErrorOwned {
963 match self {
964 Self::Error(s) => CssGradientStopParseErrorOwned::Error((*s).to_string().into()),
965 Self::Percentage(e) => CssGradientStopParseErrorOwned::Percentage(e.to_contained()),
966 Self::Angle(e) => CssGradientStopParseErrorOwned::Angle(e.to_contained()),
967 Self::ColorParseError(e) => {
968 CssGradientStopParseErrorOwned::ColorParseError(e.to_contained())
969 }
970 }
971 }
972}
973
974impl CssGradientStopParseErrorOwned {
975 #[must_use]
976 pub fn to_shared(&self) -> CssGradientStopParseError<'_> {
977 match self {
978 Self::Error(s) => CssGradientStopParseError::Error(s),
979 Self::Percentage(e) => CssGradientStopParseError::Percentage(e.to_shared()),
980 Self::Angle(e) => CssGradientStopParseError::Angle(e.to_shared()),
981 Self::ColorParseError(e) => CssGradientStopParseError::ColorParseError(e.to_shared()),
982 }
983 }
984}
985
986#[derive(Clone, PartialEq, Eq)]
987pub enum CssConicGradientParseError<'a> {
988 Position(CssBackgroundPositionParseError<'a>),
989 Angle(CssAngleValueParseError<'a>),
990 NoAngle(&'a str),
991}
992impl_debug_as_display!(CssConicGradientParseError<'a>);
993impl_display! { CssConicGradientParseError<'a>, {
994 Position(val) => format!("Invalid position attribute: \"{}\"", val),
995 Angle(val) => format!("Invalid angle value: \"{}\"", val),
996 NoAngle(val) => format!("Expected angle: \"{}\"", val),
997}}
998#[cfg(feature = "parser")]
999impl_from!(
1000 CssAngleValueParseError<'a>,
1001 CssConicGradientParseError::Angle
1002);
1003#[cfg(feature = "parser")]
1004impl_from!(
1005 CssBackgroundPositionParseError<'a>,
1006 CssConicGradientParseError::Position
1007);
1008
1009#[derive(Debug, Clone, PartialEq, Eq)]
1010#[repr(C, u8)]
1011pub enum CssConicGradientParseErrorOwned {
1012 Position(CssBackgroundPositionParseErrorOwned),
1013 Angle(CssAngleValueParseErrorOwned),
1014 NoAngle(AzString),
1015}
1016impl CssConicGradientParseError<'_> {
1017 #[must_use]
1018 pub fn to_contained(&self) -> CssConicGradientParseErrorOwned {
1019 match self {
1020 Self::Position(e) => CssConicGradientParseErrorOwned::Position(e.to_contained()),
1021 Self::Angle(e) => CssConicGradientParseErrorOwned::Angle(e.to_contained()),
1022 Self::NoAngle(s) => CssConicGradientParseErrorOwned::NoAngle((*s).to_string().into()),
1023 }
1024 }
1025}
1026impl CssConicGradientParseErrorOwned {
1027 #[must_use]
1028 pub fn to_shared(&self) -> CssConicGradientParseError<'_> {
1029 match self {
1030 Self::Position(e) => CssConicGradientParseError::Position(e.to_shared()),
1031 Self::Angle(e) => CssConicGradientParseError::Angle(e.to_shared()),
1032 Self::NoAngle(s) => CssConicGradientParseError::NoAngle(s),
1033 }
1034 }
1035}
1036
1037#[derive(Debug, Copy, Clone, PartialEq, Eq)]
1038pub enum CssShapeParseError<'a> {
1039 ShapeErr(InvalidValueErr<'a>),
1040}
1041impl_display! {CssShapeParseError<'a>, {
1042 ShapeErr(e) => format!("\"{}\"", e.0),
1043}}
1044#[derive(Debug, Clone, PartialEq, Eq)]
1045#[repr(C, u8)]
1046pub enum CssShapeParseErrorOwned {
1047 ShapeErr(InvalidValueErrOwned),
1048}
1049impl CssShapeParseError<'_> {
1050 #[must_use]
1051 pub fn to_contained(&self) -> CssShapeParseErrorOwned {
1052 match self {
1053 Self::ShapeErr(err) => CssShapeParseErrorOwned::ShapeErr(err.to_contained()),
1054 }
1055 }
1056}
1057impl CssShapeParseErrorOwned {
1058 #[must_use]
1059 pub fn to_shared(&self) -> CssShapeParseError<'_> {
1060 match self {
1061 Self::ShapeErr(err) => CssShapeParseError::ShapeErr(err.to_shared()),
1062 }
1063 }
1064}
1065
1066#[derive(Debug, Clone, PartialEq, Eq)]
1067pub enum CssBackgroundPositionParseError<'a> {
1068 NoPosition(&'a str),
1069 TooManyComponents(&'a str),
1070 FirstComponentWrong(CssPixelValueParseError<'a>),
1071 SecondComponentWrong(CssPixelValueParseError<'a>),
1072}
1073
1074impl_display! {CssBackgroundPositionParseError<'a>, {
1075 NoPosition(e) => format!("First background position missing: \"{}\"", e),
1076 TooManyComponents(e) => format!("background-position can only have one or two components, not more: \"{}\"", e),
1077 FirstComponentWrong(e) => format!("Failed to parse first component: \"{}\"", e),
1078 SecondComponentWrong(e) => format!("Failed to parse second component: \"{}\"", e),
1079}}
1080#[derive(Debug, Clone, PartialEq, Eq)]
1081#[repr(C, u8)]
1082pub enum CssBackgroundPositionParseErrorOwned {
1083 NoPosition(AzString),
1084 TooManyComponents(AzString),
1085 FirstComponentWrong(CssPixelValueParseErrorOwned),
1086 SecondComponentWrong(CssPixelValueParseErrorOwned),
1087}
1088impl CssBackgroundPositionParseError<'_> {
1089 #[must_use]
1090 pub fn to_contained(&self) -> CssBackgroundPositionParseErrorOwned {
1091 match self {
1092 Self::NoPosition(s) => {
1093 CssBackgroundPositionParseErrorOwned::NoPosition((*s).to_string().into())
1094 }
1095 Self::TooManyComponents(s) => {
1096 CssBackgroundPositionParseErrorOwned::TooManyComponents((*s).to_string().into())
1097 }
1098 Self::FirstComponentWrong(e) => {
1099 CssBackgroundPositionParseErrorOwned::FirstComponentWrong(e.to_contained())
1100 }
1101 Self::SecondComponentWrong(e) => {
1102 CssBackgroundPositionParseErrorOwned::SecondComponentWrong(e.to_contained())
1103 }
1104 }
1105 }
1106}
1107impl CssBackgroundPositionParseErrorOwned {
1108 #[must_use]
1109 pub fn to_shared(&self) -> CssBackgroundPositionParseError<'_> {
1110 match self {
1111 Self::NoPosition(s) => CssBackgroundPositionParseError::NoPosition(s),
1112 Self::TooManyComponents(s) => CssBackgroundPositionParseError::TooManyComponents(s),
1113 Self::FirstComponentWrong(e) => {
1114 CssBackgroundPositionParseError::FirstComponentWrong(e.to_shared())
1115 }
1116 Self::SecondComponentWrong(e) => {
1117 CssBackgroundPositionParseError::SecondComponentWrong(e.to_shared())
1118 }
1119 }
1120 }
1121}
1122
1123#[cfg(feature = "parser")]
1126pub mod parser {
1127 #[allow(clippy::wildcard_imports)]
1128 use super::*;
1130
1131 #[allow(clippy::enum_variant_names)]
1134 #[derive(Debug, Copy, Clone, PartialEq, Eq)]
1135 enum GradientType {
1136 LinearGradient,
1137 RepeatingLinearGradient,
1138 RadialGradient,
1139 RepeatingRadialGradient,
1140 ConicGradient,
1141 RepeatingConicGradient,
1142 }
1143
1144 impl GradientType {
1145 pub(crate) const fn get_extend_mode(self) -> ExtendMode {
1146 match self {
1147 Self::LinearGradient | Self::RadialGradient | Self::ConicGradient => {
1148 ExtendMode::Clamp
1149 }
1150 Self::RepeatingLinearGradient
1151 | Self::RepeatingRadialGradient
1152 | Self::RepeatingConicGradient => ExtendMode::Repeat,
1153 }
1154 }
1155 }
1156
1157 pub fn parse_style_background_content_multiple(
1164 input: &str,
1165 ) -> Result<StyleBackgroundContentVec, CssBackgroundParseError<'_>> {
1166 Ok(split_string_respect_comma(input)
1167 .iter()
1168 .map(|i| parse_style_background_content(i))
1169 .collect::<Result<Vec<_>, _>>()?
1170 .into())
1171 }
1172
1173 pub fn parse_style_background_content(
1178 input: &str,
1179 ) -> Result<StyleBackgroundContent, CssBackgroundParseError<'_>> {
1180 match parse_parentheses(
1181 input,
1182 &[
1183 "linear-gradient",
1184 "repeating-linear-gradient",
1185 "radial-gradient",
1186 "repeating-radial-gradient",
1187 "conic-gradient",
1188 "repeating-conic-gradient",
1189 "image",
1190 "url",
1191 ],
1192 ) {
1193 Ok((background_type, brace_contents)) => {
1194 let gradient_type = match background_type {
1195 "linear-gradient" => GradientType::LinearGradient,
1196 "repeating-linear-gradient" => GradientType::RepeatingLinearGradient,
1197 "radial-gradient" => GradientType::RadialGradient,
1198 "repeating-radial-gradient" => GradientType::RepeatingRadialGradient,
1199 "conic-gradient" => GradientType::ConicGradient,
1200 "repeating-conic-gradient" => GradientType::RepeatingConicGradient,
1201 "image" | "url" => {
1202 return Ok(StyleBackgroundContent::Image(parse_image(brace_contents)?))
1203 }
1204 _ => unreachable!(),
1205 };
1206 parse_gradient(brace_contents, gradient_type)
1207 }
1208 Err(_) => Ok(match parse_color_or_system(input)? {
1214 ColorOrSystem::Color(c) => StyleBackgroundContent::Color(c),
1215 ColorOrSystem::System(s) => StyleBackgroundContent::SystemColor(s),
1216 }),
1217 }
1218 }
1219
1220 pub fn parse_style_background_position_multiple(
1225 input: &str,
1226 ) -> Result<StyleBackgroundPositionVec, CssBackgroundPositionParseError<'_>> {
1227 Ok(split_string_respect_comma(input)
1228 .iter()
1229 .map(|i| parse_style_background_position(i))
1230 .collect::<Result<Vec<_>, _>>()?
1231 .into())
1232 }
1233
1234 pub fn parse_style_background_position(
1239 input: &str,
1240 ) -> Result<StyleBackgroundPosition, CssBackgroundPositionParseError<'_>> {
1241 let input = input.trim();
1242 let mut whitespace_iter = input.split_whitespace();
1243
1244 let first = whitespace_iter
1245 .next()
1246 .ok_or(CssBackgroundPositionParseError::NoPosition(input))?;
1247 let second = whitespace_iter.next();
1248
1249 if whitespace_iter.next().is_some() {
1250 return Err(CssBackgroundPositionParseError::TooManyComponents(input));
1251 }
1252
1253 if let Ok(horizontal) = parse_background_position_horizontal(first) {
1255 let vertical = match second {
1256 Some(s) => parse_background_position_vertical(s)
1257 .map_err(CssBackgroundPositionParseError::SecondComponentWrong)?,
1258 None => BackgroundPositionVertical::Center,
1259 };
1260 return Ok(StyleBackgroundPosition {
1261 horizontal,
1262 vertical,
1263 });
1264 }
1265
1266 if let Ok(vertical) = parse_background_position_vertical(first) {
1268 let horizontal = match second {
1269 Some(s) => parse_background_position_horizontal(s)
1270 .map_err(CssBackgroundPositionParseError::FirstComponentWrong)?,
1271 None => BackgroundPositionHorizontal::Center,
1272 };
1273 return Ok(StyleBackgroundPosition {
1274 horizontal,
1275 vertical,
1276 });
1277 }
1278
1279 Err(CssBackgroundPositionParseError::FirstComponentWrong(
1280 CssPixelValueParseError::InvalidPixelValue(first),
1281 ))
1282 }
1283
1284 pub fn parse_style_background_size_multiple(
1289 input: &str,
1290 ) -> Result<StyleBackgroundSizeVec, InvalidValueErr<'_>> {
1291 Ok(split_string_respect_comma(input)
1292 .iter()
1293 .map(|i| parse_style_background_size(i))
1294 .collect::<Result<Vec<_>, _>>()?
1295 .into())
1296 }
1297
1298 pub fn parse_style_background_size(
1303 input: &str,
1304 ) -> Result<StyleBackgroundSize, InvalidValueErr<'_>> {
1305 let input = input.trim();
1306 match input {
1307 "contain" => Ok(StyleBackgroundSize::Contain),
1308 "cover" => Ok(StyleBackgroundSize::Cover),
1309 other => {
1310 let mut iter = other.split_whitespace();
1311 let x_val = iter.next().ok_or(InvalidValueErr(input))?;
1312 let x_pos = parse_pixel_value(x_val).map_err(|_| InvalidValueErr(input))?;
1313 let y_pos = match iter.next() {
1314 Some(y_val) => parse_pixel_value(y_val).map_err(|_| InvalidValueErr(input))?,
1315 None => x_pos, };
1317 Ok(StyleBackgroundSize::ExactSize(PixelValueSize {
1318 width: x_pos,
1319 height: y_pos,
1320 }))
1321 }
1322 }
1323 }
1324
1325 pub fn parse_style_background_repeat_multiple(
1330 input: &str,
1331 ) -> Result<StyleBackgroundRepeatVec, InvalidValueErr<'_>> {
1332 Ok(split_string_respect_comma(input)
1333 .iter()
1334 .map(|i| parse_style_background_repeat(i))
1335 .collect::<Result<Vec<_>, _>>()?
1336 .into())
1337 }
1338
1339 pub fn parse_style_background_repeat(
1344 input: &str,
1345 ) -> Result<StyleBackgroundRepeat, InvalidValueErr<'_>> {
1346 match input.trim() {
1347 "no-repeat" => Ok(StyleBackgroundRepeat::NoRepeat),
1348 "repeat" => Ok(StyleBackgroundRepeat::PatternRepeat),
1349 "repeat-x" => Ok(StyleBackgroundRepeat::RepeatX),
1350 "repeat-y" => Ok(StyleBackgroundRepeat::RepeatY),
1351 _ => Err(InvalidValueErr(input)),
1352 }
1353 }
1354
1355 fn parse_gradient(
1359 input: &str,
1360 gradient_type: GradientType,
1361 ) -> Result<StyleBackgroundContent, CssBackgroundParseError<'_>> {
1362 let input = input.trim();
1363 let comma_separated_items = split_string_respect_comma(input);
1364 let mut brace_iterator = comma_separated_items.iter();
1365 let first_brace_item = brace_iterator
1366 .next()
1367 .ok_or(CssBackgroundParseError::NoDirection(input))?;
1368
1369 match gradient_type {
1370 GradientType::LinearGradient | GradientType::RepeatingLinearGradient => {
1371 let mut linear_gradient = LinearGradient {
1372 extend_mode: gradient_type.get_extend_mode(),
1373 ..Default::default()
1374 };
1375 let mut linear_stops = Vec::new();
1376
1377 if let Ok(dir) = parse_direction(first_brace_item) {
1378 linear_gradient.direction = dir;
1379 } else {
1380 linear_stops.push(parse_linear_color_stop(first_brace_item)?);
1381 }
1382
1383 for item in brace_iterator {
1384 linear_stops.push(parse_linear_color_stop(item)?);
1385 }
1386
1387 linear_gradient.stops = get_normalized_linear_stops(&linear_stops).into();
1388 Ok(StyleBackgroundContent::LinearGradient(linear_gradient))
1389 }
1390 GradientType::RadialGradient | GradientType::RepeatingRadialGradient => {
1391 let mut radial_gradient = RadialGradient {
1394 extend_mode: gradient_type.get_extend_mode(),
1395 ..Default::default()
1396 };
1397 let mut radial_stops = Vec::new();
1398 let mut current_item = *first_brace_item;
1399 let mut items_consumed = false;
1400
1401 loop {
1403 let mut consumed_in_iteration = false;
1404 let mut temp_iter = current_item.split_whitespace();
1405 for word in temp_iter {
1406 if let Ok(shape) = parse_shape(word) {
1407 radial_gradient.shape = shape;
1408 consumed_in_iteration = true;
1409 } else if let Ok(size) = parse_radial_gradient_size(word) {
1410 radial_gradient.size = size;
1411 consumed_in_iteration = true;
1412 } else if let Ok(pos) = parse_style_background_position(current_item) {
1413 radial_gradient.position = pos;
1414 consumed_in_iteration = true;
1415 break; }
1418 }
1419 if consumed_in_iteration {
1420 if let Some(next_item) = brace_iterator.next() {
1421 current_item = next_item;
1422 items_consumed = true;
1423 } else {
1424 break;
1425 }
1426 } else {
1427 break;
1428 }
1429 }
1430
1431 if items_consumed || parse_linear_color_stop(current_item).is_ok() {
1432 radial_stops.push(parse_linear_color_stop(current_item)?);
1433 }
1434
1435 for item in brace_iterator {
1436 radial_stops.push(parse_linear_color_stop(item)?);
1437 }
1438
1439 radial_gradient.stops = get_normalized_linear_stops(&radial_stops).into();
1440 Ok(StyleBackgroundContent::RadialGradient(radial_gradient))
1441 }
1442 GradientType::ConicGradient | GradientType::RepeatingConicGradient => {
1443 let mut conic_gradient = ConicGradient {
1444 extend_mode: gradient_type.get_extend_mode(),
1445 ..Default::default()
1446 };
1447 let mut conic_stops = Vec::new();
1448
1449 if let Some((angle, center)) = parse_conic_first_item(first_brace_item)? {
1450 conic_gradient.angle = angle;
1451 conic_gradient.center = center;
1452 } else {
1453 conic_stops.push(parse_radial_color_stop(first_brace_item)?);
1454 }
1455
1456 for item in brace_iterator {
1457 conic_stops.push(parse_radial_color_stop(item)?);
1458 }
1459
1460 conic_gradient.stops = get_normalized_radial_stops(&conic_stops).into();
1461 Ok(StyleBackgroundContent::ConicGradient(conic_gradient))
1462 }
1463 }
1464 }
1465
1466 fn parse_linear_color_stop(
1475 input: &str,
1476 ) -> Result<LinearColorStop, CssGradientStopParseError<'_>> {
1477 let input = input.trim();
1478 let (color_str, offset1_str, offset2_str) = split_color_and_offsets(input);
1479
1480 let color = parse_color_or_system(color_str)?;
1481 let offset1 = match offset1_str {
1482 None => OptionPercentageValue::None,
1483 Some(s) => OptionPercentageValue::Some(
1484 parse_percentage_value(s).map_err(CssGradientStopParseError::Percentage)?,
1485 ),
1486 };
1487 let offset2 = match offset2_str {
1488 None => OptionPercentageValue::None,
1489 Some(s) => OptionPercentageValue::Some(
1490 parse_percentage_value(s).map_err(CssGradientStopParseError::Percentage)?,
1491 ),
1492 };
1493
1494 Ok(LinearColorStop {
1495 color,
1496 offset1,
1497 offset2,
1498 })
1499 }
1500
1501 fn parse_radial_color_stop(
1508 input: &str,
1509 ) -> Result<RadialColorStop, CssGradientStopParseError<'_>> {
1510 let input = input.trim();
1511 let (color_str, offset1_str, offset2_str) = split_color_and_offsets(input);
1512
1513 let color = parse_color_or_system(color_str)?;
1514 let offset1 = match offset1_str {
1515 None => OptionAngleValue::None,
1516 Some(s) => OptionAngleValue::Some(
1517 parse_angle_value(s).map_err(CssGradientStopParseError::Angle)?,
1518 ),
1519 };
1520 let offset2 = match offset2_str {
1521 None => OptionAngleValue::None,
1522 Some(s) => OptionAngleValue::Some(
1523 parse_angle_value(s).map_err(CssGradientStopParseError::Angle)?,
1524 ),
1525 };
1526
1527 Ok(RadialColorStop {
1528 color,
1529 offset1,
1530 offset2,
1531 })
1532 }
1533
1534 fn split_color_and_offsets(input: &str) -> (&str, Option<&str>, Option<&str>) {
1542 let input = input.trim();
1547
1548 if let Some((remaining, last_offset)) = try_split_last_offset(input) {
1550 if let Some((color_part, first_offset)) = try_split_last_offset(remaining) {
1552 return (color_part.trim(), Some(first_offset), Some(last_offset));
1553 }
1554 return (remaining.trim(), Some(last_offset), None);
1555 }
1556
1557 (input, None, None)
1558 }
1559
1560 fn try_split_last_offset(input: &str) -> Option<(&str, &str)> {
1563 let input = input.trim();
1564 if let Some(last_ws_idx) = input.rfind(char::is_whitespace) {
1565 let (potential_color, potential_offset) = input.split_at(last_ws_idx);
1566 let potential_offset = potential_offset.trim();
1567
1568 if is_likely_offset(potential_offset) {
1571 return Some((potential_color, potential_offset));
1572 }
1573 }
1574 None
1575 }
1576
1577 fn is_likely_offset(s: &str) -> bool {
1580 if !s.contains(|c: char| c.is_ascii_digit()) {
1581 return false;
1582 }
1583 let units = [
1585 "%", "px", "em", "rem", "ex", "ch", "vw", "vh", "vmin", "vmax", "cm", "mm", "in", "pt",
1586 "pc", "deg", "rad", "grad", "turn",
1587 ];
1588 units.iter().any(|u| s.ends_with(u))
1589 }
1590
1591 fn parse_conic_first_item(
1593 input: &str,
1594 ) -> Result<Option<(AngleValue, StyleBackgroundPosition)>, CssConicGradientParseError<'_>> {
1595 let input = input.trim();
1596 if !input.starts_with("from") {
1597 return Ok(None);
1598 }
1599
1600 let mut parts = input["from".len()..].trim().split("at");
1601 let angle_part = parts
1602 .next()
1603 .ok_or(CssConicGradientParseError::NoAngle(input))?
1604 .trim();
1605 let angle = parse_angle_value(angle_part)?;
1606
1607 let position = match parts.next() {
1608 Some(pos_part) => parse_style_background_position(pos_part.trim())?,
1609 None => StyleBackgroundPosition::default(),
1610 };
1611
1612 Ok(Some((angle, position)))
1613 }
1614
1615 macro_rules! impl_get_normalized_stops {
1618 (
1619 fn $fn_name:ident($input_stop:ty) -> Vec<$output_stop:ident>,
1620 pos_type = $pos_ty:ty,
1621 default_start = $default_start:expr,
1622 default_end = $default_end:expr,
1623 pos_ctor = $pos_ctor:expr,
1624 pos_to_f32 = $pos_to_f32:expr,
1625 output_field = $out_field:ident,
1626 ) => {
1627 #[allow(clippy::suboptimal_flops)] fn $fn_name(stops: &[$input_stop]) -> Vec<$output_stop> {
1629 if stops.is_empty() {
1630 return Vec::new();
1631 }
1632
1633 let mut expanded: Vec<(ColorOrSystem, Option<$pos_ty>)> = Vec::new();
1634
1635 for stop in stops {
1636 match (stop.offset1.into_option(), stop.offset2.into_option()) {
1637 (None, _) => {
1638 expanded.push((stop.color, None));
1639 }
1640 (Some(pos1), None) => {
1641 expanded.push((stop.color, Some(pos1)));
1642 }
1643 (Some(pos1), Some(pos2)) => {
1644 expanded.push((stop.color, Some(pos1)));
1645 expanded.push((stop.color, Some(pos2)));
1646 }
1647 }
1648 }
1649
1650 if expanded.is_empty() {
1651 return Vec::new();
1652 }
1653
1654 let pos_ctor: fn(f32) -> $pos_ty = $pos_ctor;
1655 let pos_to_f32: fn(&$pos_ty) -> f32 = $pos_to_f32;
1656
1657 if expanded[0].1.is_none() {
1658 expanded[0].1 = Some(pos_ctor($default_start));
1659 }
1660 let last_idx = expanded.len() - 1;
1661 if expanded[last_idx].1.is_none() {
1662 expanded[last_idx].1 = Some(pos_ctor($default_end));
1663 }
1664
1665 let mut max_so_far: f32 = 0.0;
1666 for (_, pos) in expanded.iter_mut() {
1667 if let Some(p) = pos {
1668 let val = pos_to_f32(p);
1669 if val < max_so_far {
1670 *p = pos_ctor(max_so_far);
1671 } else {
1672 max_so_far = val;
1673 }
1674 }
1675 }
1676
1677 let mut i = 0;
1678 while i < expanded.len() {
1679 if expanded[i].1.is_none() {
1680 let run_start = i;
1681 let mut run_end = i;
1682 while run_end < expanded.len() && expanded[run_end].1.is_none() {
1683 run_end += 1;
1684 }
1685
1686 let prev_pos = if run_start > 0 {
1687 pos_to_f32(&expanded[run_start - 1].1.unwrap())
1688 } else {
1689 $default_start
1690 };
1691
1692 let next_pos = if run_end < expanded.len() {
1693 pos_to_f32(&expanded[run_end].1.unwrap())
1694 } else {
1695 $default_end
1696 };
1697
1698 let run_len = run_end - run_start;
1699 let step = (next_pos - prev_pos) / crate::cast::usize_to_f32(run_len + 1);
1700
1701 for j in 0..run_len {
1702 expanded[run_start + j].1 =
1703 Some(pos_ctor(prev_pos + step * crate::cast::usize_to_f32(j + 1)));
1704 }
1705
1706 i = run_end;
1707 } else {
1708 i += 1;
1709 }
1710 }
1711
1712 expanded
1713 .into_iter()
1714 .map(|(color, pos)| $output_stop {
1715 $out_field: pos.unwrap_or(pos_ctor($default_start)),
1716 color,
1717 })
1718 .collect()
1719 }
1720 };
1721 }
1722
1723 impl_get_normalized_stops! {
1724 fn get_normalized_linear_stops(LinearColorStop) -> Vec<NormalizedLinearColorStop>,
1725 pos_type = PercentageValue,
1726 default_start = 0.0,
1727 default_end = 100.0,
1728 pos_ctor = (|v| PercentageValue::new(v)),
1729 pos_to_f32 = (|p: &PercentageValue| p.normalized() * 100.0),
1730 output_field = offset,
1731 }
1732
1733 impl_get_normalized_stops! {
1734 fn get_normalized_radial_stops(RadialColorStop) -> Vec<NormalizedRadialColorStop>,
1735 pos_type = AngleValue,
1736 default_start = 0.0,
1737 default_end = 360.0,
1738 pos_ctor = (|v| AngleValue::deg(v)),
1739 pos_to_f32 = (|p: &AngleValue| p.to_degrees_raw()),
1740 output_field = angle,
1741 }
1742
1743 fn parse_background_position_horizontal(
1746 input: &str,
1747 ) -> Result<BackgroundPositionHorizontal, CssPixelValueParseError<'_>> {
1748 Ok(match input {
1749 "left" => BackgroundPositionHorizontal::Left,
1750 "center" => BackgroundPositionHorizontal::Center,
1751 "right" => BackgroundPositionHorizontal::Right,
1752 other => BackgroundPositionHorizontal::Exact(parse_pixel_value(other)?),
1753 })
1754 }
1755
1756 fn parse_background_position_vertical(
1757 input: &str,
1758 ) -> Result<BackgroundPositionVertical, CssPixelValueParseError<'_>> {
1759 Ok(match input {
1760 "top" => BackgroundPositionVertical::Top,
1761 "center" => BackgroundPositionVertical::Center,
1762 "bottom" => BackgroundPositionVertical::Bottom,
1763 other => BackgroundPositionVertical::Exact(parse_pixel_value(other)?),
1764 })
1765 }
1766
1767 fn parse_shape(input: &str) -> Result<Shape, CssShapeParseError<'_>> {
1768 match input.trim() {
1769 "circle" => Ok(Shape::Circle),
1770 "ellipse" => Ok(Shape::Ellipse),
1771 _ => Err(CssShapeParseError::ShapeErr(InvalidValueErr(input))),
1772 }
1773 }
1774
1775 fn parse_radial_gradient_size(input: &str) -> Result<RadialGradientSize, InvalidValueErr<'_>> {
1776 match input.trim() {
1777 "closest-side" => Ok(RadialGradientSize::ClosestSide),
1778 "closest-corner" => Ok(RadialGradientSize::ClosestCorner),
1779 "farthest-side" => Ok(RadialGradientSize::FarthestSide),
1780 "farthest-corner" => Ok(RadialGradientSize::FarthestCorner),
1781 _ => Err(InvalidValueErr(input)),
1782 }
1783 }
1784
1785 #[cfg(test)]
1790 #[allow(
1791 clippy::float_cmp,
1792 clippy::too_many_lines,
1793 clippy::unreadable_literal,
1794 clippy::cognitive_complexity,
1795 clippy::wildcard_imports
1796 )]
1797 mod autotest_generated {
1798 use super::*;
1801 use crate::props::style::background::*;
1802 use crate::{
1803 props::basic::{
1804 angle::CssAngleValueParseError,
1805 color::{CssColorParseError, OptionColorU, SystemColorRef},
1806 direction::CssDirectionParseError,
1807 error::InvalidValueErr,
1808 length::PercentageParseError,
1809 parse::{CssImageParseError, ParenthesisParseError},
1810 pixel::CssPixelValueParseError,
1811 },
1812 system::SystemColors,
1813 };
1814 use alloc::{string::ToString, vec::Vec};
1815
1816 const ADVERSARIAL: &[&str] = &[
1823 "",
1824 " ",
1825 " ",
1826 "\t\n\r",
1827 "\u{0}",
1828 "!!!",
1829 ";",
1830 ",",
1831 ",,",
1832 "(",
1833 ")",
1834 "()",
1835 "((((",
1836 "0",
1837 "-0",
1838 "+0",
1839 "NaN",
1840 "nan",
1841 "inf",
1842 "-inf",
1843 "1e40",
1844 "-1e40",
1845 "1e-45",
1846 "3.4028235e38",
1847 "9223372036854775807",
1848 "-9223372036854775808",
1849 "\u{1F600}",
1850 "e\u{0301}\u{0301}\u{0301}",
1851 "\u{00a0}",
1852 "red\u{00a0}50%",
1853 " valid ",
1854 "valid;garbage",
1855 "red;blue",
1856 "linear-gradient",
1857 "linear-gradient(",
1858 "linear-gradient()",
1859 "url(",
1860 "url()",
1861 "rgba(",
1862 "rgb(0,0,0",
1863 "to right",
1864 "circle",
1865 "from",
1866 ];
1867
1868 const ALL_SYSTEM_REFS: [SystemColorRef; 9] = [
1869 SystemColorRef::Text,
1870 SystemColorRef::Background,
1871 SystemColorRef::Accent,
1872 SystemColorRef::AccentText,
1873 SystemColorRef::ButtonFace,
1874 SystemColorRef::ButtonText,
1875 SystemColorRef::WindowBackground,
1876 SystemColorRef::SelectionBackground,
1877 SystemColorRef::SelectionText,
1878 ];
1879
1880 const ALL_GRADIENT_TYPES: [GradientType; 6] = [
1881 GradientType::LinearGradient,
1882 GradientType::RepeatingLinearGradient,
1883 GradientType::RadialGradient,
1884 GradientType::RepeatingRadialGradient,
1885 GradientType::ConicGradient,
1886 GradientType::RepeatingConicGradient,
1887 ];
1888
1889 fn blue() -> ColorU {
1890 ColorU::new_rgb(0, 0, 255)
1891 }
1892
1893 fn linear(input: &str) -> LinearGradient {
1894 match parse_style_background_content(input) {
1895 Ok(StyleBackgroundContent::LinearGradient(g)) => g,
1896 other => panic!("expected a linear gradient for {input:?}, got {other:?}"),
1897 }
1898 }
1899
1900 fn radial(input: &str) -> RadialGradient {
1901 match parse_style_background_content(input) {
1902 Ok(StyleBackgroundContent::RadialGradient(g)) => g,
1903 other => panic!("expected a radial gradient for {input:?}, got {other:?}"),
1904 }
1905 }
1906
1907 fn conic(input: &str) -> ConicGradient {
1908 match parse_style_background_content(input) {
1909 Ok(StyleBackgroundContent::ConicGradient(g)) => g,
1910 other => panic!("expected a conic gradient for {input:?}, got {other:?}"),
1911 }
1912 }
1913
1914 fn offsets(stops: &NormalizedLinearColorStopVec) -> Vec<f32> {
1916 stops
1917 .iter()
1918 .map(|s| s.offset.normalized() * 100.0)
1919 .collect()
1920 }
1921
1922 #[test]
1927 fn autotest_shape_display_is_exact_and_never_empty() {
1928 assert_eq!(Shape::Ellipse.to_string(), "ellipse");
1929 assert_eq!(Shape::Circle.to_string(), "circle");
1930 assert_eq!(Shape::default(), Shape::Ellipse);
1931 assert_eq!(Shape::default().to_string(), "ellipse");
1932 for s in [Shape::Ellipse, Shape::Circle] {
1933 assert!(!s.to_string().is_empty());
1934 assert_eq!(parse_shape(&s.to_string()).unwrap(), s);
1936 }
1937 }
1938
1939 #[test]
1940 fn autotest_radial_gradient_size_display_is_exact_and_never_empty() {
1941 assert_eq!(RadialGradientSize::ClosestSide.to_string(), "closest-side");
1942 assert_eq!(
1943 RadialGradientSize::ClosestCorner.to_string(),
1944 "closest-corner"
1945 );
1946 assert_eq!(
1947 RadialGradientSize::FarthestSide.to_string(),
1948 "farthest-side"
1949 );
1950 assert_eq!(
1951 RadialGradientSize::FarthestCorner.to_string(),
1952 "farthest-corner"
1953 );
1954 assert_eq!(
1955 RadialGradientSize::default(),
1956 RadialGradientSize::FarthestCorner
1957 );
1958 for s in [
1959 RadialGradientSize::ClosestSide,
1960 RadialGradientSize::ClosestCorner,
1961 RadialGradientSize::FarthestSide,
1962 RadialGradientSize::FarthestCorner,
1963 ] {
1964 assert!(!s.to_string().is_empty());
1965 assert_eq!(parse_radial_gradient_size(&s.to_string()).unwrap(), s);
1966 }
1967 }
1968
1969 #[test]
1974 fn autotest_normalized_linear_stop_new_keeps_its_arguments() {
1975 let stop = NormalizedLinearColorStop::new(PercentageValue::new(42.5), ColorU::RED);
1976 assert_eq!(stop.offset.normalized() * 100.0, 42.5);
1977 assert_eq!(stop.color, ColorOrSystem::Color(ColorU::RED));
1978
1979 for f in [
1982 0.0_f32,
1983 -0.0,
1984 f32::NAN,
1985 f32::INFINITY,
1986 f32::NEG_INFINITY,
1987 f32::MAX,
1988 f32::MIN,
1989 f32::MIN_POSITIVE,
1990 -100.0,
1991 1e30,
1992 ] {
1993 let stop =
1994 NormalizedLinearColorStop::new(PercentageValue::new(f), ColorU::TRANSPARENT);
1995 assert!(
1996 stop.offset.normalized().is_finite(),
1997 "offset went non-finite for {f}"
1998 );
1999 assert_eq!(stop.color, ColorOrSystem::Color(ColorU::TRANSPARENT));
2000 }
2001 assert_eq!(
2003 NormalizedLinearColorStop::new(PercentageValue::new(f32::NAN), ColorU::RED).offset,
2004 PercentageValue::new(0.0)
2005 );
2006 }
2007
2008 #[test]
2009 fn autotest_normalized_radial_stop_new_keeps_its_arguments() {
2010 let stop = NormalizedRadialColorStop::new(AngleValue::deg(90.0), ColorU::RED);
2011 assert_eq!(stop.angle, AngleValue::deg(90.0));
2012 assert_eq!(stop.angle.to_degrees_raw(), 90.0);
2013 assert_eq!(stop.color, ColorOrSystem::Color(ColorU::RED));
2014
2015 for f in [
2016 0.0_f32,
2017 -0.0,
2018 f32::NAN,
2019 f32::INFINITY,
2020 f32::NEG_INFINITY,
2021 f32::MAX,
2022 f32::MIN,
2023 720.0,
2024 -360.0,
2025 ] {
2026 let stop = NormalizedRadialColorStop::new(AngleValue::deg(f), ColorU::WHITE);
2027 assert!(
2028 stop.angle.to_degrees_raw().is_finite(),
2029 "angle went non-finite for {f}"
2030 );
2031 assert_eq!(stop.color, ColorOrSystem::Color(ColorU::WHITE));
2032 }
2033 assert_eq!(
2034 NormalizedRadialColorStop::new(AngleValue::deg(f32::NAN), ColorU::RED).angle,
2035 AngleValue::deg(0.0)
2036 );
2037 }
2038
2039 #[test]
2044 fn autotest_resolve_concrete_color_ignores_system_colors() {
2045 let stop = NormalizedLinearColorStop::new(PercentageValue::new(0.0), ColorU::RED);
2046 assert_eq!(
2047 stop.resolve(&SystemColors::default(), ColorU::WHITE),
2048 ColorU::RED
2049 );
2050
2051 let populated = SystemColors {
2052 accent: OptionColorU::Some(ColorU::new_rgb(1, 2, 3)),
2053 ..SystemColors::default()
2054 };
2055 assert_eq!(stop.resolve(&populated, ColorU::WHITE), ColorU::RED);
2056
2057 let rstop = NormalizedRadialColorStop::new(AngleValue::deg(0.0), ColorU::RED);
2058 assert_eq!(rstop.resolve(&populated, ColorU::WHITE), ColorU::RED);
2059 }
2060
2061 #[test]
2062 fn autotest_resolve_system_stop_falls_back_for_every_variant() {
2063 let fallback = ColorU::rgba(9, 8, 7, 6);
2064 for r in ALL_SYSTEM_REFS {
2065 let lin = NormalizedLinearColorStop {
2066 offset: PercentageValue::new(50.0),
2067 color: ColorOrSystem::System(r),
2068 };
2069 let rad = NormalizedRadialColorStop {
2070 angle: AngleValue::deg(180.0),
2071 color: ColorOrSystem::System(r),
2072 };
2073 assert_eq!(lin.resolve(&SystemColors::default(), fallback), fallback);
2075 assert_eq!(rad.resolve(&SystemColors::default(), fallback), fallback);
2076 }
2077
2078 let accent = ColorU::new_rgb(0, 122, 255);
2080 let populated = SystemColors {
2081 accent: OptionColorU::Some(accent),
2082 ..SystemColors::default()
2083 };
2084 let stop = NormalizedLinearColorStop {
2085 offset: PercentageValue::new(0.0),
2086 color: ColorOrSystem::System(SystemColorRef::Accent),
2087 };
2088 assert_eq!(stop.resolve(&populated, fallback), accent);
2089 let other = NormalizedLinearColorStop {
2090 offset: PercentageValue::new(0.0),
2091 color: ColorOrSystem::System(SystemColorRef::ButtonText),
2092 };
2093 assert_eq!(other.resolve(&populated, fallback), fallback);
2094 }
2095
2096 #[test]
2101 fn autotest_background_position_horizontal_scale_for_dpi() {
2102 for f in [
2104 0.0_f32,
2105 1.0,
2106 -1.0,
2107 f32::NAN,
2108 f32::INFINITY,
2109 f32::MIN,
2110 f32::MAX,
2111 ] {
2112 for keyword in [
2113 BackgroundPositionHorizontal::Left,
2114 BackgroundPositionHorizontal::Center,
2115 BackgroundPositionHorizontal::Right,
2116 ] {
2117 let mut k = keyword;
2118 k.scale_for_dpi(f);
2119 assert_eq!(k, keyword, "keyword mutated by scale factor {f}");
2120 }
2121 }
2122
2123 let mut exact = BackgroundPositionHorizontal::Exact(PixelValue::px(10.0));
2124 exact.scale_for_dpi(2.0);
2125 assert_eq!(
2126 exact,
2127 BackgroundPositionHorizontal::Exact(PixelValue::px(20.0))
2128 );
2129
2130 let mut zeroed = BackgroundPositionHorizontal::Exact(PixelValue::px(10.0));
2132 zeroed.scale_for_dpi(0.0);
2133 assert_eq!(
2134 zeroed,
2135 BackgroundPositionHorizontal::Exact(PixelValue::px(0.0))
2136 );
2137
2138 let mut negated = BackgroundPositionHorizontal::Exact(PixelValue::px(10.0));
2139 negated.scale_for_dpi(-1.0);
2140 assert_eq!(
2141 negated,
2142 BackgroundPositionHorizontal::Exact(PixelValue::px(-10.0))
2143 );
2144
2145 let mut nan = BackgroundPositionHorizontal::Exact(PixelValue::px(10.0));
2147 nan.scale_for_dpi(f32::NAN);
2148 assert_eq!(
2149 nan,
2150 BackgroundPositionHorizontal::Exact(PixelValue::px(0.0))
2151 );
2152
2153 for f in [f32::INFINITY, f32::NEG_INFINITY, f32::MAX, f32::MIN] {
2155 let mut v = BackgroundPositionHorizontal::Exact(PixelValue::px(10.0));
2156 v.scale_for_dpi(f);
2157 let BackgroundPositionHorizontal::Exact(px) = v else {
2158 panic!("variant changed under scaling");
2159 };
2160 assert!(px.number.get().is_finite(), "non-finite result for {f}");
2161 assert_eq!(px.number.get().is_sign_negative(), f.is_sign_negative());
2162 }
2163 }
2164
2165 #[test]
2166 fn autotest_background_position_vertical_scale_for_dpi() {
2167 for f in [
2168 0.0_f32,
2169 1.0,
2170 -1.0,
2171 f32::NAN,
2172 f32::INFINITY,
2173 f32::MIN,
2174 f32::MAX,
2175 ] {
2176 for keyword in [
2177 BackgroundPositionVertical::Top,
2178 BackgroundPositionVertical::Center,
2179 BackgroundPositionVertical::Bottom,
2180 ] {
2181 let mut k = keyword;
2182 k.scale_for_dpi(f);
2183 assert_eq!(k, keyword, "keyword mutated by scale factor {f}");
2184 }
2185 }
2186
2187 let mut exact = BackgroundPositionVertical::Exact(PixelValue::em(4.0));
2188 exact.scale_for_dpi(0.5);
2189 assert_eq!(
2190 exact,
2191 BackgroundPositionVertical::Exact(PixelValue::em(2.0))
2192 );
2193
2194 let mut nan = BackgroundPositionVertical::Exact(PixelValue::px(10.0));
2195 nan.scale_for_dpi(f32::NAN);
2196 assert_eq!(nan, BackgroundPositionVertical::Exact(PixelValue::px(0.0)));
2197
2198 let mut saturated = BackgroundPositionVertical::Exact(PixelValue::px(f32::MAX));
2201 saturated.scale_for_dpi(f32::MAX);
2202 let once = saturated;
2203 saturated.scale_for_dpi(f32::MAX);
2204 assert_eq!(saturated, once);
2205 let BackgroundPositionVertical::Exact(px) = saturated else {
2206 panic!("variant changed under scaling");
2207 };
2208 assert!(px.number.get() > 0.0);
2209 assert!(px.number.get().is_finite());
2210 }
2211
2212 #[test]
2213 fn autotest_style_background_position_scale_for_dpi_scales_both_axes() {
2214 let mut pos = StyleBackgroundPosition {
2215 horizontal: BackgroundPositionHorizontal::Exact(PixelValue::px(10.0)),
2216 vertical: BackgroundPositionVertical::Exact(PixelValue::px(20.0)),
2217 };
2218 pos.scale_for_dpi(3.0);
2219 assert_eq!(
2220 pos.horizontal,
2221 BackgroundPositionHorizontal::Exact(PixelValue::px(30.0))
2222 );
2223 assert_eq!(
2224 pos.vertical,
2225 BackgroundPositionVertical::Exact(PixelValue::px(60.0))
2226 );
2227
2228 pos.scale_for_dpi(2.0);
2231 assert_eq!(
2232 pos.horizontal,
2233 BackgroundPositionHorizontal::Exact(PixelValue::px(60.0))
2234 );
2235
2236 for f in [
2238 0.0_f32,
2239 1.0,
2240 -2.5,
2241 f32::NAN,
2242 f32::INFINITY,
2243 f32::NEG_INFINITY,
2244 ] {
2245 let mut default = StyleBackgroundPosition::default();
2246 default.scale_for_dpi(f);
2247 assert_eq!(default, StyleBackgroundPosition::default());
2248 }
2249 }
2250
2251 #[test]
2252 fn autotest_style_background_size_scale_for_dpi() {
2253 for f in [
2255 0.0_f32,
2256 2.0,
2257 -1.0,
2258 f32::NAN,
2259 f32::INFINITY,
2260 f32::NEG_INFINITY,
2261 ] {
2262 for keyword in [StyleBackgroundSize::Contain, StyleBackgroundSize::Cover] {
2263 let mut k = keyword;
2264 k.scale_for_dpi(f);
2265 assert_eq!(k, keyword, "keyword mutated by scale factor {f}");
2266 }
2267 }
2268
2269 let mut size = StyleBackgroundSize::ExactSize(PixelValueSize {
2270 width: PixelValue::px(10.0),
2271 height: PixelValue::percent(50.0),
2272 });
2273 size.scale_for_dpi(2.0);
2274 assert_eq!(
2275 size,
2276 StyleBackgroundSize::ExactSize(PixelValueSize {
2277 width: PixelValue::px(20.0),
2278 height: PixelValue::percent(100.0),
2281 })
2282 );
2283
2284 let mut nan = StyleBackgroundSize::ExactSize(PixelValueSize {
2285 width: PixelValue::px(10.0),
2286 height: PixelValue::px(20.0),
2287 });
2288 nan.scale_for_dpi(f32::NAN);
2289 assert_eq!(
2290 nan,
2291 StyleBackgroundSize::ExactSize(PixelValueSize {
2292 width: PixelValue::px(0.0),
2293 height: PixelValue::px(0.0),
2294 })
2295 );
2296
2297 let mut inf = StyleBackgroundSize::ExactSize(PixelValueSize {
2298 width: PixelValue::px(1.0),
2299 height: PixelValue::px(-1.0),
2300 });
2301 inf.scale_for_dpi(f32::INFINITY);
2302 let StyleBackgroundSize::ExactSize(s) = inf else {
2303 panic!("variant changed under scaling");
2304 };
2305 assert!(s.width.number.get().is_finite() && s.width.number.get() > 0.0);
2306 assert!(s.height.number.get().is_finite() && s.height.number.get() < 0.0);
2307 }
2308
2309 #[test]
2314 fn autotest_css_background_parse_error_round_trips() {
2315 let errors = [
2316 CssBackgroundParseError::Error(""),
2317 CssBackgroundParseError::Error("boom \u{1F600}"),
2318 CssBackgroundParseError::InvalidBackground(ParenthesisParseError::EmptyInput),
2319 CssBackgroundParseError::InvalidBackground(
2320 ParenthesisParseError::StopWordNotFound("nope"),
2321 ),
2322 CssBackgroundParseError::UnclosedGradient(""),
2323 CssBackgroundParseError::NoDirection("nodir"),
2324 CssBackgroundParseError::TooFewGradientStops("few"),
2325 CssBackgroundParseError::DirectionParseError(CssDirectionParseError::Error("d")),
2326 CssBackgroundParseError::DirectionParseError(
2327 CssDirectionParseError::InvalidArguments("args"),
2328 ),
2329 CssBackgroundParseError::GradientParseError(CssGradientStopParseError::Error("g")),
2330 CssBackgroundParseError::ConicGradient(CssConicGradientParseError::NoAngle("a")),
2331 CssBackgroundParseError::ShapeParseError(CssShapeParseError::ShapeErr(
2332 InvalidValueErr("s"),
2333 )),
2334 CssBackgroundParseError::ImageParseError(CssImageParseError::UnclosedQuotes("q")),
2335 CssBackgroundParseError::ColorParseError(CssColorParseError::InvalidColor("c")),
2336 CssBackgroundParseError::ColorParseError(CssColorParseError::EmptyInput),
2337 ];
2338 for e in &errors {
2339 let owned = e.to_contained();
2340 assert_eq!(&owned.to_shared(), e, "round-trip changed {e:?}");
2341 assert_eq!(
2343 alloc::format!("{}", owned.to_shared()),
2344 alloc::format!("{e}")
2345 );
2346 }
2347 }
2348
2349 #[test]
2350 fn autotest_error_round_trip_survives_huge_and_unicode_payloads() {
2351 let huge = "x".repeat(100_000);
2352 let weird = "\u{1F600}\u{0}\u{00a0}e\u{0301}";
2353 for s in [huge.as_str(), weird, "", " "] {
2354 let e = CssBackgroundParseError::UnclosedGradient(s);
2355 assert_eq!(e.to_contained().to_shared(), e);
2356
2357 let e = CssGradientStopParseError::Error(s);
2358 assert_eq!(e.to_contained().to_shared(), e);
2359
2360 let e = CssConicGradientParseError::NoAngle(s);
2361 assert_eq!(e.to_contained().to_shared(), e);
2362
2363 let e = CssShapeParseError::ShapeErr(InvalidValueErr(s));
2364 assert_eq!(e.to_contained().to_shared(), e);
2365
2366 let e = CssBackgroundPositionParseError::NoPosition(s);
2367 assert_eq!(e.to_contained().to_shared(), e);
2368 }
2369 }
2370
2371 #[test]
2372 fn autotest_css_gradient_stop_parse_error_round_trips() {
2373 let errors = [
2374 CssGradientStopParseError::Error("boom"),
2375 CssGradientStopParseError::Percentage(PercentageParseError::NoPercentSign),
2376 CssGradientStopParseError::Percentage(PercentageParseError::InvalidUnit(
2377 "px".to_string().into(),
2378 )),
2379 CssGradientStopParseError::Angle(CssAngleValueParseError::EmptyString),
2380 CssGradientStopParseError::Angle(CssAngleValueParseError::InvalidAngle("q")),
2381 CssGradientStopParseError::ColorParseError(CssColorParseError::InvalidColor("c")),
2382 ];
2383 for e in &errors {
2384 assert_eq!(&e.to_contained().to_shared(), e, "round-trip changed {e:?}");
2385 }
2386 }
2387
2388 #[test]
2389 fn autotest_css_conic_and_shape_parse_error_round_trip() {
2390 let errors = [
2391 CssConicGradientParseError::NoAngle("n"),
2392 CssConicGradientParseError::Angle(CssAngleValueParseError::EmptyString),
2393 CssConicGradientParseError::Position(CssBackgroundPositionParseError::NoPosition(
2394 "p",
2395 )),
2396 ];
2397 for e in &errors {
2398 assert_eq!(&e.to_contained().to_shared(), e);
2399 }
2400
2401 let shape = CssShapeParseError::ShapeErr(InvalidValueErr("blob"));
2402 assert_eq!(shape.to_contained().to_shared(), shape);
2403 }
2404
2405 #[test]
2406 fn autotest_css_background_position_parse_error_round_trips() {
2407 let errors = [
2408 CssBackgroundPositionParseError::NoPosition(""),
2409 CssBackgroundPositionParseError::TooManyComponents("a b c"),
2410 CssBackgroundPositionParseError::FirstComponentWrong(
2411 CssPixelValueParseError::EmptyString,
2412 ),
2413 CssBackgroundPositionParseError::FirstComponentWrong(
2414 CssPixelValueParseError::InvalidPixelValue("q"),
2415 ),
2416 CssBackgroundPositionParseError::SecondComponentWrong(
2417 CssPixelValueParseError::InvalidPixelValue("\u{1F600}"),
2418 ),
2419 ];
2420 for e in &errors {
2421 assert_eq!(&e.to_contained().to_shared(), e, "round-trip changed {e:?}");
2422 }
2423 }
2424
2425 #[test]
2426 fn autotest_real_parse_errors_round_trip_through_the_owned_form() {
2427 for input in ADVERSARIAL {
2429 if let Err(e) = parse_style_background_content(input) {
2430 assert_eq!(e.to_contained().to_shared(), e, "for input {input:?}");
2431 }
2432 if let Err(e) = parse_style_background_position(input) {
2433 assert_eq!(e.to_contained().to_shared(), e, "for input {input:?}");
2434 }
2435 }
2436 }
2437
2438 #[test]
2443 fn autotest_get_extend_mode_is_repeat_exactly_for_the_repeating_variants() {
2444 assert_eq!(
2445 GradientType::LinearGradient.get_extend_mode(),
2446 ExtendMode::Clamp
2447 );
2448 assert_eq!(
2449 GradientType::RadialGradient.get_extend_mode(),
2450 ExtendMode::Clamp
2451 );
2452 assert_eq!(
2453 GradientType::ConicGradient.get_extend_mode(),
2454 ExtendMode::Clamp
2455 );
2456 assert_eq!(
2457 GradientType::RepeatingLinearGradient.get_extend_mode(),
2458 ExtendMode::Repeat
2459 );
2460 assert_eq!(
2461 GradientType::RepeatingRadialGradient.get_extend_mode(),
2462 ExtendMode::Repeat
2463 );
2464 assert_eq!(
2465 GradientType::RepeatingConicGradient.get_extend_mode(),
2466 ExtendMode::Repeat
2467 );
2468 for t in ALL_GRADIENT_TYPES {
2470 assert_eq!(t.get_extend_mode(), t.get_extend_mode());
2471 }
2472 assert_eq!(ExtendMode::default(), ExtendMode::Clamp);
2473 }
2474
2475 #[test]
2480 fn autotest_background_content_never_panics_and_is_deterministic() {
2481 for input in ADVERSARIAL {
2482 let a = parse_style_background_content(input);
2483 let b = parse_style_background_content(input);
2484 assert_eq!(a, b, "non-deterministic for {input:?}");
2485 }
2486 }
2487
2488 #[test]
2489 fn autotest_background_content_rejects_empty_whitespace_and_garbage() {
2490 for input in [
2491 "",
2492 " ",
2493 " ",
2494 "\t\n\r",
2495 "\u{0}",
2496 "!!!",
2497 ";",
2498 "valid;garbage",
2499 ] {
2500 assert!(
2501 parse_style_background_content(input).is_err(),
2502 "{input:?} should not parse as a background"
2503 );
2504 }
2505 }
2506
2507 #[test]
2508 fn autotest_background_content_valid_minimal_positive_controls() {
2509 assert_eq!(
2510 parse_style_background_content("red").unwrap(),
2511 StyleBackgroundContent::Color(ColorU::RED)
2512 );
2513 assert_eq!(
2515 parse_style_background_content(" red ").unwrap(),
2516 StyleBackgroundContent::Color(ColorU::RED)
2517 );
2518 assert_eq!(
2519 parse_style_background_content("system:accent").unwrap(),
2520 StyleBackgroundContent::SystemColor(SystemColorRef::Accent)
2521 );
2522 assert_eq!(
2523 parse_style_background_content("url(a.png)").unwrap(),
2524 StyleBackgroundContent::Image("a.png".into())
2525 );
2526 }
2527
2528 #[test]
2529 fn autotest_background_content_unicode_is_rejected_without_panicking() {
2530 for input in [
2531 "\u{1F600}",
2532 "url(\u{1F600}.png)",
2533 "linear-gradient(\u{1F600}, red)",
2534 "e\u{0301}\u{0301}\u{0301}",
2535 "\u{00a0}",
2536 ] {
2537 let parsed = parse_style_background_content(input);
2538 if input.starts_with("url(") {
2540 assert!(parsed.is_ok());
2541 } else {
2542 assert!(parsed.is_err(), "{input:?} unexpectedly parsed");
2543 }
2544 }
2545 }
2546
2547 #[test]
2548 fn autotest_background_content_extremely_long_input_terminates() {
2549 let huge = "a".repeat(100_000);
2550 assert!(parse_style_background_content(&huge).is_err());
2551
2552 let huge_gradient =
2553 alloc::format!("linear-gradient({})", "red, ".repeat(2_000) + "blue");
2554 let g = linear(&huge_gradient);
2555 assert_eq!(g.stops.len(), 2_001);
2556
2557 let huge_url = alloc::format!("url({})", "a".repeat(100_000));
2558 assert!(matches!(
2559 parse_style_background_content(&huge_url),
2560 Ok(StyleBackgroundContent::Image(_))
2561 ));
2562 }
2563
2564 #[test]
2565 fn autotest_background_content_deep_nesting_does_not_stack_overflow() {
2566 let nested = alloc::format!(
2567 "linear-gradient({}red{})",
2568 "(".repeat(10_000),
2569 ")".repeat(10_000)
2570 );
2571 assert!(parse_style_background_content(&nested).is_err());
2572
2573 let unbalanced = alloc::format!("linear-gradient({}", "(".repeat(10_000));
2574 assert!(parse_style_background_content(&unbalanced).is_err());
2575 }
2576
2577 #[test]
2578 fn autotest_unclosed_gradient_reports_a_color_error_not_unclosed_gradient() {
2579 let err = parse_style_background_content("linear-gradient(red, blue").unwrap_err();
2582 assert!(
2583 matches!(err, CssBackgroundParseError::ColorParseError(_)),
2584 "got {err:?}"
2585 );
2586 }
2587
2588 #[test]
2589 fn autotest_empty_gradient_body_is_a_no_direction_error() {
2590 let err = parse_style_background_content("linear-gradient()").unwrap_err();
2591 assert!(
2592 matches!(err, CssBackgroundParseError::NoDirection(_)),
2593 "got {err:?}"
2594 );
2595 for f in [
2596 "radial-gradient()",
2597 "conic-gradient()",
2598 "repeating-linear-gradient()",
2599 ] {
2600 assert!(parse_style_background_content(f).is_err(), "{f:?}");
2601 }
2602 }
2603
2604 #[test]
2605 fn autotest_url_with_empty_payload_is_accepted_as_an_empty_image() {
2606 assert_eq!(
2608 parse_style_background_content("url()").unwrap(),
2609 StyleBackgroundContent::Image("".into())
2610 );
2611 }
2612
2613 #[test]
2614 fn autotest_gradient_boundary_number_directions_stay_finite() {
2615 let g = linear("linear-gradient(NaN, red, blue)");
2618 assert_eq!(g.direction, Direction::Angle(AngleValue::deg(0.0)));
2619
2620 for input in [
2622 "linear-gradient(0deg, red, blue)",
2623 "linear-gradient(-0deg, red, blue)",
2624 "linear-gradient(1e40deg, red, blue)",
2625 "linear-gradient(-1e40deg, red, blue)",
2626 "linear-gradient(1e-45deg, red, blue)",
2627 "linear-gradient(inf, red, blue)",
2628 "linear-gradient(-inf, red, blue)",
2629 "linear-gradient(9223372036854775807deg, red, blue)",
2630 ] {
2631 let g = linear(input);
2632 let Direction::Angle(a) = g.direction else {
2633 panic!("expected an angle direction for {input:?}");
2634 };
2635 assert!(
2636 a.to_degrees_raw().is_finite(),
2637 "non-finite angle for {input:?}"
2638 );
2639 assert_eq!(g.stops.len(), 2, "{input:?}");
2640 }
2641 }
2642
2643 #[test]
2644 fn autotest_gradient_stop_offsets_are_monotonic_and_finite() {
2645 for input in [
2646 "linear-gradient(red, blue)",
2647 "linear-gradient(red, green, blue)",
2648 "linear-gradient(red 50%, blue 20%)",
2649 "linear-gradient(red -50%, blue)",
2650 "linear-gradient(red 0%, yellow, green, blue 100%)",
2651 "linear-gradient(red 10% 30%, blue)",
2652 "linear-gradient(red 200%, blue 10%)",
2653 "repeating-linear-gradient(red, blue 20%)",
2654 "radial-gradient(circle, red, blue)",
2655 ] {
2656 let content = parse_style_background_content(input).unwrap();
2657 let stops = match &content {
2658 StyleBackgroundContent::LinearGradient(g) => &g.stops,
2659 StyleBackgroundContent::RadialGradient(g) => &g.stops,
2660 other => panic!("unexpected content {other:?}"),
2661 };
2662 let mut prev = f32::NEG_INFINITY;
2663 for o in offsets(stops) {
2664 assert!(o.is_finite(), "non-finite offset in {input:?}");
2665 assert!(
2666 o >= prev,
2667 "offsets not monotonic in {input:?}: {o} < {prev}"
2668 );
2669 prev = o;
2670 }
2671 }
2672 }
2673
2674 #[test]
2675 fn autotest_negative_and_overflowing_stop_offsets_are_clamped() {
2676 let g = linear("linear-gradient(red -50%, blue)");
2678 assert_eq!(offsets(&g.stops), alloc::vec![0.0, 100.0]);
2679
2680 let g = linear("linear-gradient(red 200%, blue 10%)");
2683 assert_eq!(offsets(&g.stops), alloc::vec![200.0, 200.0]);
2684 }
2685
2686 #[test]
2687 fn autotest_offsets_that_are_not_percentages_are_rejected() {
2688 let err =
2691 parse_style_background_content("linear-gradient(red 50px, blue)").unwrap_err();
2692 assert!(
2693 matches!(
2694 err,
2695 CssBackgroundParseError::GradientParseError(
2696 CssGradientStopParseError::Percentage(_)
2697 )
2698 ),
2699 "got {err:?}"
2700 );
2701
2702 assert!(parse_style_background_content("linear-gradient(red 0.5, blue)").is_err());
2705 assert!(parse_style_background_content("linear-gradient(red NaN%, blue)").is_err());
2707 }
2708
2709 #[test]
2710 fn autotest_huge_stop_offsets_do_not_produce_nan_or_inf() {
2711 let g = linear("linear-gradient(red 1e40%, blue)");
2712 assert_eq!(g.stops.len(), 2);
2713 for o in offsets(&g.stops) {
2714 assert!(o.is_finite(), "offset leaked a non-finite value: {o}");
2715 }
2716 }
2717
2718 #[test]
2723 fn autotest_background_content_multiple_empty_input_yields_an_empty_vec() {
2724 let parsed = parse_style_background_content_multiple("").unwrap();
2727 assert_eq!(parsed.len(), 0);
2728
2729 assert!(parse_style_background_content_multiple(" ").is_err());
2731 assert!(parse_style_background_content_multiple(",").is_err());
2732 assert!(parse_style_background_content_multiple("red,,blue").is_err());
2733 }
2734
2735 #[test]
2736 fn autotest_background_content_multiple_valid_and_adversarial() {
2737 let parsed =
2738 parse_style_background_content_multiple("linear-gradient(red, blue), url(a.png)")
2739 .unwrap();
2740 assert_eq!(parsed.len(), 2);
2741 assert!(matches!(
2742 parsed.as_slice()[0],
2743 StyleBackgroundContent::LinearGradient(_)
2744 ));
2745 assert!(matches!(
2746 parsed.as_slice()[1],
2747 StyleBackgroundContent::Image(_)
2748 ));
2749
2750 assert!(parse_style_background_content_multiple("red, !!!").is_err());
2752
2753 let many = "red,".repeat(2_000) + "blue";
2755 assert_eq!(
2756 parse_style_background_content_multiple(&many)
2757 .unwrap()
2758 .len(),
2759 2_001
2760 );
2761
2762 for input in ADVERSARIAL {
2763 let a = parse_style_background_content_multiple(input);
2764 let b = parse_style_background_content_multiple(input);
2765 assert_eq!(a, b, "non-deterministic for {input:?}");
2766 }
2767 }
2768
2769 #[test]
2774 fn autotest_background_position_empty_and_whitespace() {
2775 assert_eq!(
2776 parse_style_background_position(""),
2777 Err(CssBackgroundPositionParseError::NoPosition(""))
2778 );
2779 assert_eq!(
2780 parse_style_background_position(" "),
2781 Err(CssBackgroundPositionParseError::NoPosition(""))
2782 );
2783 assert_eq!(
2784 parse_style_background_position("\t\n\r"),
2785 Err(CssBackgroundPositionParseError::NoPosition(""))
2786 );
2787 }
2788
2789 #[test]
2790 fn autotest_background_position_valid_minimal_and_keyword_order() {
2791 let p = parse_style_background_position("left").unwrap();
2792 assert_eq!(p.horizontal, BackgroundPositionHorizontal::Left);
2793 assert_eq!(p.vertical, BackgroundPositionVertical::Center);
2794
2795 let p = parse_style_background_position("top").unwrap();
2797 assert_eq!(p.horizontal, BackgroundPositionHorizontal::Center);
2798 assert_eq!(p.vertical, BackgroundPositionVertical::Top);
2799
2800 assert_eq!(
2802 parse_style_background_position("left top").unwrap(),
2803 parse_style_background_position("top left").unwrap()
2804 );
2805
2806 assert!(matches!(
2808 parse_style_background_position("left right"),
2809 Err(CssBackgroundPositionParseError::SecondComponentWrong(_))
2810 ));
2811 }
2812
2813 #[test]
2814 fn autotest_background_position_too_many_components() {
2815 assert!(matches!(
2816 parse_style_background_position("left 10px top 20px"),
2817 Err(CssBackgroundPositionParseError::TooManyComponents(_))
2818 ));
2819 assert!(matches!(
2820 parse_style_background_position("a b c"),
2821 Err(CssBackgroundPositionParseError::TooManyComponents(_))
2822 ));
2823 }
2824
2825 #[test]
2826 fn autotest_background_position_boundary_numbers_are_accepted_and_saturate() {
2827 assert_eq!(
2830 parse_style_background_position("NaN").unwrap().horizontal,
2831 BackgroundPositionHorizontal::Exact(PixelValue::px(0.0))
2832 );
2833 assert_eq!(
2834 parse_style_background_position("-0").unwrap().horizontal,
2835 BackgroundPositionHorizontal::Exact(PixelValue::px(0.0))
2836 );
2837 assert_eq!(
2838 parse_style_background_position("inf").unwrap().horizontal,
2839 BackgroundPositionHorizontal::Exact(PixelValue::px(f32::INFINITY))
2840 );
2841 for input in ["0", "1e40px", "-1e40px", "1e-45px", "3.4028235e38px"] {
2842 let p = parse_style_background_position(input).unwrap();
2843 let BackgroundPositionHorizontal::Exact(px) = p.horizontal else {
2844 panic!("expected an exact value for {input:?}");
2845 };
2846 assert!(px.number.get().is_finite(), "non-finite for {input:?}");
2847 }
2848 }
2849
2850 #[test]
2851 fn autotest_background_position_garbage_unicode_and_long_input() {
2852 for input in ["garbage", "!!!", "\u{1F600}", "e\u{0301}", "left;top"] {
2853 assert!(
2854 parse_style_background_position(input).is_err(),
2855 "{input:?} unexpectedly parsed"
2856 );
2857 }
2858 let huge = "a".repeat(100_000);
2859 assert!(parse_style_background_position(&huge).is_err());
2860 let nested = "(".repeat(10_000);
2861 assert!(parse_style_background_position(&nested).is_err());
2862
2863 for input in ADVERSARIAL {
2864 let a = parse_style_background_position(input);
2865 let b = parse_style_background_position(input);
2866 assert_eq!(a, b, "non-deterministic for {input:?}");
2867 }
2868 }
2869
2870 #[test]
2871 fn autotest_background_position_multiple() {
2872 assert_eq!(
2874 parse_style_background_position_multiple("").unwrap().len(),
2875 0
2876 );
2877
2878 let parsed = parse_style_background_position_multiple("left top, 10px 20px").unwrap();
2879 assert_eq!(parsed.len(), 2);
2880 assert_eq!(
2881 parsed.as_slice()[1].horizontal,
2882 BackgroundPositionHorizontal::Exact(PixelValue::px(10.0))
2883 );
2884
2885 assert!(parse_style_background_position_multiple("left top, !!!").is_err());
2886 assert!(parse_style_background_position_multiple(" ").is_err());
2887
2888 let many = "left top,".repeat(2_000) + "center";
2889 assert_eq!(
2890 parse_style_background_position_multiple(&many)
2891 .unwrap()
2892 .len(),
2893 2_001
2894 );
2895 }
2896
2897 #[test]
2902 fn autotest_background_size_empty_whitespace_and_garbage() {
2903 for input in [
2904 "",
2905 " ",
2906 "\t\n",
2907 "auto",
2908 "!!!",
2909 "\u{1F600}",
2910 "CONTAIN",
2911 "Cover",
2912 ] {
2913 assert!(
2914 parse_style_background_size(input).is_err(),
2915 "{input:?} unexpectedly parsed"
2916 );
2917 }
2918 let huge = "a".repeat(100_000);
2919 assert!(parse_style_background_size(&huge).is_err());
2920 }
2921
2922 #[test]
2923 fn autotest_background_size_valid_minimal_and_trimming() {
2924 assert_eq!(
2925 parse_style_background_size(" contain ").unwrap(),
2926 StyleBackgroundSize::Contain
2927 );
2928 assert_eq!(
2929 parse_style_background_size("cover").unwrap(),
2930 StyleBackgroundSize::Cover
2931 );
2932 assert_eq!(
2934 parse_style_background_size("50%").unwrap(),
2935 StyleBackgroundSize::ExactSize(PixelValueSize {
2936 width: PixelValue::percent(50.0),
2937 height: PixelValue::percent(50.0),
2938 })
2939 );
2940 }
2941
2942 #[test]
2943 fn autotest_background_size_silently_ignores_extra_components() {
2944 assert_eq!(
2947 parse_style_background_size("10px 20px 30px").unwrap(),
2948 StyleBackgroundSize::ExactSize(PixelValueSize {
2949 width: PixelValue::px(10.0),
2950 height: PixelValue::px(20.0),
2951 })
2952 );
2953 }
2954
2955 #[test]
2956 fn autotest_background_size_boundary_numbers_saturate_without_panicking() {
2957 assert_eq!(
2959 parse_style_background_size("NaN").unwrap(),
2960 StyleBackgroundSize::ExactSize(PixelValueSize {
2961 width: PixelValue::px(0.0),
2962 height: PixelValue::px(0.0),
2963 })
2964 );
2965 assert_eq!(
2966 parse_style_background_size("inf").unwrap(),
2967 StyleBackgroundSize::ExactSize(PixelValueSize {
2968 width: PixelValue::px(f32::INFINITY),
2969 height: PixelValue::px(f32::INFINITY),
2970 })
2971 );
2972 for input in ["0", "-0", "1e40px", "-1e40px", "1e-45px"] {
2973 let StyleBackgroundSize::ExactSize(s) = parse_style_background_size(input).unwrap()
2974 else {
2975 panic!("expected an exact size for {input:?}");
2976 };
2977 assert!(s.width.number.get().is_finite(), "non-finite for {input:?}");
2978 assert!(
2979 s.height.number.get().is_finite(),
2980 "non-finite for {input:?}"
2981 );
2982 }
2983
2984 for input in ADVERSARIAL {
2985 let a = parse_style_background_size(input);
2986 let b = parse_style_background_size(input);
2987 assert_eq!(a, b, "non-deterministic for {input:?}");
2988 }
2989 }
2990
2991 #[test]
2992 fn autotest_background_size_multiple() {
2993 assert_eq!(parse_style_background_size_multiple("").unwrap().len(), 0);
2994
2995 let parsed = parse_style_background_size_multiple("contain, 10px 20px, cover").unwrap();
2996 assert_eq!(parsed.len(), 3);
2997 assert_eq!(parsed.as_slice()[0], StyleBackgroundSize::Contain);
2998 assert_eq!(parsed.as_slice()[2], StyleBackgroundSize::Cover);
2999
3000 assert!(parse_style_background_size_multiple("cover, auto").is_err());
3001 assert!(parse_style_background_size_multiple(" ").is_err());
3002
3003 let many = "cover,".repeat(2_000) + "contain";
3004 assert_eq!(
3005 parse_style_background_size_multiple(&many).unwrap().len(),
3006 2_001
3007 );
3008 }
3009
3010 #[test]
3015 fn autotest_background_repeat_valid_and_invalid() {
3016 assert_eq!(
3017 parse_style_background_repeat(" repeat ").unwrap(),
3018 StyleBackgroundRepeat::PatternRepeat
3019 );
3020 assert_eq!(
3021 parse_style_background_repeat("no-repeat").unwrap(),
3022 StyleBackgroundRepeat::NoRepeat
3023 );
3024 assert_eq!(
3025 parse_style_background_repeat("repeat-x").unwrap(),
3026 StyleBackgroundRepeat::RepeatX
3027 );
3028 assert_eq!(
3029 parse_style_background_repeat("repeat-y").unwrap(),
3030 StyleBackgroundRepeat::RepeatY
3031 );
3032 assert_eq!(
3033 StyleBackgroundRepeat::default(),
3034 StyleBackgroundRepeat::PatternRepeat
3035 );
3036
3037 for input in [
3038 "",
3039 " ",
3040 "\t\n",
3041 "REPEAT",
3042 "Repeat",
3043 "repeat-xy",
3044 "repeat repeat",
3045 "!!!",
3046 "\u{1F600}",
3047 "0",
3048 "NaN",
3049 ] {
3050 assert!(
3051 parse_style_background_repeat(input).is_err(),
3052 "{input:?} unexpectedly parsed"
3053 );
3054 }
3055
3056 let huge = "repeat".repeat(20_000);
3057 assert!(parse_style_background_repeat(&huge).is_err());
3058
3059 for input in ADVERSARIAL {
3060 let a = parse_style_background_repeat(input);
3061 let b = parse_style_background_repeat(input);
3062 assert_eq!(a, b, "non-deterministic for {input:?}");
3063 }
3064 }
3065
3066 #[test]
3067 fn autotest_background_repeat_multiple() {
3068 assert_eq!(parse_style_background_repeat_multiple("").unwrap().len(), 0);
3069
3070 let parsed = parse_style_background_repeat_multiple("repeat, no-repeat").unwrap();
3071 assert_eq!(parsed.len(), 2);
3072 assert_eq!(parsed.as_slice()[0], StyleBackgroundRepeat::PatternRepeat);
3073 assert_eq!(parsed.as_slice()[1], StyleBackgroundRepeat::NoRepeat);
3074
3075 assert!(parse_style_background_repeat_multiple("repeat,,repeat").is_err());
3076 assert!(parse_style_background_repeat_multiple(" ").is_err());
3077
3078 let many = "repeat,".repeat(2_000) + "no-repeat";
3079 assert_eq!(
3080 parse_style_background_repeat_multiple(&many).unwrap().len(),
3081 2_001
3082 );
3083 }
3084
3085 #[test]
3090 fn autotest_parse_gradient_empty_input_is_no_direction_for_every_type() {
3091 for t in ALL_GRADIENT_TYPES {
3092 assert!(
3093 matches!(
3094 parse_gradient("", t),
3095 Err(CssBackgroundParseError::NoDirection(_))
3096 ),
3097 "empty body accepted for {t:?}"
3098 );
3099 }
3100 }
3101
3102 #[test]
3103 fn autotest_parse_gradient_extend_mode_follows_the_gradient_type() {
3104 let StyleBackgroundContent::LinearGradient(g) =
3105 parse_gradient("red, blue", GradientType::LinearGradient).unwrap()
3106 else {
3107 panic!("expected a linear gradient");
3108 };
3109 assert_eq!(g.extend_mode, ExtendMode::Clamp);
3110
3111 let StyleBackgroundContent::LinearGradient(g) =
3112 parse_gradient("red, blue", GradientType::RepeatingLinearGradient).unwrap()
3113 else {
3114 panic!("expected a linear gradient");
3115 };
3116 assert_eq!(g.extend_mode, ExtendMode::Repeat);
3117
3118 let StyleBackgroundContent::RadialGradient(g) =
3119 parse_gradient("red, blue", GradientType::RepeatingRadialGradient).unwrap()
3120 else {
3121 panic!("expected a radial gradient");
3122 };
3123 assert_eq!(g.extend_mode, ExtendMode::Repeat);
3124
3125 let StyleBackgroundContent::ConicGradient(g) =
3126 parse_gradient("red, blue", GradientType::RepeatingConicGradient).unwrap()
3127 else {
3128 panic!("expected a conic gradient");
3129 };
3130 assert_eq!(g.extend_mode, ExtendMode::Repeat);
3131 }
3132
3133 #[test]
3134 fn autotest_parse_gradient_accepts_gradients_with_too_few_stops() {
3135 let StyleBackgroundContent::LinearGradient(g) =
3138 parse_gradient("red", GradientType::LinearGradient).unwrap()
3139 else {
3140 panic!("expected a linear gradient");
3141 };
3142 assert_eq!(g.stops.len(), 1);
3143 assert_eq!(offsets(&g.stops), alloc::vec![0.0]);
3144
3145 let StyleBackgroundContent::LinearGradient(g) =
3147 parse_gradient("to right", GradientType::LinearGradient).unwrap()
3148 else {
3149 panic!("expected a linear gradient");
3150 };
3151 assert_eq!(g.stops.len(), 0);
3152
3153 let StyleBackgroundContent::RadialGradient(g) =
3155 parse_gradient("circle", GradientType::RadialGradient).unwrap()
3156 else {
3157 panic!("expected a radial gradient");
3158 };
3159 assert_eq!(g.shape, Shape::Circle);
3160 assert_eq!(g.stops.len(), 0);
3161 }
3162
3163 #[test]
3164 fn autotest_parse_gradient_never_panics_on_adversarial_input() {
3165 let huge = "a".repeat(100_000);
3166 let nested = "(".repeat(10_000) + &")".repeat(10_000);
3167 let many_commas = ",".repeat(10_000);
3168 for t in ALL_GRADIENT_TYPES {
3169 for input in ADVERSARIAL {
3170 let a = parse_gradient(input, t);
3171 let b = parse_gradient(input, t);
3172 assert_eq!(a, b, "non-deterministic for {input:?} / {t:?}");
3173 }
3174 for input in [huge.as_str(), nested.as_str(), many_commas.as_str()] {
3175 let a = parse_gradient(input, t);
3176 let b = parse_gradient(input, t);
3177 assert_eq!(a, b, "non-deterministic for a long input / {t:?}");
3178 }
3179 assert!(
3181 parse_gradient("", t).is_err(),
3182 "empty body accepted for {t:?}"
3183 );
3184 assert!(
3186 parse_gradient(&many_commas, t).is_err(),
3187 "comma soup accepted for {t:?}"
3188 );
3189 }
3190 for t in [
3193 GradientType::LinearGradient,
3194 GradientType::RepeatingLinearGradient,
3195 GradientType::ConicGradient,
3196 GradientType::RepeatingConicGradient,
3197 ] {
3198 assert!(parse_gradient(&huge, t).is_err(), "junk accepted for {t:?}");
3199 assert!(
3200 parse_gradient(&nested, t).is_err(),
3201 "junk accepted for {t:?}"
3202 );
3203 assert!(parse_gradient("!!!", t).is_err(), "junk accepted for {t:?}");
3204 }
3205 }
3206
3207 #[test]
3208 fn autotest_radial_gradient_silently_drops_unparseable_items() {
3209 let StyleBackgroundContent::RadialGradient(g) =
3214 parse_gradient("!!!", GradientType::RadialGradient).unwrap()
3215 else {
3216 panic!("expected a radial gradient");
3217 };
3218 assert_eq!(g.stops.len(), 0);
3219
3220 let g = radial("radial-gradient(!!!, red)");
3221 assert_eq!(g.stops.len(), 1, "the junk item should have been dropped");
3222 assert_eq!(g.stops.as_ref()[0].color, ColorOrSystem::Color(ColorU::RED));
3223
3224 assert!(parse_style_background_content("linear-gradient(!!!, red)").is_err());
3226 }
3227
3228 #[test]
3229 fn autotest_radial_gradient_position_is_ignored_when_combined_with_a_shape() {
3230 let g = radial("radial-gradient(circle at 50% 50%, red, blue)");
3235 assert_eq!(g.shape, Shape::Circle);
3236 assert_eq!(g.position, StyleBackgroundPosition::default());
3237 assert_eq!(g.stops.len(), 2);
3238
3239 let g = radial("radial-gradient(50% 50%, red, blue)");
3241 assert_eq!(
3242 g.position,
3243 StyleBackgroundPosition {
3244 horizontal: BackgroundPositionHorizontal::Exact(PixelValue::percent(50.0)),
3245 vertical: BackgroundPositionVertical::Exact(PixelValue::percent(50.0)),
3246 }
3247 );
3248 assert_eq!(g.stops.len(), 2);
3249 }
3250
3251 #[test]
3252 fn autotest_radial_gradient_shape_and_size_keywords() {
3253 let g = radial("radial-gradient(circle closest-side, red, blue)");
3254 assert_eq!(g.shape, Shape::Circle);
3255 assert_eq!(g.size, RadialGradientSize::ClosestSide);
3256
3257 let g = radial("radial-gradient(ellipse farthest-side, red, blue)");
3258 assert_eq!(g.shape, Shape::Ellipse);
3259 assert_eq!(g.size, RadialGradientSize::FarthestSide);
3260
3261 let g = radial("radial-gradient(red, blue)");
3263 assert_eq!(g.shape, Shape::default());
3264 assert_eq!(g.size, RadialGradientSize::default());
3265 }
3266
3267 #[test]
3272 fn autotest_parse_linear_color_stop_valid_minimal() {
3273 let s = parse_linear_color_stop("red").unwrap();
3274 assert_eq!(s.color, ColorOrSystem::Color(ColorU::RED));
3275 assert_eq!(s.offset1, OptionPercentageValue::None);
3276 assert_eq!(s.offset2, OptionPercentageValue::None);
3277
3278 let s = parse_linear_color_stop(" red 50% ").unwrap();
3279 assert_eq!(
3280 s.offset1,
3281 OptionPercentageValue::Some(PercentageValue::new(50.0))
3282 );
3283 assert_eq!(s.offset2, OptionPercentageValue::None);
3284
3285 let s = parse_linear_color_stop("red 10% 30%").unwrap();
3286 assert_eq!(
3287 s.offset1,
3288 OptionPercentageValue::Some(PercentageValue::new(10.0))
3289 );
3290 assert_eq!(
3291 s.offset2,
3292 OptionPercentageValue::Some(PercentageValue::new(30.0))
3293 );
3294
3295 let s = parse_linear_color_stop("rgba(0, 0, 0, 0.5) 50%").unwrap();
3297 assert_eq!(
3298 s.offset1,
3299 OptionPercentageValue::Some(PercentageValue::new(50.0))
3300 );
3301
3302 let s = parse_linear_color_stop("system:accent 50%").unwrap();
3304 assert_eq!(s.color, ColorOrSystem::System(SystemColorRef::Accent));
3305 }
3306
3307 #[test]
3308 fn autotest_parse_linear_color_stop_rejects_junk() {
3309 for input in [
3310 "",
3311 " ",
3312 "\t\n",
3313 "!!!",
3314 "\u{1F600}",
3315 "red 50px", "red 0.5", "red 10% 20% 30%", "red blue",
3319 ] {
3320 assert!(
3321 parse_linear_color_stop(input).is_err(),
3322 "{input:?} unexpectedly parsed"
3323 );
3324 }
3325 let huge = "a".repeat(100_000);
3326 assert!(parse_linear_color_stop(&huge).is_err());
3327 }
3328
3329 #[test]
3330 fn autotest_parse_radial_color_stop_valid_and_junk() {
3331 let s = parse_radial_color_stop("red").unwrap();
3332 assert_eq!(s.color, ColorOrSystem::Color(ColorU::RED));
3333 assert_eq!(s.offset1, OptionAngleValue::None);
3334
3335 let s = parse_radial_color_stop("red 90deg").unwrap();
3336 assert_eq!(s.offset1, OptionAngleValue::Some(AngleValue::deg(90.0)));
3337 assert_eq!(s.offset2, OptionAngleValue::None);
3338
3339 let s = parse_radial_color_stop("red 45deg 90deg").unwrap();
3340 assert_eq!(s.offset1, OptionAngleValue::Some(AngleValue::deg(45.0)));
3341 assert_eq!(s.offset2, OptionAngleValue::Some(AngleValue::deg(90.0)));
3342
3343 assert!(parse_radial_color_stop("red 50%").is_ok());
3345
3346 for input in [
3347 "",
3348 " ",
3349 "!!!",
3350 "\u{1F600}",
3351 "red 5",
3352 "red 90deg 45deg 10deg",
3353 ] {
3354 assert!(
3355 parse_radial_color_stop(input).is_err(),
3356 "{input:?} unexpectedly parsed"
3357 );
3358 }
3359 let huge = "a".repeat(100_000);
3360 assert!(parse_radial_color_stop(&huge).is_err());
3361 }
3362
3363 #[test]
3368 fn autotest_split_color_and_offsets_w3c_shapes() {
3369 assert_eq!(split_color_and_offsets("red"), ("red", None, None));
3370 assert_eq!(
3371 split_color_and_offsets("red 50%"),
3372 ("red", Some("50%"), None)
3373 );
3374 assert_eq!(
3375 split_color_and_offsets("red 10% 30%"),
3376 ("red", Some("10%"), Some("30%"))
3377 );
3378 assert_eq!(
3379 split_color_and_offsets("rgba(0, 0, 0, 0.5) 10% 30%"),
3380 ("rgba(0, 0, 0, 0.5)", Some("10%"), Some("30%"))
3381 );
3382 assert_eq!(
3384 split_color_and_offsets("to right bottom"),
3385 ("to right bottom", None, None)
3386 );
3387 }
3388
3389 #[test]
3390 fn autotest_split_color_and_offsets_never_panics_on_edges() {
3391 assert_eq!(split_color_and_offsets(""), ("", None, None));
3392 assert_eq!(split_color_and_offsets(" "), ("", None, None));
3393 assert_eq!(
3395 split_color_and_offsets("\u{1F600} 50%"),
3396 ("\u{1F600}", Some("50%"), None)
3397 );
3398 assert_eq!(
3400 split_color_and_offsets("red\u{00a0}50%"),
3401 ("red", Some("50%"), None)
3402 );
3403 let huge = "a".repeat(100_000);
3404 assert_eq!(split_color_and_offsets(&huge), (huge.as_str(), None, None));
3405
3406 for input in ADVERSARIAL {
3407 assert_eq!(
3408 split_color_and_offsets(input),
3409 split_color_and_offsets(input)
3410 );
3411 }
3412 }
3413
3414 #[test]
3415 fn autotest_try_split_last_offset() {
3416 assert_eq!(try_split_last_offset("red 50%"), Some(("red", "50%")));
3417 assert_eq!(try_split_last_offset("red 10px"), Some(("red", "10px")));
3418 assert_eq!(try_split_last_offset("50%"), None);
3420 assert_eq!(try_split_last_offset("red blue"), None);
3422 assert_eq!(try_split_last_offset("red 5"), None);
3423 assert_eq!(try_split_last_offset("to right"), None);
3424 assert_eq!(try_split_last_offset(""), None);
3426 assert_eq!(try_split_last_offset(" "), None);
3427 assert_eq!(try_split_last_offset("\t\n"), None);
3428
3429 for input in ADVERSARIAL {
3430 assert_eq!(try_split_last_offset(input), try_split_last_offset(input));
3431 }
3432 }
3433
3434 #[test]
3439 fn autotest_is_likely_offset_basic_true_false() {
3440 for s in [
3441 "50%", "10px", "0.5turn", "90deg", "1rem", "2vmin", "3vmax", "4grad", "5rad",
3442 "-50%", "1e40%",
3443 ] {
3444 assert!(is_likely_offset(s), "{s:?} should look like an offset");
3445 }
3446 for s in [
3447 "",
3448 " ",
3449 "red",
3450 "px",
3451 "%",
3452 "5",
3453 "0.5",
3454 "NaN%",
3455 "to",
3456 "right",
3457 "\u{1F600}",
3458 "contain",
3459 ] {
3460 assert!(!is_likely_offset(s), "{s:?} should not look like an offset");
3461 }
3462 }
3463
3464 #[test]
3465 fn autotest_is_likely_offset_is_a_shape_check_not_a_validator() {
3466 assert!(is_likely_offset("abc1px"));
3469 assert!(is_likely_offset("\u{1F600}5%"));
3470 assert!(is_likely_offset("--1--px"));
3471 assert!(is_likely_offset("1%%%"));
3472 assert!(!is_likely_offset("\u{0661}%"));
3475
3476 let huge = "9".repeat(100_000) + "%";
3477 assert!(is_likely_offset(&huge));
3478 for input in ADVERSARIAL {
3479 assert_eq!(is_likely_offset(input), is_likely_offset(input));
3480 }
3481 }
3482
3483 #[test]
3488 fn autotest_parse_conic_first_item_valid_and_absent() {
3489 assert_eq!(parse_conic_first_item("").unwrap(), None);
3491 assert_eq!(parse_conic_first_item("red").unwrap(), None);
3492 assert_eq!(parse_conic_first_item(" ").unwrap(), None);
3493
3494 let (angle, pos) = parse_conic_first_item("from 90deg").unwrap().unwrap();
3495 assert_eq!(angle, AngleValue::deg(90.0));
3496 assert_eq!(pos, StyleBackgroundPosition::default());
3497
3498 let (angle, pos) = parse_conic_first_item("from 0deg at center")
3499 .unwrap()
3500 .unwrap();
3501 assert_eq!(angle, AngleValue::deg(0.0));
3502 assert_eq!(pos.horizontal, BackgroundPositionHorizontal::Center);
3503 assert_eq!(pos.vertical, BackgroundPositionVertical::Center);
3504 }
3505
3506 #[test]
3507 fn autotest_parse_conic_first_item_rejects_malformed_preludes() {
3508 assert!(parse_conic_first_item("from").is_err());
3510 assert!(parse_conic_first_item("from at center").is_err());
3511 assert!(parse_conic_first_item("fromage").is_err());
3514 assert!(matches!(
3516 parse_conic_first_item("from 90deg at left top center"),
3517 Err(CssConicGradientParseError::Position(_))
3518 ));
3519
3520 let huge = alloc::format!("from {}", "9".repeat(100_000));
3521 let _ = parse_conic_first_item(&huge);
3522 for input in ADVERSARIAL {
3523 let a = parse_conic_first_item(input);
3524 let b = parse_conic_first_item(input);
3525 assert_eq!(a, b, "non-deterministic for {input:?}");
3526 }
3527 }
3528
3529 #[test]
3530 fn autotest_conic_gradient_end_to_end() {
3531 let g = conic("conic-gradient(from 45deg, red, blue)");
3532 assert_eq!(g.angle, AngleValue::deg(45.0));
3533 assert_eq!(g.extend_mode, ExtendMode::Clamp);
3534 assert_eq!(g.stops.len(), 2);
3535 assert_eq!(g.stops.as_ref()[0].angle.to_degrees_raw(), 0.0);
3536 assert_eq!(g.stops.as_ref()[1].angle.to_degrees_raw(), 360.0);
3537
3538 for input in [
3540 "conic-gradient(red, blue)",
3541 "conic-gradient(red 0deg, blue 180deg, green 360deg)",
3542 "conic-gradient(red 180deg, blue 90deg)",
3543 "conic-gradient(red -90deg, blue)",
3544 "repeating-conic-gradient(red, blue 30deg)",
3545 ] {
3546 let g = conic(input);
3547 let mut prev = f32::NEG_INFINITY;
3548 for s in &g.stops {
3549 let deg = s.angle.to_degrees_raw();
3550 assert!(deg.is_finite(), "non-finite angle in {input:?}");
3551 assert!(deg >= prev, "angles not monotonic in {input:?}");
3552 prev = deg;
3553 }
3554 }
3555
3556 let g = conic("conic-gradient(red 1e40deg, blue)");
3558 assert_eq!(g.stops.len(), 2);
3559 for s in &g.stops {
3560 assert!(s.angle.to_degrees_raw().is_finite());
3561 }
3562
3563 assert!(parse_style_background_content("conic-gradient(from, red)").is_err());
3564 }
3565
3566 #[test]
3571 fn autotest_parse_background_position_horizontal() {
3572 assert_eq!(
3573 parse_background_position_horizontal("left").unwrap(),
3574 BackgroundPositionHorizontal::Left
3575 );
3576 assert_eq!(
3577 parse_background_position_horizontal("center").unwrap(),
3578 BackgroundPositionHorizontal::Center
3579 );
3580 assert_eq!(
3581 parse_background_position_horizontal("right").unwrap(),
3582 BackgroundPositionHorizontal::Right
3583 );
3584 assert_eq!(
3585 parse_background_position_horizontal("10px").unwrap(),
3586 BackgroundPositionHorizontal::Exact(PixelValue::px(10.0))
3587 );
3588 assert!(parse_background_position_horizontal("top").is_err());
3590 assert!(parse_background_position_horizontal(" left").is_err());
3592 assert!(parse_background_position_horizontal("").is_err());
3593 assert!(parse_background_position_horizontal("\u{1F600}").is_err());
3594
3595 let huge = "a".repeat(100_000);
3596 assert!(parse_background_position_horizontal(&huge).is_err());
3597 }
3598
3599 #[test]
3600 fn autotest_parse_background_position_vertical() {
3601 assert_eq!(
3602 parse_background_position_vertical("top").unwrap(),
3603 BackgroundPositionVertical::Top
3604 );
3605 assert_eq!(
3606 parse_background_position_vertical("center").unwrap(),
3607 BackgroundPositionVertical::Center
3608 );
3609 assert_eq!(
3610 parse_background_position_vertical("bottom").unwrap(),
3611 BackgroundPositionVertical::Bottom
3612 );
3613 assert_eq!(
3614 parse_background_position_vertical("-10px").unwrap(),
3615 BackgroundPositionVertical::Exact(PixelValue::px(-10.0))
3616 );
3617 assert!(parse_background_position_vertical("left").is_err());
3618 assert!(parse_background_position_vertical("").is_err());
3619 assert!(parse_background_position_vertical("\u{1F600}").is_err());
3620
3621 let huge = "a".repeat(100_000);
3622 assert!(parse_background_position_vertical(&huge).is_err());
3623 }
3624
3625 #[test]
3630 fn autotest_parse_shape() {
3631 assert_eq!(parse_shape("circle").unwrap(), Shape::Circle);
3632 assert_eq!(parse_shape(" ellipse ").unwrap(), Shape::Ellipse);
3633 for input in [
3634 "",
3635 " ",
3636 "Circle",
3637 "CIRCLE",
3638 "circles",
3639 "!!!",
3640 "\u{1F600}",
3641 "0",
3642 ] {
3643 assert!(parse_shape(input).is_err(), "{input:?} unexpectedly parsed");
3644 }
3645 let huge = "a".repeat(100_000);
3646 assert!(parse_shape(&huge).is_err());
3647 for input in ADVERSARIAL {
3648 assert_eq!(parse_shape(input), parse_shape(input));
3649 }
3650 }
3651
3652 #[test]
3653 fn autotest_parse_radial_gradient_size() {
3654 assert_eq!(
3655 parse_radial_gradient_size("closest-side").unwrap(),
3656 RadialGradientSize::ClosestSide
3657 );
3658 assert_eq!(
3659 parse_radial_gradient_size(" closest-corner ").unwrap(),
3660 RadialGradientSize::ClosestCorner
3661 );
3662 assert_eq!(
3663 parse_radial_gradient_size("farthest-side").unwrap(),
3664 RadialGradientSize::FarthestSide
3665 );
3666 assert_eq!(
3667 parse_radial_gradient_size("farthest-corner").unwrap(),
3668 RadialGradientSize::FarthestCorner
3669 );
3670 for input in [
3671 "",
3672 " ",
3673 "closest",
3674 "CLOSEST-SIDE",
3675 "farthest-corners",
3676 "!!!",
3677 "\u{1F600}",
3678 ] {
3679 assert!(
3680 parse_radial_gradient_size(input).is_err(),
3681 "{input:?} unexpectedly parsed"
3682 );
3683 }
3684 let huge = "a".repeat(100_000);
3685 assert!(parse_radial_gradient_size(&huge).is_err());
3686 }
3687
3688 #[test]
3693 fn autotest_round_trip_background_repeat() {
3694 for r in [
3695 StyleBackgroundRepeat::NoRepeat,
3696 StyleBackgroundRepeat::PatternRepeat,
3697 StyleBackgroundRepeat::RepeatX,
3698 StyleBackgroundRepeat::RepeatY,
3699 ] {
3700 let printed = r.print_as_css_value();
3701 assert!(!printed.is_empty());
3702 assert_eq!(parse_style_background_repeat(&printed).unwrap(), r);
3703 }
3704 }
3705
3706 #[test]
3707 fn autotest_round_trip_background_size() {
3708 for s in [
3709 StyleBackgroundSize::Contain,
3710 StyleBackgroundSize::Cover,
3711 StyleBackgroundSize::ExactSize(PixelValueSize {
3712 width: PixelValue::px(100.0),
3713 height: PixelValue::em(20.0),
3714 }),
3715 StyleBackgroundSize::ExactSize(PixelValueSize {
3716 width: PixelValue::percent(50.0),
3717 height: PixelValue::percent(50.0),
3718 }),
3719 StyleBackgroundSize::ExactSize(PixelValueSize {
3720 width: PixelValue::px(0.0),
3721 height: PixelValue::px(-25.5),
3722 }),
3723 ] {
3724 let printed = s.print_as_css_value();
3725 assert_eq!(
3726 parse_style_background_size(&printed).unwrap(),
3727 s,
3728 "round-trip failed for {printed:?}"
3729 );
3730 }
3731 }
3732
3733 #[test]
3734 fn autotest_round_trip_background_position() {
3735 let horizontals = [
3736 BackgroundPositionHorizontal::Left,
3737 BackgroundPositionHorizontal::Center,
3738 BackgroundPositionHorizontal::Right,
3739 BackgroundPositionHorizontal::Exact(PixelValue::px(50.0)),
3740 BackgroundPositionHorizontal::Exact(PixelValue::percent(25.0)),
3741 ];
3742 let verticals = [
3743 BackgroundPositionVertical::Top,
3744 BackgroundPositionVertical::Center,
3745 BackgroundPositionVertical::Bottom,
3746 BackgroundPositionVertical::Exact(PixelValue::px(-10.0)),
3747 BackgroundPositionVertical::Exact(PixelValue::em(2.5)),
3748 ];
3749 for horizontal in horizontals {
3750 for vertical in verticals {
3751 let pos = StyleBackgroundPosition {
3752 horizontal,
3753 vertical,
3754 };
3755 let printed = pos.print_as_css_value();
3756 assert_eq!(
3757 parse_style_background_position(&printed).unwrap(),
3758 pos,
3759 "round-trip failed for {printed:?}"
3760 );
3761 }
3762 }
3763 }
3764
3765 #[test]
3766 fn autotest_round_trip_background_content_colors_and_images() {
3767 for content in [
3768 StyleBackgroundContent::Color(ColorU::RED),
3769 StyleBackgroundContent::Color(ColorU::TRANSPARENT),
3770 StyleBackgroundContent::Color(ColorU::rgba(1, 2, 3, 4)),
3771 StyleBackgroundContent::Color(ColorU::WHITE),
3772 StyleBackgroundContent::Image("a.png".into()),
3773 StyleBackgroundContent::Image("some/deep/path.jpeg".into()),
3774 StyleBackgroundContent::SystemColor(SystemColorRef::Accent),
3775 StyleBackgroundContent::SystemColor(SystemColorRef::SelectionText),
3776 ] {
3777 let printed = content.print_as_css_value();
3778 assert_eq!(
3779 parse_style_background_content(&printed).unwrap(),
3780 content,
3781 "round-trip failed for {printed:?}"
3782 );
3783 }
3784 assert_eq!(
3785 StyleBackgroundContent::default(),
3786 StyleBackgroundContent::Color(ColorU::TRANSPARENT)
3787 );
3788 }
3789
3790 #[test]
3791 fn autotest_round_trip_gradients() {
3792 for input in [
3793 "linear-gradient(to right, red 0%, blue 100%)",
3794 "repeating-linear-gradient(to bottom, red 25%, blue 75%)",
3795 "linear-gradient(45deg, red 0%, blue 50%)",
3796 "radial-gradient(circle farthest-corner at left top, red 0%, blue 100%)",
3797 "conic-gradient(from 90deg at left top, red 0deg, blue 360deg)",
3798 "repeating-conic-gradient(from 0deg at left top, red 0deg, blue 180deg)",
3799 ] {
3800 let parsed = parse_style_background_content(input).unwrap();
3801 let printed = parsed.print_as_css_value();
3802 let reparsed = parse_style_background_content(&printed).unwrap();
3803 assert_eq!(
3804 parsed, reparsed,
3805 "gradient did not survive print -> parse ({printed:?})"
3806 );
3807 assert_eq!(printed, reparsed.print_as_css_value());
3809 }
3810 }
3811
3812 #[test]
3813 fn autotest_round_trip_vec_printing_is_comma_separated() {
3814 let contents = parse_style_background_content_multiple("red, blue").unwrap();
3815 assert_eq!(contents.print_as_css_value(), "#ff0000ff, #0000ffff");
3816 assert_eq!(
3817 contents.as_slice()[1],
3818 StyleBackgroundContent::Color(blue())
3819 );
3820 let reparsed =
3821 parse_style_background_content_multiple(&contents.print_as_css_value()).unwrap();
3822 assert_eq!(reparsed, contents);
3823
3824 let sizes = parse_style_background_size_multiple("contain, 10px 20px").unwrap();
3825 assert_eq!(
3826 parse_style_background_size_multiple(&sizes.print_as_css_value()).unwrap(),
3827 sizes
3828 );
3829
3830 let repeats = parse_style_background_repeat_multiple("repeat, no-repeat").unwrap();
3831 assert_eq!(
3832 parse_style_background_repeat_multiple(&repeats.print_as_css_value()).unwrap(),
3833 repeats
3834 );
3835
3836 let positions =
3837 parse_style_background_position_multiple("left top, 10px 20px").unwrap();
3838 assert_eq!(
3839 parse_style_background_position_multiple(&positions.print_as_css_value()).unwrap(),
3840 positions
3841 );
3842 }
3843
3844 #[test]
3845 fn autotest_normalized_stop_printing_is_reparseable() {
3846 let stop = NormalizedLinearColorStop::new(PercentageValue::new(25.0), ColorU::RED);
3847 assert_eq!(stop.print_as_css_value(), "#ff0000ff 25%");
3848 let reparsed = parse_linear_color_stop(&stop.print_as_css_value()).unwrap();
3849 assert_eq!(reparsed.color, stop.color);
3850 assert_eq!(
3851 reparsed.offset1,
3852 OptionPercentageValue::Some(PercentageValue::new(25.0))
3853 );
3854
3855 let rstop = NormalizedRadialColorStop::new(AngleValue::deg(90.0), blue());
3856 assert_eq!(rstop.print_as_css_value(), "#0000ffff 90deg");
3857 let reparsed = parse_radial_color_stop(&rstop.print_as_css_value()).unwrap();
3858 assert_eq!(reparsed.color, rstop.color);
3859 assert_eq!(
3860 reparsed.offset1,
3861 OptionAngleValue::Some(AngleValue::deg(90.0))
3862 );
3863
3864 let sys = NormalizedLinearColorStop {
3866 offset: PercentageValue::new(50.0),
3867 color: ColorOrSystem::System(SystemColorRef::Accent),
3868 };
3869 assert_eq!(sys.print_as_css_value(), "system:accent 50%");
3870 assert_eq!(
3871 parse_linear_color_stop(&sys.print_as_css_value())
3872 .unwrap()
3873 .color,
3874 ColorOrSystem::System(SystemColorRef::Accent)
3875 );
3876 }
3877
3878 #[test]
3879 fn autotest_empty_gradient_printing_does_not_panic() {
3880 let lg = StyleBackgroundContent::LinearGradient(LinearGradient::default());
3883 assert!(lg.print_as_css_value().starts_with("linear-gradient("));
3884
3885 let rg = StyleBackgroundContent::RadialGradient(RadialGradient::default());
3886 assert!(rg.print_as_css_value().starts_with("radial-gradient("));
3887
3888 let cg = StyleBackgroundContent::ConicGradient(ConicGradient::default());
3889 assert!(cg.print_as_css_value().starts_with("conic-gradient("));
3890
3891 let empty_stops = StyleBackgroundContent::LinearGradient(LinearGradient {
3893 extend_mode: ExtendMode::Repeat,
3894 stops: Vec::<NormalizedLinearColorStop>::new().into(),
3895 ..LinearGradient::default()
3896 });
3897 assert!(empty_stops
3898 .print_as_css_value()
3899 .starts_with("repeating-linear-gradient("));
3900 }
3901 }
3902}
3903
3904#[cfg(feature = "parser")]
3905pub use self::parser::*;
3906
3907#[cfg(all(test, feature = "parser"))]
3908mod tests {
3909 use super::*;
3910 use crate::props::basic::{DirectionCorner, DirectionCorners};
3911
3912 #[test]
3913 fn test_parse_single_background_content() {
3914 assert_eq!(
3916 parse_style_background_content("red").unwrap(),
3917 StyleBackgroundContent::Color(ColorU::RED)
3918 );
3919 assert_eq!(
3920 parse_style_background_content("#ff00ff").unwrap(),
3921 StyleBackgroundContent::Color(ColorU::new_rgb(255, 0, 255))
3922 );
3923
3924 assert_eq!(
3926 parse_style_background_content("url(\"image.png\")").unwrap(),
3927 StyleBackgroundContent::Image("image.png".into())
3928 );
3929
3930 let lg = parse_style_background_content("linear-gradient(to right, red, blue)").unwrap();
3932 assert!(matches!(lg, StyleBackgroundContent::LinearGradient(_)));
3933 if let StyleBackgroundContent::LinearGradient(grad) = lg {
3934 assert_eq!(grad.stops.len(), 2);
3935 assert_eq!(
3936 grad.direction,
3937 Direction::FromTo(DirectionCorners {
3938 dir_from: DirectionCorner::Left,
3939 dir_to: DirectionCorner::Right
3940 })
3941 );
3942 }
3943
3944 let rg = parse_style_background_content("radial-gradient(circle, white, black)").unwrap();
3946 assert!(matches!(rg, StyleBackgroundContent::RadialGradient(_)));
3947 if let StyleBackgroundContent::RadialGradient(grad) = rg {
3948 assert_eq!(grad.stops.len(), 2);
3949 assert_eq!(grad.shape, Shape::Circle);
3950 }
3951
3952 let cg = parse_style_background_content("conic-gradient(from 90deg, red, blue)").unwrap();
3954 assert!(matches!(cg, StyleBackgroundContent::ConicGradient(_)));
3955 if let StyleBackgroundContent::ConicGradient(grad) = cg {
3956 assert_eq!(grad.stops.len(), 2);
3957 assert_eq!(grad.angle, AngleValue::deg(90.0));
3958 }
3959 }
3960
3961 #[test]
3962 fn test_parse_multiple_background_content() {
3963 let result =
3964 parse_style_background_content_multiple("url(foo.png), linear-gradient(red, blue)")
3965 .unwrap();
3966 assert_eq!(result.len(), 2);
3967 assert!(matches!(
3968 result.as_slice()[0],
3969 StyleBackgroundContent::Image(_)
3970 ));
3971 assert!(matches!(
3972 result.as_slice()[1],
3973 StyleBackgroundContent::LinearGradient(_)
3974 ));
3975 }
3976
3977 #[test]
3978 fn test_parse_background_position() {
3979 let result = parse_style_background_position("center").unwrap();
3981 assert_eq!(result.horizontal, BackgroundPositionHorizontal::Center);
3982 assert_eq!(result.vertical, BackgroundPositionVertical::Center);
3983
3984 let result = parse_style_background_position("25%").unwrap();
3985 assert_eq!(
3986 result.horizontal,
3987 BackgroundPositionHorizontal::Exact(PixelValue::percent(25.0))
3988 );
3989 assert_eq!(result.vertical, BackgroundPositionVertical::Center);
3990
3991 let result = parse_style_background_position("right 50px").unwrap();
3993 assert_eq!(result.horizontal, BackgroundPositionHorizontal::Right);
3994 assert_eq!(
3995 result.vertical,
3996 BackgroundPositionVertical::Exact(PixelValue::px(50.0))
3997 );
3998
3999 assert!(parse_style_background_position("left 10px top 20px").is_err());
4001 }
4002
4003 #[test]
4004 fn test_parse_background_size() {
4005 assert_eq!(
4006 parse_style_background_size("contain").unwrap(),
4007 StyleBackgroundSize::Contain
4008 );
4009 assert_eq!(
4010 parse_style_background_size("cover").unwrap(),
4011 StyleBackgroundSize::Cover
4012 );
4013 assert_eq!(
4014 parse_style_background_size("50%").unwrap(),
4015 StyleBackgroundSize::ExactSize(PixelValueSize {
4016 width: PixelValue::percent(50.0),
4017 height: PixelValue::percent(50.0)
4018 })
4019 );
4020 assert_eq!(
4021 parse_style_background_size("100px 20em").unwrap(),
4022 StyleBackgroundSize::ExactSize(PixelValueSize {
4023 width: PixelValue::px(100.0),
4024 height: PixelValue::em(20.0)
4025 })
4026 );
4027 assert!(parse_style_background_size("auto").is_err());
4028 }
4029
4030 #[test]
4031 fn test_parse_background_repeat() {
4032 assert_eq!(
4033 parse_style_background_repeat("repeat").unwrap(),
4034 StyleBackgroundRepeat::PatternRepeat
4035 );
4036 assert_eq!(
4037 parse_style_background_repeat("repeat-x").unwrap(),
4038 StyleBackgroundRepeat::RepeatX
4039 );
4040 assert_eq!(
4041 parse_style_background_repeat("repeat-y").unwrap(),
4042 StyleBackgroundRepeat::RepeatY
4043 );
4044 assert_eq!(
4045 parse_style_background_repeat("no-repeat").unwrap(),
4046 StyleBackgroundRepeat::NoRepeat
4047 );
4048 assert!(parse_style_background_repeat("repeat-xy").is_err());
4049 }
4050
4051 #[test]
4056 fn test_gradient_no_position_stops() {
4057 let lg = parse_style_background_content("linear-gradient(red, blue)").unwrap();
4059 if let StyleBackgroundContent::LinearGradient(grad) = lg {
4060 assert_eq!(grad.stops.len(), 2);
4061 assert!((grad.stops.as_ref()[0].offset.normalized() - 0.0).abs() < 0.001);
4063 assert!((grad.stops.as_ref()[1].offset.normalized() - 1.0).abs() < 0.001);
4065 } else {
4066 panic!("Expected LinearGradient");
4067 }
4068 }
4069
4070 #[test]
4071 fn test_gradient_single_position_stops() {
4072 let lg = parse_style_background_content("linear-gradient(red 25%, blue 75%)").unwrap();
4074 if let StyleBackgroundContent::LinearGradient(grad) = lg {
4075 assert_eq!(grad.stops.len(), 2);
4076 assert!((grad.stops.as_ref()[0].offset.normalized() - 0.25).abs() < 0.001);
4077 assert!((grad.stops.as_ref()[1].offset.normalized() - 0.75).abs() < 0.001);
4078 } else {
4079 panic!("Expected LinearGradient");
4080 }
4081 }
4082
4083 #[test]
4084 fn test_gradient_multi_position_stops() {
4085 let lg = parse_style_background_content("linear-gradient(red 10% 30%, blue)").unwrap();
4087 if let StyleBackgroundContent::LinearGradient(grad) = lg {
4088 assert_eq!(grad.stops.len(), 3, "Expected 3 stops for multi-position");
4090 assert!((grad.stops.as_ref()[0].offset.normalized() - 0.10).abs() < 0.001);
4091 assert!((grad.stops.as_ref()[1].offset.normalized() - 0.30).abs() < 0.001);
4092 assert!((grad.stops.as_ref()[2].offset.normalized() - 1.0).abs() < 0.001);
4093 assert_eq!(grad.stops.as_ref()[0].color, grad.stops.as_ref()[1].color);
4095 } else {
4096 panic!("Expected LinearGradient");
4097 }
4098 }
4099
4100 #[test]
4101 fn test_gradient_three_colors_no_positions() {
4102 let lg = parse_style_background_content("linear-gradient(red, green, blue)").unwrap();
4104 if let StyleBackgroundContent::LinearGradient(grad) = lg {
4105 assert_eq!(grad.stops.len(), 3);
4106 assert!((grad.stops.as_ref()[0].offset.normalized() - 0.0).abs() < 0.001);
4108 assert!((grad.stops.as_ref()[1].offset.normalized() - 0.5).abs() < 0.001);
4109 assert!((grad.stops.as_ref()[2].offset.normalized() - 1.0).abs() < 0.001);
4110 } else {
4111 panic!("Expected LinearGradient");
4112 }
4113 }
4114
4115 #[test]
4116 fn test_gradient_fixup_ascending_order() {
4117 let lg = parse_style_background_content("linear-gradient(red 50%, blue 20%)").unwrap();
4120 if let StyleBackgroundContent::LinearGradient(grad) = lg {
4121 assert_eq!(grad.stops.len(), 2);
4122 assert!((grad.stops.as_ref()[0].offset.normalized() - 0.50).abs() < 0.001);
4124 assert!((grad.stops.as_ref()[1].offset.normalized() - 0.50).abs() < 0.001);
4126 } else {
4127 panic!("Expected LinearGradient");
4128 }
4129 }
4130
4131 #[test]
4132 fn test_gradient_distribute_unpositioned() {
4133 let lg =
4136 parse_style_background_content("linear-gradient(red 0%, yellow, green, blue 100%)")
4137 .unwrap();
4138 if let StyleBackgroundContent::LinearGradient(grad) = lg {
4139 assert_eq!(grad.stops.len(), 4);
4140 assert!((grad.stops.as_ref()[0].offset.normalized() - 0.0).abs() < 0.001);
4142 assert!((grad.stops.as_ref()[1].offset.normalized() - 0.333).abs() < 0.01);
4143 assert!((grad.stops.as_ref()[2].offset.normalized() - 0.666).abs() < 0.01);
4144 assert!((grad.stops.as_ref()[3].offset.normalized() - 1.0).abs() < 0.001);
4145 } else {
4146 panic!("Expected LinearGradient");
4147 }
4148 }
4149
4150 #[test]
4151 fn test_gradient_direction_to_corner() {
4152 let lg =
4154 parse_style_background_content("linear-gradient(to top right, red, blue)").unwrap();
4155 if let StyleBackgroundContent::LinearGradient(grad) = lg {
4156 assert_eq!(
4157 grad.direction,
4158 Direction::FromTo(DirectionCorners {
4159 dir_from: DirectionCorner::BottomLeft,
4160 dir_to: DirectionCorner::TopRight
4161 })
4162 );
4163 } else {
4164 panic!("Expected LinearGradient");
4165 }
4166 }
4167
4168 #[test]
4169 fn test_gradient_direction_angle() {
4170 let lg = parse_style_background_content("linear-gradient(45deg, red, blue)").unwrap();
4172 if let StyleBackgroundContent::LinearGradient(grad) = lg {
4173 assert_eq!(grad.direction, Direction::Angle(AngleValue::deg(45.0)));
4174 } else {
4175 panic!("Expected LinearGradient");
4176 }
4177 }
4178
4179 #[test]
4180 fn test_repeating_gradient() {
4181 let lg =
4183 parse_style_background_content("repeating-linear-gradient(red, blue 20%)").unwrap();
4184 if let StyleBackgroundContent::LinearGradient(grad) = lg {
4185 assert_eq!(grad.extend_mode, ExtendMode::Repeat);
4186 } else {
4187 panic!("Expected LinearGradient");
4188 }
4189 }
4190
4191 #[test]
4192 fn test_radial_gradient_circle() {
4193 let rg = parse_style_background_content("radial-gradient(circle, red, blue)").unwrap();
4195 if let StyleBackgroundContent::RadialGradient(grad) = rg {
4196 assert_eq!(grad.shape, Shape::Circle);
4197 assert_eq!(grad.stops.len(), 2);
4198 assert_eq!(grad.position.horizontal, BackgroundPositionHorizontal::Left);
4200 assert_eq!(grad.position.vertical, BackgroundPositionVertical::Top);
4201 } else {
4202 panic!("Expected RadialGradient");
4203 }
4204 }
4205
4206 #[test]
4207 fn test_radial_gradient_ellipse() {
4208 let rg = parse_style_background_content("radial-gradient(ellipse, red, blue)").unwrap();
4210 if let StyleBackgroundContent::RadialGradient(grad) = rg {
4211 assert_eq!(grad.shape, Shape::Ellipse);
4212 assert_eq!(grad.stops.len(), 2);
4213 } else {
4214 panic!("Expected RadialGradient");
4215 }
4216 }
4217
4218 #[test]
4219 fn test_radial_gradient_size_keywords() {
4220 let rg = parse_style_background_content("radial-gradient(circle closest-side, red, blue)")
4222 .unwrap();
4223 if let StyleBackgroundContent::RadialGradient(grad) = rg {
4224 assert_eq!(grad.shape, Shape::Circle);
4225 assert_eq!(grad.size, RadialGradientSize::ClosestSide);
4226 } else {
4227 panic!("Expected RadialGradient");
4228 }
4229 }
4230
4231 #[test]
4232 fn test_radial_gradient_stop_positions() {
4233 let rg = parse_style_background_content("radial-gradient(red 0%, blue 100%)").unwrap();
4235 if let StyleBackgroundContent::RadialGradient(grad) = rg {
4236 assert_eq!(grad.stops.len(), 2);
4237 assert!((grad.stops.as_ref()[0].offset.normalized() - 0.0).abs() < 0.001);
4238 assert!((grad.stops.as_ref()[1].offset.normalized() - 1.0).abs() < 0.001);
4239 } else {
4240 panic!("Expected RadialGradient");
4241 }
4242 }
4243
4244 #[test]
4245 fn test_repeating_radial_gradient() {
4246 let rg = parse_style_background_content("repeating-radial-gradient(circle, red, blue 20%)")
4247 .unwrap();
4248 if let StyleBackgroundContent::RadialGradient(grad) = rg {
4249 assert_eq!(grad.extend_mode, ExtendMode::Repeat);
4250 assert_eq!(grad.shape, Shape::Circle);
4251 } else {
4252 panic!("Expected RadialGradient");
4253 }
4254 }
4255
4256 #[test]
4257 fn test_conic_gradient_angle() {
4258 let cg = parse_style_background_content("conic-gradient(from 45deg, red, blue)").unwrap();
4260 if let StyleBackgroundContent::ConicGradient(grad) = cg {
4261 assert_eq!(grad.angle, AngleValue::deg(45.0));
4262 assert_eq!(grad.stops.len(), 2);
4263 } else {
4264 panic!("Expected ConicGradient");
4265 }
4266 }
4267
4268 #[test]
4269 fn test_conic_gradient_default() {
4270 let cg = parse_style_background_content("conic-gradient(red, blue)").unwrap();
4272 if let StyleBackgroundContent::ConicGradient(grad) = cg {
4273 assert_eq!(grad.stops.len(), 2);
4274 assert!(
4276 (grad.stops.as_ref()[0].angle.to_degrees_raw() - 0.0).abs() < 0.001,
4277 "First stop should be 0deg, got {}",
4278 grad.stops.as_ref()[0].angle.to_degrees_raw()
4279 );
4280 assert!(
4282 (grad.stops.as_ref()[1].angle.to_degrees_raw() - 360.0).abs() < 0.001,
4283 "Last stop should be 360deg, got {}",
4284 grad.stops.as_ref()[1].angle.to_degrees_raw()
4285 );
4286 } else {
4287 panic!("Expected ConicGradient");
4288 }
4289 }
4290
4291 #[test]
4292 fn test_conic_gradient_with_positions() {
4293 let cg =
4295 parse_style_background_content("conic-gradient(red 0deg, blue 180deg, green 360deg)")
4296 .unwrap();
4297 if let StyleBackgroundContent::ConicGradient(grad) = cg {
4298 assert_eq!(grad.stops.len(), 3);
4299 assert!(
4301 (grad.stops.as_ref()[0].angle.to_degrees_raw() - 0.0).abs() < 0.001,
4302 "First stop should be 0deg, got {}",
4303 grad.stops.as_ref()[0].angle.to_degrees_raw()
4304 );
4305 assert!(
4306 (grad.stops.as_ref()[1].angle.to_degrees_raw() - 180.0).abs() < 0.001,
4307 "Second stop should be 180deg, got {}",
4308 grad.stops.as_ref()[1].angle.to_degrees_raw()
4309 );
4310 assert!(
4311 (grad.stops.as_ref()[2].angle.to_degrees_raw() - 360.0).abs() < 0.001,
4312 "Last stop should be 360deg, got {}",
4313 grad.stops.as_ref()[2].angle.to_degrees_raw()
4314 );
4315 } else {
4316 panic!("Expected ConicGradient");
4317 }
4318 }
4319
4320 #[test]
4321 fn test_repeating_conic_gradient() {
4322 let cg =
4323 parse_style_background_content("repeating-conic-gradient(red, blue 30deg)").unwrap();
4324 if let StyleBackgroundContent::ConicGradient(grad) = cg {
4325 assert_eq!(grad.extend_mode, ExtendMode::Repeat);
4326 } else {
4327 panic!("Expected ConicGradient");
4328 }
4329 }
4330
4331 #[test]
4332 fn test_gradient_with_rgba_color() {
4333 let lg =
4335 parse_style_background_content("linear-gradient(rgba(255,0,0,0.5), blue)").unwrap();
4336 if let StyleBackgroundContent::LinearGradient(grad) = lg {
4337 assert_eq!(grad.stops.len(), 2);
4338 let first_color = grad.stops.as_ref()[0].color.to_color_u_default();
4340 assert!(first_color.a >= 127 && first_color.a <= 128);
4341 } else {
4342 panic!("Expected LinearGradient");
4343 }
4344 }
4345
4346 #[test]
4347 fn test_gradient_with_rgba_and_position() {
4348 let lg =
4350 parse_style_background_content("linear-gradient(rgba(0,0,0,0.5) 50%, white)").unwrap();
4351 if let StyleBackgroundContent::LinearGradient(grad) = lg {
4352 assert_eq!(grad.stops.len(), 2);
4353 assert!((grad.stops.as_ref()[0].offset.normalized() - 0.5).abs() < 0.001);
4354 } else {
4355 panic!("Expected LinearGradient");
4356 }
4357 }
4358
4359 #[test]
4360 fn test_gradient_resolves_system_color_stop() {
4361 use crate::props::basic::color::ColorOrSystem;
4366 use crate::system::SystemColors;
4367
4368 let lg = parse_style_background_content("linear-gradient(red, system:accent)").unwrap();
4369 let StyleBackgroundContent::LinearGradient(grad) = lg else {
4370 panic!("Expected LinearGradient");
4371 };
4372 let stops = grad.stops.as_ref();
4373 assert_eq!(stops.len(), 2);
4374
4375 let accent_stop = &stops[1];
4376 assert!(matches!(accent_stop.color, ColorOrSystem::System(_)));
4377
4378 let populated = SystemColors {
4379 accent: crate::props::basic::color::OptionColorU::Some(ColorU::new_rgb(0, 122, 255)),
4380 ..SystemColors::default()
4381 };
4382
4383 let resolved = accent_stop.resolve(&populated, ColorU::TRANSPARENT);
4384 assert_eq!(resolved, ColorU::new_rgb(0, 122, 255));
4385
4386 let empty = SystemColors::default();
4387 let fallback = accent_stop.resolve(&empty, ColorU::TRANSPARENT);
4388 assert_eq!(fallback, ColorU::TRANSPARENT);
4389 }
4390}