1use std::fmt;
7
8#[derive(Debug, Clone, Copy, PartialEq, Default)]
10pub enum Length {
11 Px(f32),
13 Percent(f32),
15 Rem(f32),
17 Em(f32),
19 Vw(f32),
21 Vh(f32),
23 Dvh(f32),
25 Dvw(f32),
27 Auto,
29 #[default]
31 Zero,
32}
33
34impl Length {
35 pub fn parse(s: &str) -> Option<Self> {
37 let s = s.trim();
38
39 if s == "auto" {
40 return Some(Length::Auto);
41 }
42
43 if s == "0" {
44 return Some(Length::Zero);
45 }
46
47 if let Some(val) = s.strip_suffix("px") {
49 return val.trim().parse::<f32>().ok().map(Length::Px);
50 }
51
52 if let Some(val) = s.strip_suffix('%') {
53 return val.trim().parse::<f32>().ok().map(Length::Percent);
54 }
55
56 if let Some(val) = s.strip_suffix("rem") {
57 return val.trim().parse::<f32>().ok().map(Length::Rem);
58 }
59
60 if let Some(val) = s.strip_suffix("em") {
61 return val.trim().parse::<f32>().ok().map(Length::Em);
62 }
63
64 if let Some(val) = s.strip_suffix("dvw") {
65 return val.trim().parse::<f32>().ok().map(Length::Dvw);
66 }
67
68 if let Some(val) = s.strip_suffix("dvh") {
69 return val.trim().parse::<f32>().ok().map(Length::Dvh);
70 }
71
72 if let Some(val) = s.strip_suffix("vw") {
73 return val.trim().parse::<f32>().ok().map(Length::Vw);
74 }
75
76 if let Some(val) = s.strip_suffix("vh") {
77 return val.trim().parse::<f32>().ok().map(Length::Vh);
78 }
79
80 s.parse::<f32>().ok().map(Length::Px)
82 }
83
84 pub fn to_px(&self, parent_size: f32, root_size: f32, viewport: (f32, f32)) -> f32 {
86 match *self {
87 Length::Px(v) => v,
88 Length::Percent(v) => parent_size * v / 100.0,
89 Length::Rem(v) => v * root_size,
90 Length::Em(v) => v * parent_size,
91 Length::Vw(v) => v * viewport.0 / 100.0,
92 Length::Vh(v) => v * viewport.1 / 100.0,
93 Length::Dvw(v) => v * viewport.0 / 100.0,
94 Length::Dvh(v) => v * viewport.1 / 100.0,
95 Length::Auto => 0.0, Length::Zero => 0.0,
97 }
98 }
99
100 pub fn is_auto(&self) -> bool {
102 matches!(self, Length::Auto)
103 }
104}
105
106impl fmt::Display for Length {
107 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108 match self {
109 Length::Px(v) => write!(f, "{}px", v),
110 Length::Percent(v) => write!(f, "{}%", v),
111 Length::Rem(v) => write!(f, "{}rem", v),
112 Length::Em(v) => write!(f, "{}em", v),
113 Length::Vw(v) => write!(f, "{}vw", v),
114 Length::Vh(v) => write!(f, "{}vh", v),
115 Length::Dvw(v) => write!(f, "{}dvw", v),
116 Length::Dvh(v) => write!(f, "{}dvh", v),
117 Length::Auto => write!(f, "auto"),
118 Length::Zero => write!(f, "0"),
119 }
120 }
121}
122
123#[derive(Debug, Clone, Copy, PartialEq)]
125pub struct Color {
126 pub r: u8,
127 pub g: u8,
128 pub b: u8,
129 pub a: u8,
130}
131
132impl Color {
133 pub const TRANSPARENT: Color = Color {
135 r: 0,
136 g: 0,
137 b: 0,
138 a: 0,
139 };
140 pub const BLACK: Color = Color {
142 r: 0,
143 g: 0,
144 b: 0,
145 a: 255,
146 };
147 pub const WHITE: Color = Color {
149 r: 255,
150 g: 255,
151 b: 255,
152 a: 255,
153 };
154 pub const RED: Color = Color {
156 r: 255,
157 g: 0,
158 b: 0,
159 a: 255,
160 };
161 pub const GREEN: Color = Color {
163 r: 0,
164 g: 255,
165 b: 0,
166 a: 255,
167 };
168 pub const BLUE: Color = Color {
170 r: 0,
171 g: 0,
172 b: 255,
173 a: 255,
174 };
175
176 pub const DARK_BG: Color = Color {
178 r: 26,
179 g: 26,
180 b: 26,
181 a: 255,
182 }; pub const DARK_TEXT: Color = Color {
184 r: 224,
185 g: 224,
186 b: 224,
187 a: 255,
188 }; pub const DARK_CARD: Color = Color {
190 r: 38,
191 g: 38,
192 b: 38,
193 a: 255,
194 }; pub const ACCENT_BLUE: Color = Color {
196 r: 52,
197 g: 120,
198 b: 246,
199 a: 255,
200 }; pub const ACCENT_GREEN: Color = Color {
202 r: 34,
203 g: 197,
204 b: 94,
205 a: 255,
206 }; pub const BORDER_DARK: Color = Color {
208 r: 55,
209 g: 65,
210 b: 81,
211 a: 255,
212 }; pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
216 Color { r, g, b, a }
217 }
218
219 pub fn parse(s: &str) -> Option<Self> {
221 let s = s.trim();
222
223 let named = match s.to_lowercase().as_str() {
225 "transparent" => return Some(Color::TRANSPARENT),
226 "black" => Color::BLACK,
227 "white" => Color::WHITE,
228 "red" => Color::RED,
229 "green" => Color::GREEN,
230 "blue" => Color::BLUE,
231 "yellow" => Color {
232 r: 255,
233 g: 255,
234 b: 0,
235 a: 255,
236 },
237 "cyan" => Color {
238 r: 0,
239 g: 255,
240 b: 255,
241 a: 255,
242 },
243 "magenta" => Color {
244 r: 255,
245 g: 0,
246 b: 255,
247 a: 255,
248 },
249 "silver" => Color {
250 r: 192,
251 g: 192,
252 b: 192,
253 a: 255,
254 },
255 "gray" | "grey" => Color {
256 r: 128,
257 g: 128,
258 b: 128,
259 a: 255,
260 },
261 "maroon" => Color {
262 r: 128,
263 g: 0,
264 b: 0,
265 a: 255,
266 },
267 "olive" => Color {
268 r: 128,
269 g: 128,
270 b: 0,
271 a: 255,
272 },
273 "lime" => Color {
274 r: 0,
275 g: 255,
276 b: 0,
277 a: 255,
278 },
279 "aqua" => Color {
280 r: 0,
281 g: 255,
282 b: 255,
283 a: 255,
284 },
285 "teal" => Color {
286 r: 0,
287 g: 128,
288 b: 128,
289 a: 255,
290 },
291 "navy" => Color {
292 r: 0,
293 g: 0,
294 b: 128,
295 a: 255,
296 },
297 "fuchsia" => Color {
298 r: 255,
299 g: 0,
300 b: 255,
301 a: 255,
302 },
303 "purple" => Color {
304 r: 128,
305 g: 0,
306 b: 128,
307 a: 255,
308 },
309 "orange" => Color {
310 r: 255,
311 g: 165,
312 b: 0,
313 a: 255,
314 },
315 "pink" => Color {
316 r: 255,
317 g: 192,
318 b: 203,
319 a: 255,
320 },
321 _ => {
322 if let Some(hex) = s.strip_prefix('#') {
324 return Self::parse_hex(hex);
325 }
326 if s.starts_with("rgb") {
328 return Self::parse_rgb(s);
329 }
330 return None;
331 }
332 };
333
334 Some(named)
335 }
336
337 fn parse_hex(hex: &str) -> Option<Self> {
338 let hex = hex.trim();
339 match hex.len() {
340 3 => {
341 let r = u8::from_str_radix(&hex[0..1].repeat(2), 16).ok()?;
342 let g = u8::from_str_radix(&hex[1..2].repeat(2), 16).ok()?;
343 let b = u8::from_str_radix(&hex[2..3].repeat(2), 16).ok()?;
344 Some(Color::new(r, g, b, 255))
345 }
346 6 => {
347 let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
348 let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
349 let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
350 Some(Color::new(r, g, b, 255))
351 }
352 8 => {
353 let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
354 let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
355 let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
356 let a = u8::from_str_radix(&hex[6..8], 16).ok()?;
357 Some(Color::new(r, g, b, a))
358 }
359 _ => None,
360 }
361 }
362
363 fn parse_rgb(s: &str) -> Option<Self> {
364 let inner = s
366 .strip_prefix("rgba(")
367 .or_else(|| s.strip_prefix("rgb("))?
368 .trim_end_matches(')')
369 .trim();
370
371 let parts: Vec<&str> = inner.split(',').map(|p| p.trim()).collect();
372
373 if parts.len() < 3 {
374 return None;
375 }
376
377 let r = parts[0].parse::<u8>().ok()?;
378 let g = parts[1].parse::<u8>().ok()?;
379 let b = parts[2].parse::<u8>().ok()?;
380 let a = if parts.len() >= 4 {
381 let a_str = parts[3];
383 if let Ok(a_val) = a_str.parse::<f32>() {
384 if a_val <= 1.0 {
385 (a_val * 255.0) as u8
386 } else {
387 a_val as u8
388 }
389 } else {
390 255
391 }
392 } else {
393 255
394 };
395
396 Some(Color::new(r, g, b, a))
397 }
398
399 pub fn to_rgba(&self) -> (u8, u8, u8, u8) {
401 (self.r, self.g, self.b, self.a)
402 }
403
404 pub fn to_normalized(&self) -> (f32, f32, f32, f32) {
406 (
407 self.r as f32 / 255.0,
408 self.g as f32 / 255.0,
409 self.b as f32 / 255.0,
410 self.a as f32 / 255.0,
411 )
412 }
413}
414
415impl fmt::Display for Color {
416 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
417 if self.a == 255 {
418 write!(f, "#{:02x}{:02x}{:02x}", self.r, self.g, self.b)
419 } else {
420 write!(
421 f,
422 "rgba({}, {}, {}, {})",
423 self.r,
424 self.g,
425 self.b,
426 self.a as f32 / 255.0
427 )
428 }
429 }
430}
431
432impl Default for Color {
433 fn default() -> Self {
434 Color::BLACK
435 }
436}
437
438#[derive(Debug, Clone, PartialEq)]
440pub struct Sides<T> {
441 pub top: T,
442 pub right: T,
443 pub bottom: T,
444 pub left: T,
445}
446
447impl<T: Default> Default for Sides<T> {
448 fn default() -> Self {
449 Self {
450 top: T::default(),
451 right: T::default(),
452 bottom: T::default(),
453 left: T::default(),
454 }
455 }
456}
457
458impl<T> Sides<T> {
459 pub fn new() -> Self
460 where
461 T: Default,
462 {
463 Self::default()
464 }
465
466 pub fn all(value: T) -> Self
467 where
468 T: Copy,
469 {
470 Self {
471 top: value,
472 right: value,
473 bottom: value,
474 left: value,
475 }
476 }
477}
478
479#[derive(Debug, Clone, Copy, PartialEq, Default)]
481pub enum BorderStyle {
482 #[default]
483 None,
484 Hidden,
485 Solid,
486 Dashed,
487 Dotted,
488 Double,
489 Groove,
490 Ridge,
491 Inset,
492 Outset,
493}
494
495impl BorderStyle {
496 pub fn parse(s: &str) -> Option<Self> {
497 match s.trim().to_lowercase().as_str() {
498 "none" => Some(BorderStyle::None),
499 "hidden" => Some(BorderStyle::Hidden),
500 "solid" => Some(BorderStyle::Solid),
501 "dashed" => Some(BorderStyle::Dashed),
502 "dotted" => Some(BorderStyle::Dotted),
503 "double" => Some(BorderStyle::Double),
504 "groove" => Some(BorderStyle::Groove),
505 "ridge" => Some(BorderStyle::Ridge),
506 "inset" => Some(BorderStyle::Inset),
507 "outset" => Some(BorderStyle::Outset),
508 _ => None,
509 }
510 }
511}
512
513#[derive(Debug, Clone, PartialEq)]
515pub struct Border {
516 pub width: Sides<Length>,
517 pub style: Sides<BorderStyle>,
518 pub color: Sides<Color>,
519}
520
521impl Default for Border {
522 fn default() -> Self {
523 Self {
524 width: Sides::default(),
525 style: Sides::default(),
526 color: Sides::all(Color::BLACK),
527 }
528 }
529}
530
531#[derive(Debug, Clone, Copy, PartialEq, Default)]
533pub enum TextAlign {
534 #[default]
535 Left,
536 Center,
537 Right,
538 Justify,
539}
540
541impl TextAlign {
542 pub fn parse(s: &str) -> Option<Self> {
543 match s.trim().to_lowercase().as_str() {
544 "left" => Some(TextAlign::Left),
545 "center" => Some(TextAlign::Center),
546 "right" => Some(TextAlign::Right),
547 "justify" => Some(TextAlign::Justify),
548 _ => None,
549 }
550 }
551}
552
553#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
566pub enum VerticalAlign {
567 #[default]
568 Baseline,
569 Top,
570 Bottom,
571 Middle,
572}
573
574impl VerticalAlign {
575 pub fn parse(s: &str) -> Option<Self> {
576 match s.trim().to_lowercase().as_str() {
577 "baseline" => Some(VerticalAlign::Baseline),
578 "top" => Some(VerticalAlign::Top),
579 "bottom" => Some(VerticalAlign::Bottom),
580 "middle" => Some(VerticalAlign::Middle),
581 _ => None,
587 }
588 }
589}
590
591#[derive(Debug, Clone, Copy, PartialEq, Default)]
593pub enum FontWeight {
594 #[default]
595 Normal,
596 Bold,
597 Bolder,
598 Lighter,
599 Value(u16),
600}
601
602impl FontWeight {
603 pub fn parse(s: &str) -> Option<Self> {
604 match s.trim().to_lowercase().as_str() {
605 "normal" => Some(FontWeight::Normal),
606 "bold" => Some(FontWeight::Bold),
607 "bolder" => Some(FontWeight::Bolder),
608 "lighter" => Some(FontWeight::Lighter),
609 _ => s.parse::<u16>().ok().map(FontWeight::Value),
610 }
611 }
612
613 pub fn to_number(&self) -> u16 {
614 match self {
615 FontWeight::Normal => 400,
616 FontWeight::Bold => 700,
617 FontWeight::Bolder => 900,
618 FontWeight::Lighter => 100,
619 FontWeight::Value(v) => *v,
620 }
621 }
622}
623
624#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
632pub enum FontStyle {
633 #[default]
634 Normal,
635 Italic,
636 Oblique,
637}
638
639impl FontStyle {
640 pub fn parse(s: &str) -> Option<Self> {
641 match s.trim().to_lowercase().as_str() {
642 "normal" => Some(FontStyle::Normal),
643 "italic" => Some(FontStyle::Italic),
644 "oblique" => Some(FontStyle::Oblique),
645 _ => None,
646 }
647 }
648}
649
650#[derive(Debug, Clone, Copy, PartialEq, Default)]
652pub enum TextDecoration {
653 #[default]
654 None,
655 Underline,
656 Overline,
657 LineThrough,
658}
659
660impl TextDecoration {
661 pub fn parse(s: &str) -> Option<Self> {
662 match s.trim().to_lowercase().as_str() {
663 "none" => Some(TextDecoration::None),
664 "underline" => Some(TextDecoration::Underline),
665 "overline" => Some(TextDecoration::Overline),
666 "line-through" => Some(TextDecoration::LineThrough),
667 _ => None,
668 }
669 }
670}
671
672#[derive(Debug, Clone, Copy, PartialEq, Default)]
674pub enum FlexWrap {
675 #[default]
676 NoWrap,
677 Wrap,
678 WrapReverse,
679}
680
681impl FlexWrap {
682 pub fn parse(s: &str) -> Option<Self> {
683 match s.trim().to_lowercase().as_str() {
684 "nowrap" => Some(FlexWrap::NoWrap),
685 "wrap" => Some(FlexWrap::Wrap),
686 "wrap-reverse" => Some(FlexWrap::WrapReverse),
687 _ => None,
688 }
689 }
690}
691
692#[derive(Debug, Clone, Copy, PartialEq, Default)]
694pub enum BoxSizing {
695 #[default]
696 ContentBox,
697 BorderBox,
698}
699
700impl BoxSizing {
701 pub fn parse(s: &str) -> Option<Self> {
702 match s.trim().to_lowercase().as_str() {
703 "content-box" => Some(BoxSizing::ContentBox),
704 "border-box" => Some(BoxSizing::BorderBox),
705 _ => None,
706 }
707 }
708}
709
710#[derive(Debug, Clone, Copy, PartialEq, Default)]
712pub enum Visibility {
713 #[default]
714 Visible,
715 Hidden,
716 Collapse,
717}
718
719impl Visibility {
720 pub fn parse(s: &str) -> Option<Self> {
721 match s.trim().to_lowercase().as_str() {
722 "visible" => Some(Visibility::Visible),
723 "hidden" => Some(Visibility::Hidden),
724 "collapse" => Some(Visibility::Collapse),
725 _ => None,
726 }
727 }
728}
729
730#[derive(Debug, Clone, PartialEq)]
732pub enum TransformOp {
733 TranslateX(Length),
734 TranslateY(Length),
735 Translate(Length, Length),
736 Rotate(f32),
737 ScaleX(f32),
738 ScaleY(f32),
739 Scale(f32, f32),
740 SkewX(f32),
741 SkewY(f32),
742 Skew(f32, f32),
743}
744
745#[derive(Debug, Clone, PartialEq)]
747pub struct Transform {
748 pub operations: Vec<TransformOp>,
749}
750
751impl Transform {
752 pub fn new() -> Self {
753 Self {
754 operations: Vec::new(),
755 }
756 }
757
758 pub fn is_empty(&self) -> bool {
759 self.operations.is_empty()
760 }
761}
762
763impl Default for Transform {
764 fn default() -> Self {
765 Self::new()
766 }
767}
768
769impl Transform {
770 pub fn parse(s: &str) -> Option<Self> {
775 let s = s.trim();
776 if s.is_empty() || s.eq_ignore_ascii_case("none") {
777 return Some(Self::new());
778 }
779 let mut operations = Vec::new();
780 let mut current = String::new();
784 let mut depth = 0usize;
785 for token in s.split_whitespace() {
786 if !current.is_empty() {
787 current.push(' ');
788 }
789 current.push_str(token);
790 depth = depth
791 .saturating_add(token.matches('(').count())
792 .saturating_sub(token.matches(')').count());
793 if depth == 0 {
794 operations.push(TransformOp::parse(¤t)?);
795 current.clear();
796 }
797 }
798 if !current.is_empty() {
799 operations.push(TransformOp::parse(¤t)?);
800 }
801 Some(Transform { operations })
802 }
803}
804
805impl TransformOp {
806 fn parse(s: &str) -> Option<Self> {
807 let s = s.trim();
808 let open = s.find('(')?;
809 let close = s.rfind(')')?;
810 if close < open {
811 return None;
812 }
813 let name = s[..open].trim().to_ascii_lowercase();
814 let args: Vec<&str> = s[open + 1..close].split(',').map(|a| a.trim()).collect();
815
816 match name.as_str() {
817 "translate" => {
818 let x = Length::parse(args.first()?)?;
819 let y = if args.len() > 1 {
820 Length::parse(args[1])?
821 } else {
822 Length::Zero
823 };
824 Some(TransformOp::Translate(x, y))
825 }
826 "translatex" => Some(TransformOp::TranslateX(Length::parse(args.first()?)?)),
827 "translatey" => Some(TransformOp::TranslateY(Length::parse(args.first()?)?)),
828 "rotate" => {
829 let raw = args.first()?.trim();
830 let deg_raw = raw.strip_suffix("deg").unwrap_or(raw).trim();
831 let deg = deg_raw.parse::<f32>().ok()?;
832 Some(TransformOp::Rotate(deg))
833 }
834 "scale" => {
835 let x = args.first()?.trim().parse::<f32>().ok()?;
836 let y = if args.len() > 1 {
837 args[1].trim().parse::<f32>().ok()?
838 } else {
839 x
840 };
841 Some(TransformOp::Scale(x, y))
842 }
843 "scalex" => Some(TransformOp::ScaleX(
844 args.first()?.trim().parse::<f32>().ok()?,
845 )),
846 "scaley" => Some(TransformOp::ScaleY(
847 args.first()?.trim().parse::<f32>().ok()?,
848 )),
849 _ => None,
850 }
851 }
852}
853
854#[derive(Debug, Clone, PartialEq)]
856pub struct BoxShadow {
857 pub offset_x: Length,
858 pub offset_y: Length,
859 pub blur_radius: Length,
860 pub spread_radius: Length,
861 pub color: Color,
862 pub inset: bool,
863}
864
865impl BoxShadow {
866 pub fn parse(s: &str) -> Option<Self> {
869 let s = s.trim();
870 if s.is_empty() || s.eq_ignore_ascii_case("none") {
871 return None;
872 }
873 let mut inset = false;
874 let mut parts: Vec<&str> = Vec::new();
875 for tok in s.split_whitespace() {
876 if tok.eq_ignore_ascii_case("inset") {
877 inset = true;
878 } else {
879 parts.push(tok);
880 }
881 }
882 let offset_x = Length::parse(parts.first()?)?;
884 let offset_y = Length::parse(parts.get(1)?)?;
885 let mut blur: Option<Length> = None;
886 let mut spread: Option<Length> = None;
887 let mut color = Color::BLACK;
888 for tok in &parts[2..] {
889 if let Some(l) = Length::parse(tok) {
890 if blur.is_none() {
891 blur = Some(l);
892 } else if spread.is_none() {
893 spread = Some(l);
894 }
895 } else if let Some(c) = Color::parse(tok) {
896 color = c;
897 }
898 }
899 Some(BoxShadow {
900 offset_x,
901 offset_y,
902 blur_radius: blur.unwrap_or(Length::Zero),
903 spread_radius: spread.unwrap_or(Length::Zero),
904 color,
905 inset,
906 })
907 }
908}
909
910#[derive(Debug, Clone, PartialEq, Default)]
912pub enum TimingFunction {
913 #[default]
914 Linear,
915 Ease,
916 EaseIn,
917 EaseOut,
918 EaseInOut,
919 CubicBezier(f32, f32, f32, f32),
920}
921
922impl TimingFunction {
923 pub fn parse(s: &str) -> Option<Self> {
924 match s.trim().to_ascii_lowercase().as_str() {
925 "linear" => Some(TimingFunction::Linear),
926 "ease" => Some(TimingFunction::Ease),
927 "ease-in" => Some(TimingFunction::EaseIn),
928 "ease-out" => Some(TimingFunction::EaseOut),
929 "ease-in-out" => Some(TimingFunction::EaseInOut),
930 _ => {
931 let s = s.trim();
932 if let Some(inner) = s.strip_prefix("cubic-bezier(") {
933 let inner = inner.trim_end_matches(')');
934 let parts: Vec<f32> = inner
935 .split(',')
936 .map(|p| p.trim().parse::<f32>())
937 .collect::<Result<Vec<_>, _>>()
938 .ok()?;
939 if parts.len() == 4 {
940 return Some(TimingFunction::CubicBezier(
941 parts[0], parts[1], parts[2], parts[3],
942 ));
943 }
944 }
945 None
946 }
947 }
948 }
949}
950
951#[derive(Debug, Clone, PartialEq)]
953pub struct Transition {
954 pub property: String,
956 pub duration: f32,
958 pub timing_function: TimingFunction,
960 pub delay: f32,
962}
963
964impl Default for Transition {
965 fn default() -> Self {
966 Self {
967 property: "all".to_string(),
968 duration: 0.0,
969 timing_function: TimingFunction::Linear,
970 delay: 0.0,
971 }
972 }
973}
974
975impl Transition {
976 pub fn parse(value: &str) -> Option<Self> {
979 let value = value.trim();
980 if value.is_empty() || value.eq_ignore_ascii_case("none") {
981 return None;
982 }
983 let mut t = Transition::default();
984 let mut seen_time = false;
985 for tok in value.split_whitespace() {
986 if let Some(num) = parse_time_seconds(tok) {
987 if !seen_time {
988 t.duration = num;
989 } else {
990 t.delay = num;
991 }
992 seen_time = true;
993 } else if let Some(tf) = TimingFunction::parse(tok) {
994 t.timing_function = tf;
995 } else {
996 t.property = tok.trim_matches(',').to_string();
997 }
998 }
999 if t.property.is_empty() {
1000 t.property = "all".to_string();
1001 }
1002 Some(t)
1003 }
1004}
1005
1006fn parse_time_seconds(s: &str) -> Option<f32> {
1007 if let Some(v) = s.strip_suffix("ms") {
1008 return v.trim().parse::<f32>().ok().map(|ms| ms / 1000.0);
1009 }
1010 if let Some(v) = s.strip_suffix('s') {
1011 return v.trim().parse::<f32>().ok();
1012 }
1013 None
1014}
1015
1016#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1018pub enum Display {
1019 #[default]
1020 Block,
1021 Inline,
1022 InlineBlock,
1023 Flex,
1024 Grid,
1025 None,
1026}
1027
1028impl Display {
1029 pub fn parse(s: &str) -> Option<Self> {
1030 match s.trim().to_lowercase().as_str() {
1031 "block" => Some(Display::Block),
1032 "inline" => Some(Display::Inline),
1033 "inline-block" => Some(Display::InlineBlock),
1034 "flex" => Some(Display::Flex),
1035 "grid" => Some(Display::Grid),
1036 "none" | "hidden" => Some(Display::None),
1037 _ => None,
1038 }
1039 }
1040}
1041
1042#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1044pub enum Position {
1045 #[default]
1046 Static,
1047 Relative,
1048 Absolute,
1049 Fixed,
1050 Sticky,
1051}
1052
1053impl Position {
1054 pub fn parse(s: &str) -> Option<Self> {
1055 match s.trim().to_lowercase().as_str() {
1056 "static" => Some(Position::Static),
1057 "relative" => Some(Position::Relative),
1058 "absolute" => Some(Position::Absolute),
1059 "fixed" => Some(Position::Fixed),
1060 "sticky" => Some(Position::Sticky),
1061 _ => None,
1062 }
1063 }
1064}
1065
1066#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1068pub enum FlexDirection {
1069 #[default]
1070 Row,
1071 RowReverse,
1072 Column,
1073 ColumnReverse,
1074}
1075
1076impl FlexDirection {
1077 pub fn parse(s: &str) -> Option<Self> {
1078 match s.trim().to_lowercase().as_str() {
1079 "row" => Some(FlexDirection::Row),
1080 "row-reverse" => Some(FlexDirection::RowReverse),
1081 "column" => Some(FlexDirection::Column),
1082 "column-reverse" => Some(FlexDirection::ColumnReverse),
1083 _ => None,
1084 }
1085 }
1086}
1087
1088#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1090pub enum JustifyContent {
1091 #[default]
1092 FlexStart,
1093 FlexEnd,
1094 Center,
1095 SpaceBetween,
1096 SpaceAround,
1097 SpaceEvenly,
1098}
1099
1100impl JustifyContent {
1101 pub fn parse(s: &str) -> Option<Self> {
1102 match s.trim().to_lowercase().as_str() {
1103 "flex-start" | "start" => Some(JustifyContent::FlexStart),
1104 "flex-end" | "end" => Some(JustifyContent::FlexEnd),
1105 "center" => Some(JustifyContent::Center),
1106 "space-between" => Some(JustifyContent::SpaceBetween),
1107 "space-around" => Some(JustifyContent::SpaceAround),
1108 "space-evenly" => Some(JustifyContent::SpaceEvenly),
1109 _ => None,
1110 }
1111 }
1112}
1113
1114#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1116pub enum AlignItems {
1117 FlexStart,
1118 FlexEnd,
1119 Center,
1120 Baseline,
1121 #[default]
1122 Stretch,
1123}
1124
1125impl AlignItems {
1126 pub fn parse(s: &str) -> Option<Self> {
1127 match s.trim().to_lowercase().as_str() {
1128 "flex-start" | "start" => Some(AlignItems::FlexStart),
1129 "flex-end" | "end" => Some(AlignItems::FlexEnd),
1130 "center" => Some(AlignItems::Center),
1131 "baseline" => Some(AlignItems::Baseline),
1132 "stretch" => Some(AlignItems::Stretch),
1133 _ => None,
1134 }
1135 }
1136}
1137
1138#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1140pub enum AlignSelf {
1141 #[default]
1142 Auto,
1143 FlexStart,
1144 FlexEnd,
1145 Center,
1146 Baseline,
1147 Stretch,
1148}
1149
1150impl AlignSelf {
1151 pub fn parse(s: &str) -> Option<Self> {
1152 match s.trim().to_lowercase().as_str() {
1153 "auto" => Some(AlignSelf::Auto),
1154 "flex-start" | "start" => Some(AlignSelf::FlexStart),
1155 "flex-end" | "end" => Some(AlignSelf::FlexEnd),
1156 "center" => Some(AlignSelf::Center),
1157 "baseline" => Some(AlignSelf::Baseline),
1158 "stretch" => Some(AlignSelf::Stretch),
1159 _ => None,
1160 }
1161 }
1162
1163 pub fn resolve(&self, parent_align: AlignItems) -> AlignItems {
1165 match self {
1166 AlignSelf::Auto => parent_align,
1167 AlignSelf::FlexStart => AlignItems::FlexStart,
1168 AlignSelf::FlexEnd => AlignItems::FlexEnd,
1169 AlignSelf::Center => AlignItems::Center,
1170 AlignSelf::Baseline => AlignItems::Baseline,
1171 AlignSelf::Stretch => AlignItems::Stretch,
1172 }
1173 }
1174}
1175
1176#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1178pub enum Overflow {
1179 #[default]
1180 Visible,
1181 Hidden,
1182 Scroll,
1183 Auto,
1184}
1185
1186impl Overflow {
1187 pub fn parse(s: &str) -> Option<Self> {
1188 match s.trim().to_lowercase().as_str() {
1189 "visible" => Some(Overflow::Visible),
1190 "hidden" => Some(Overflow::Hidden),
1191 "scroll" => Some(Overflow::Scroll),
1192 "auto" => Some(Overflow::Auto),
1193 _ => None,
1194 }
1195 }
1196}
1197
1198#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1200pub enum WhiteSpace {
1201 #[default]
1202 Normal,
1203 Nowrap,
1204 Pre,
1205 PreWrap,
1206 PreLine,
1207}
1208
1209impl WhiteSpace {
1210 pub fn parse(s: &str) -> Option<Self> {
1211 match s.trim().to_ascii_lowercase().as_str() {
1212 "normal" => Some(WhiteSpace::Normal),
1213 "nowrap" => Some(WhiteSpace::Nowrap),
1214 "pre" => Some(WhiteSpace::Pre),
1215 "pre-wrap" => Some(WhiteSpace::PreWrap),
1216 "pre-line" => Some(WhiteSpace::PreLine),
1217 _ => None,
1218 }
1219 }
1220}
1221
1222#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1224pub enum TextOverflow {
1225 #[default]
1226 Clip,
1227 Ellipsis,
1228}
1229
1230impl TextOverflow {
1231 pub fn parse(s: &str) -> Option<Self> {
1232 match s.trim().to_ascii_lowercase().as_str() {
1233 "clip" => Some(TextOverflow::Clip),
1234 "ellipsis" => Some(TextOverflow::Ellipsis),
1235 _ => None,
1236 }
1237 }
1238}
1239
1240#[derive(Debug, Clone, PartialEq)]
1242pub struct ComputedStyle {
1243 pub display: Display,
1244 pub position: Position,
1245 pub z_index: Option<i32>,
1246 pub opacity: f32,
1247 pub width: Length,
1248 pub height: Length,
1249 pub min_width: Length,
1250 pub min_height: Length,
1251 pub max_width: Length,
1252 pub max_height: Length,
1253 pub margin: Sides<Length>,
1254 pub padding: Sides<Length>,
1255 pub border: Border,
1256 pub border_radius: Sides<Length>,
1257 pub box_sizing: BoxSizing,
1258 pub top: Length,
1259 pub right: Length,
1260 pub bottom: Length,
1261 pub left: Length,
1262 pub flex_direction: FlexDirection,
1263 pub flex_wrap: FlexWrap,
1264 pub justify_content: JustifyContent,
1265 pub align_items: AlignItems,
1266 pub align_self: AlignSelf,
1267 pub flex_grow: f32,
1268 pub flex_shrink: f32,
1269 pub flex_basis: Length,
1270 pub gap: Length,
1271 pub row_gap: Length,
1272 pub column_gap: Length,
1273 pub background_color: Color,
1274 pub background_image: Option<String>,
1275 pub color: Color,
1276 pub font_size: Length,
1277 pub font_family: String,
1278 pub font_weight: FontWeight,
1279 pub font_style: FontStyle,
1284 pub line_height: Option<f32>,
1285 pub letter_spacing: Length,
1286 pub text_align: TextAlign,
1287 pub vertical_align: VerticalAlign,
1289 pub text_decoration: TextDecoration,
1290 pub overflow: Overflow,
1291 pub overflow_x: Overflow,
1292 pub overflow_y: Overflow,
1293 pub visibility: Visibility,
1294 pub transform: Transform,
1295 pub box_shadow: Option<BoxShadow>,
1296 pub white_space: WhiteSpace,
1297 pub text_overflow: TextOverflow,
1298 pub transitions: Vec<Transition>,
1300}
1301
1302impl ComputedStyle {
1303 pub fn new() -> Self {
1304 Self::default()
1305 }
1306
1307 pub fn apply_inline_style(&mut self, style: &str) {
1308 for decl in style.split(';') {
1309 let d = decl.trim();
1310 if d.is_empty() {
1311 continue;
1312 }
1313 if let Some((k, v)) = d.split_once(':') {
1314 self.set_property(k.trim(), v.trim());
1315 }
1316 }
1317 }
1318
1319 pub const PARSED_BUT_UNRENDERED: &[(&str, &str)] = &[
1343 (
1344 "overflow-x",
1345 "arm at style.rs:1662 sets `overflow_x`; no reader looks up `overflow-x`",
1346 ),
1347 (
1348 "overflow-y",
1349 "arm at style.rs:1667 sets `overflow_y`; no reader looks up `overflow-y`",
1350 ),
1351 (
1352 "background-image",
1353 "arm at style.rs:1588 sets `background_image`; the box painter only reads `background`/`background-color`",
1354 ),
1355 (
1356 "font-style",
1357 "arm at style.rs:1621 sets `font_style`; the cascade now propagates it, but NO reader applies it — the renderer's `TextRenderConfig.font_style` is only ever set by `Default`, never copied from here",
1358 ),
1359 (
1360 "letter-spacing",
1361 "arm at style.rs:1633 sets `letter_spacing`; cascade-inheritable, but no reader applies it",
1362 ),
1363 (
1364 "visibility",
1365 "arm at style.rs:1674 sets `visibility`; NO reader looks up `visibility`, so `visibility: hidden` hides nothing",
1366 ),
1367 (
1368 "transform",
1369 "arm at style.rs:1681 sets `transform`; read at layout.rs:3695 ONLY to force a stacking context — no visual effect",
1370 ),
1371 (
1372 "box-shadow",
1373 "arm at style.rs:1688 sets `box_shadow`; the string name appears nowhere in the renderer",
1374 ),
1375 (
1376 "transition",
1377 "arm at style.rs:1693 pushes to `transitions`; no reader plays a transition",
1378 ),
1379 (
1380 "border-style",
1381 "arm at style.rs:1720 sets `border.style.*`; only `border`/`border-width` are read, so `border-style` alone does nothing",
1382 ),
1383 (
1384 "border-color",
1385 "arm at style.rs:1728 sets `border.color.*`; only `border`/`border-width` are read, so `border-color` alone does nothing",
1386 ),
1387 ];
1388
1389 pub fn set_property(&mut self, prop: &str, value: &str) {
1390 let prop_lower = prop.to_lowercase();
1391
1392 match prop_lower.as_str() {
1393 "display" => {
1395 if let Some(d) = Display::parse(value) {
1396 self.display = d;
1397 }
1398 }
1399
1400 "position" => {
1402 if let Some(p) = Position::parse(value) {
1403 self.position = p;
1404 }
1405 }
1406 "z-index" | "zindex" => {
1407 if let Ok(z) = value.parse::<i32>() {
1408 self.z_index = Some(z);
1409 }
1410 }
1411 "opacity" => {
1412 if let Ok(o) = value.parse::<f32>() {
1413 self.opacity = o.clamp(0.0, 1.0);
1414 }
1415 }
1416
1417 "width" => {
1419 if let Some(l) = Length::parse(value) {
1420 self.width = l;
1421 }
1422 }
1423 "height" => {
1424 if let Some(l) = Length::parse(value) {
1425 self.height = l;
1426 }
1427 }
1428 "min-width" => {
1429 if let Some(l) = Length::parse(value) {
1430 self.min_width = l;
1431 }
1432 }
1433 "min-height" => {
1434 if let Some(l) = Length::parse(value) {
1435 self.min_height = l;
1436 }
1437 }
1438 "max-width" => {
1439 if let Some(l) = Length::parse(value) {
1440 self.max_width = l;
1441 }
1442 }
1443 "max-height" => {
1444 if let Some(l) = Length::parse(value) {
1445 self.max_height = l;
1446 }
1447 }
1448
1449 "margin" => {
1451 if let Some(sides) = parse_sides_shorthand(value, Length::parse) {
1452 self.margin = sides;
1453 }
1454 }
1455 "margin-top" => {
1456 if let Some(l) = Length::parse(value) {
1457 self.margin.top = l;
1458 }
1459 }
1460 "margin-right" => {
1461 if let Some(l) = Length::parse(value) {
1462 self.margin.right = l;
1463 }
1464 }
1465 "margin-bottom" => {
1466 if let Some(l) = Length::parse(value) {
1467 self.margin.bottom = l;
1468 }
1469 }
1470 "margin-left" => {
1471 if let Some(l) = Length::parse(value) {
1472 self.margin.left = l;
1473 }
1474 }
1475 "padding" => {
1476 if let Some(sides) = parse_sides_shorthand(value, Length::parse) {
1477 self.padding = sides;
1478 }
1479 }
1480 "padding-top" => {
1481 if let Some(l) = Length::parse(value) {
1482 self.padding.top = l;
1483 }
1484 }
1485 "padding-right" => {
1486 if let Some(l) = Length::parse(value) {
1487 self.padding.right = l;
1488 }
1489 }
1490 "padding-bottom" => {
1491 if let Some(l) = Length::parse(value) {
1492 self.padding.bottom = l;
1493 }
1494 }
1495 "padding-left" => {
1496 if let Some(l) = Length::parse(value) {
1497 self.padding.left = l;
1498 }
1499 }
1500
1501 "top" => {
1503 if let Some(l) = Length::parse(value) {
1504 self.top = l;
1505 }
1506 }
1507 "right" => {
1508 if let Some(l) = Length::parse(value) {
1509 self.right = l;
1510 }
1511 }
1512 "bottom" => {
1513 if let Some(l) = Length::parse(value) {
1514 self.bottom = l;
1515 }
1516 }
1517 "left" => {
1518 if let Some(l) = Length::parse(value) {
1519 self.left = l;
1520 }
1521 }
1522
1523 "flex-direction" => {
1525 if let Some(fd) = FlexDirection::parse(value) {
1526 self.flex_direction = fd;
1527 }
1528 }
1529 "flex-wrap" => {
1530 if let Some(fw) = FlexWrap::parse(value) {
1531 self.flex_wrap = fw;
1532 }
1533 }
1534 "justify-content" => {
1535 if let Some(jc) = JustifyContent::parse(value) {
1536 self.justify_content = jc;
1537 }
1538 }
1539 "align-items" => {
1540 if let Some(ai) = AlignItems::parse(value) {
1541 self.align_items = ai;
1542 }
1543 }
1544 "align-self" => {
1545 if let Some(as_) = AlignSelf::parse(value) {
1546 self.align_self = as_;
1547 }
1548 }
1549 "flex-grow" => {
1550 if let Ok(v) = value.parse::<f32>() {
1551 self.flex_grow = v;
1552 }
1553 }
1554 "flex-shrink" => {
1555 if let Ok(v) = value.parse::<f32>() {
1556 self.flex_shrink = v;
1557 }
1558 }
1559 "flex-basis" => {
1560 if let Some(l) = Length::parse(value) {
1561 self.flex_basis = l;
1562 }
1563 }
1564 "gap" | "grid-gap" => {
1565 if let Some(l) = Length::parse(value) {
1566 self.gap = l;
1567 self.row_gap = l;
1568 self.column_gap = l;
1569 }
1570 }
1571 "row-gap" => {
1572 if let Some(l) = Length::parse(value) {
1573 self.row_gap = l;
1574 }
1575 }
1576 "column-gap" => {
1577 if let Some(l) = Length::parse(value) {
1578 self.column_gap = l;
1579 }
1580 }
1581
1582 "background-color" => {
1584 if let Some(c) = Color::parse(value) {
1585 self.background_color = c;
1586 }
1587 }
1588 "background-image" => {
1589 self.background_image = Some(value.to_string());
1590 }
1591 "background" => {
1592 for token in value.split_whitespace() {
1595 if let Some(c) = Color::parse(token) {
1596 self.background_color = c;
1597 break;
1598 }
1599 }
1600 }
1601
1602 "color" => {
1604 if let Some(c) = Color::parse(value) {
1605 self.color = c;
1606 }
1607 }
1608 "font-size" => {
1609 if let Some(l) = Length::parse(value) {
1610 self.font_size = l;
1611 }
1612 }
1613 "font-family" => {
1614 self.font_family = value.to_string();
1615 }
1616 "font-weight" => {
1617 if let Some(fw) = FontWeight::parse(value) {
1618 self.font_weight = fw;
1619 }
1620 }
1621 "font-style" => {
1622 if let Some(fs) = FontStyle::parse(value) {
1623 self.font_style = fs;
1624 }
1625 }
1626 "line-height" => {
1627 if let Ok(lh) = value.parse::<f32>() {
1628 self.line_height = Some(lh);
1629 } else if let Some(Length::Px(px)) = Length::parse(value) {
1630 self.line_height = Some(px);
1631 }
1632 }
1633 "letter-spacing" => {
1634 if let Some(l) = Length::parse(value) {
1635 self.letter_spacing = l;
1636 }
1637 }
1638 "text-align" => {
1639 if let Some(ta) = TextAlign::parse(value) {
1640 self.text_align = ta;
1641 }
1642 }
1643 "vertical-align" => {
1644 if let Some(va) = VerticalAlign::parse(value) {
1645 self.vertical_align = va;
1646 }
1647 }
1648 "text-decoration" => {
1649 if let Some(td) = TextDecoration::parse(value) {
1650 self.text_decoration = td;
1651 }
1652 }
1653
1654 "overflow" => {
1656 if let Some(o) = Overflow::parse(value) {
1657 self.overflow = o;
1658 self.overflow_x = o;
1659 self.overflow_y = o;
1660 }
1661 }
1662 "overflow-x" => {
1663 if let Some(o) = Overflow::parse(value) {
1664 self.overflow_x = o;
1665 }
1666 }
1667 "overflow-y" => {
1668 if let Some(o) = Overflow::parse(value) {
1669 self.overflow_y = o;
1670 }
1671 }
1672
1673 "visibility" => {
1675 if let Some(v) = Visibility::parse(value) {
1676 self.visibility = v;
1677 }
1678 }
1679
1680 "transform" => {
1682 if let Some(t) = Transform::parse(value) {
1683 self.transform = t;
1684 }
1685 }
1686
1687 "box-shadow" => {
1689 self.box_shadow = BoxShadow::parse(value);
1690 }
1691
1692 "transition" => {
1694 self.transitions.clear();
1695 for part in value.split(',') {
1696 let part = part.trim();
1697 if part.is_empty() || part.eq_ignore_ascii_case("none") {
1698 continue;
1699 }
1700 if let Some(t) = Transition::parse(part) {
1701 self.transitions.push(t);
1702 }
1703 }
1704 }
1705
1706 "border" => {
1708 if let Some(sides) = parse_border_shorthand(value) {
1709 self.border = sides;
1710 }
1711 }
1712 "border-width" => {
1713 if let Some(sides) = parse_sides_shorthand(value, Length::parse) {
1714 self.border.width.top = sides.top;
1715 self.border.width.right = sides.right;
1716 self.border.width.bottom = sides.bottom;
1717 self.border.width.left = sides.left;
1718 }
1719 }
1720 "border-style" => {
1721 if let Some(sides) = parse_sides_shorthand(value, BorderStyle::parse) {
1722 self.border.style.top = sides.top;
1723 self.border.style.right = sides.right;
1724 self.border.style.bottom = sides.bottom;
1725 self.border.style.left = sides.left;
1726 }
1727 }
1728 "border-color" => {
1729 if let Some(sides) = parse_sides_shorthand(value, Color::parse) {
1730 self.border.color.top = sides.top;
1731 self.border.color.right = sides.right;
1732 self.border.color.bottom = sides.bottom;
1733 self.border.color.left = sides.left;
1734 }
1735 }
1736
1737 "border-radius" => {
1739 if let Some(sides) = parse_sides_shorthand(value, Length::parse) {
1740 self.border_radius = sides;
1741 }
1742 }
1743
1744 "box-sizing" => {
1746 if let Some(bs) = BoxSizing::parse(value) {
1747 self.box_sizing = bs;
1748 }
1749 }
1750 "white-space" => {
1751 if let Some(ws) = WhiteSpace::parse(value) {
1752 self.white_space = ws;
1753 }
1754 }
1755 "text-overflow" => {
1756 if let Some(to) = TextOverflow::parse(value) {
1757 self.text_overflow = to;
1758 }
1759 }
1760
1761 _ => {}
1762 }
1763 }
1764
1765 pub fn creates_stacking_context(&self) -> bool {
1767 self.position != Position::Static
1768 || self.z_index.is_some()
1769 || self.opacity < 1.0
1770 || !self.transform.is_empty()
1771 }
1772
1773 pub fn is_display_none(&self) -> bool {
1775 self.display == Display::None
1776 }
1777
1778 pub fn is_hidden(&self) -> bool {
1780 self.visibility == Visibility::Hidden
1781 }
1782}
1783
1784pub fn is_viewport_filling(style: &ComputedStyle, is_root_index: bool) -> bool {
1788 if is_root_index {
1789 return true;
1790 }
1791 let is_100 = |l: Length| match l {
1792 Length::Percent(v) => (v - 100.0).abs() < 0.01,
1793 Length::Vw(v) => (v - 100.0).abs() < 0.01,
1794 Length::Dvw(v) => (v - 100.0).abs() < 0.01,
1795 Length::Vh(v) => (v - 100.0).abs() < 0.01,
1796 Length::Dvh(v) => (v - 100.0).abs() < 0.01,
1797 _ => false,
1798 };
1799 let has_100pct_w = is_100(style.width) && matches!(style.width, Length::Percent(_));
1800 let has_vw = matches!(
1801 style.width,
1802 Length::Vw(v) | Length::Dvw(v) if (v - 100.0).abs() < 0.01
1803 );
1804 let has_viewport_h = is_100(style.height)
1805 && matches!(
1806 style.height,
1807 Length::Percent(_) | Length::Vh(_) | Length::Dvh(_)
1808 )
1809 || matches!(
1810 style.min_height,
1811 Length::Percent(v) | Length::Vh(v) | Length::Dvh(v) if (v - 100.0).abs() < 0.01
1812 );
1813 (has_100pct_w || has_vw) && has_viewport_h
1815}
1816
1817pub fn root_is_viewport_filling(index: usize) -> bool {
1819 index == 0
1820}
1821
1822impl Default for ComputedStyle {
1825 fn default() -> Self {
1826 Self {
1827 display: Display::default(),
1828 position: Position::default(),
1829 z_index: None,
1830 opacity: 1.0,
1831 width: Length::default(),
1832 height: Length::default(),
1833 min_width: Length::default(),
1834 min_height: Length::default(),
1835 max_width: Length::default(),
1836 max_height: Length::default(),
1837 margin: Sides::default(),
1838 padding: Sides::default(),
1839 border: Border::default(),
1840 border_radius: Sides::default(),
1841 box_sizing: BoxSizing::default(),
1842 top: Length::default(),
1843 right: Length::default(),
1844 bottom: Length::default(),
1845 left: Length::default(),
1846 flex_direction: FlexDirection::default(),
1847 flex_wrap: FlexWrap::default(),
1848 justify_content: JustifyContent::default(),
1849 align_items: AlignItems::default(),
1850 align_self: AlignSelf::default(),
1851 flex_grow: 0.0,
1852 flex_shrink: 1.0,
1853 flex_basis: Length::Auto,
1854 gap: Length::default(),
1855 row_gap: Length::default(),
1856 column_gap: Length::default(),
1857 background_color: Color::default(),
1858 background_image: None,
1859 color: Color::default(),
1860 font_size: Length::Px(16.0), font_family: "sans-serif".to_string(),
1862 font_weight: FontWeight::default(),
1863 font_style: FontStyle::default(),
1864 line_height: None,
1865 letter_spacing: Length::default(),
1866 text_align: TextAlign::default(),
1867 vertical_align: VerticalAlign::default(),
1868 text_decoration: TextDecoration::default(),
1869 overflow: Overflow::default(),
1870 overflow_x: Overflow::default(),
1871 overflow_y: Overflow::default(),
1872 visibility: Visibility::default(),
1873 transform: Transform::default(),
1874 box_shadow: None,
1875 white_space: WhiteSpace::default(),
1876 text_overflow: TextOverflow::default(),
1877 transitions: Vec::new(),
1878 }
1879 }
1880}
1881
1882fn parse_sides_shorthand<T: Copy + Default>(
1884 value: &str,
1885 parser: impl Fn(&str) -> Option<T>,
1886) -> Option<Sides<T>> {
1887 let parts: Vec<&str> = value.split_whitespace().collect();
1888 match parts.len() {
1889 1 => {
1890 let v = parser(parts[0])?;
1891 Some(Sides::all(v))
1892 }
1893 2 => {
1894 let v = parser(parts[0])?;
1895 let h = parser(parts[1])?;
1896 Some(Sides {
1897 top: v,
1898 right: h,
1899 bottom: v,
1900 left: h,
1901 })
1902 }
1903 3 => {
1904 let top = parser(parts[0])?;
1905 let h = parser(parts[1])?;
1906 let bottom = parser(parts[2])?;
1907 Some(Sides {
1908 top,
1909 right: h,
1910 bottom,
1911 left: h,
1912 })
1913 }
1914 4 => {
1915 let top = parser(parts[0])?;
1916 let right = parser(parts[1])?;
1917 let bottom = parser(parts[2])?;
1918 let left = parser(parts[3])?;
1919 Some(Sides {
1920 top,
1921 right,
1922 bottom,
1923 left,
1924 })
1925 }
1926 _ => None,
1927 }
1928}
1929
1930fn parse_border_shorthand(value: &str) -> Option<Border> {
1931 let parts: Vec<&str> = value.split_whitespace().collect();
1932 if parts.is_empty() {
1933 return None;
1934 }
1935
1936 let mut width_parts = Vec::new();
1938 for part in &parts {
1939 if let Some(w) = Length::parse(part) {
1940 width_parts.push(w);
1941 } else {
1942 break;
1943 }
1944 }
1945
1946 let mut style_parts = Vec::new();
1948 for part in &parts[width_parts.len()..] {
1949 if let Some(s) = BorderStyle::parse(part) {
1950 style_parts.push(s);
1951 } else {
1952 break;
1953 }
1954 }
1955
1956 let color_start = width_parts.len() + style_parts.len();
1958 let mut color_parts = Vec::new();
1959 for part in &parts[color_start..] {
1960 if let Some(c) = Color::parse(part) {
1961 color_parts.push(c);
1962 } else {
1963 break;
1964 }
1965 }
1966
1967 let default_width = Length::Px(3.0);
1969 let width_sides = if width_parts.len() == 1 {
1970 Sides::all(width_parts[0])
1971 } else if width_parts.len() == 2 {
1972 Sides {
1973 top: width_parts[0],
1974 right: width_parts[1],
1975 bottom: width_parts[0],
1976 left: width_parts[1],
1977 }
1978 } else if width_parts.len() == 3 {
1979 Sides {
1980 top: width_parts[0],
1981 right: width_parts[1],
1982 bottom: width_parts[2],
1983 left: width_parts[1],
1984 }
1985 } else if width_parts.len() == 4 {
1986 Sides {
1987 top: width_parts[0],
1988 right: width_parts[1],
1989 bottom: width_parts[2],
1990 left: width_parts[3],
1991 }
1992 } else {
1993 Sides::all(default_width)
1994 };
1995
1996 let default_style = BorderStyle::None;
2004 let style_sides = if style_parts.len() == 1 {
2005 Sides::all(style_parts[0])
2006 } else if style_parts.len() == 2 {
2007 Sides {
2008 top: style_parts[0],
2009 right: style_parts[1],
2010 bottom: style_parts[0],
2011 left: style_parts[1],
2012 }
2013 } else if style_parts.len() == 3 {
2014 Sides {
2015 top: style_parts[0],
2016 right: style_parts[1],
2017 bottom: style_parts[2],
2018 left: style_parts[1],
2019 }
2020 } else if style_parts.len() == 4 {
2021 Sides {
2022 top: style_parts[0],
2023 right: style_parts[1],
2024 bottom: style_parts[2],
2025 left: style_parts[3],
2026 }
2027 } else {
2028 Sides::all(default_style)
2029 };
2030
2031 let default_color = Color::BLACK;
2033 let color_sides = if color_parts.len() == 1 {
2034 Sides::all(color_parts[0])
2035 } else if color_parts.len() == 2 {
2036 Sides {
2037 top: color_parts[0],
2038 right: color_parts[1],
2039 bottom: color_parts[0],
2040 left: color_parts[1],
2041 }
2042 } else if color_parts.len() == 3 {
2043 Sides {
2044 top: color_parts[0],
2045 right: color_parts[1],
2046 bottom: color_parts[2],
2047 left: color_parts[1],
2048 }
2049 } else if color_parts.len() == 4 {
2050 Sides {
2051 top: color_parts[0],
2052 right: color_parts[1],
2053 bottom: color_parts[2],
2054 left: color_parts[3],
2055 }
2056 } else {
2057 Sides::all(default_color)
2058 };
2059
2060 Some(Border {
2061 width: width_sides,
2062 style: style_sides,
2063 color: color_sides,
2064 })
2065}
2066
2067pub fn resolve_auto_margins_core(
2082 avail_w: f32,
2083 outer_w: f32,
2084 ml: f32,
2085 mr: f32,
2086 ml_auto: bool,
2087 mr_auto: bool,
2088) -> (f32, f32) {
2089 let fixed = (if ml_auto { 0.0 } else { ml }) + (if mr_auto { 0.0 } else { mr });
2090 let free = avail_w - outer_w - fixed;
2091 match (ml_auto, mr_auto) {
2092 (true, true) => {
2093 let each = free / 2.0;
2094 (each, each)
2095 }
2096 (true, false) => (free, mr),
2097 (false, true) => (ml, free),
2098 (false, false) => (ml, mr),
2099 }
2100}
2101
2102pub fn resolve_auto_margins(
2116 avail_w: f32,
2117 style: &ComputedStyle,
2118 declared_w: Option<f32>,
2119) -> (f32, f32) {
2120 let px = |l: Length| l.to_px(avail_w, 16.0, (avail_w, avail_w));
2121 let ml = px(style.margin.left);
2122 let mr = px(style.margin.right);
2123 let ml_auto = style.margin.left.is_auto();
2124 let mr_auto = style.margin.right.is_auto();
2125 if !ml_auto && !mr_auto {
2126 return (ml, mr);
2127 }
2128 let Some(dw) = declared_w else {
2129 return (
2130 if ml_auto { 0.0 } else { ml },
2131 if mr_auto { 0.0 } else { mr },
2132 );
2133 };
2134 let outer_w = match style.box_sizing {
2137 BoxSizing::BorderBox => dw,
2138 BoxSizing::ContentBox => {
2139 dw + px(style.padding.left)
2140 + px(style.padding.right)
2141 + px(style.border.width.left)
2142 + px(style.border.width.right)
2143 }
2144 };
2145 resolve_auto_margins_core(avail_w, outer_w, ml, mr, ml_auto, mr_auto)
2146}
2147
2148#[cfg(test)]
2149mod tests {
2150 use super::*;
2151
2152 #[test]
2153 fn test_length_parsing() {
2154 assert_eq!(Length::parse("10px"), Some(Length::Px(10.0)));
2155 assert_eq!(Length::parse("50%"), Some(Length::Percent(50.0)));
2156 assert_eq!(Length::parse("1.5rem"), Some(Length::Rem(1.5)));
2157 assert_eq!(Length::parse("2em"), Some(Length::Em(2.0)));
2158 assert_eq!(Length::parse("auto"), Some(Length::Auto));
2159 assert_eq!(Length::parse("0"), Some(Length::Zero));
2160 }
2161
2162 #[test]
2163 fn test_color_parsing() {
2164 assert_eq!(Color::parse("#ff0000"), Some(Color::RED));
2165 assert_eq!(Color::parse("#f00"), Some(Color::RED));
2166 assert_eq!(Color::parse("red"), Some(Color::RED));
2167 assert_eq!(Color::parse("rgb(255, 0, 0)"), Some(Color::RED));
2168 assert_eq!(Color::parse("rgba(255, 0, 0, 1.0)"), Some(Color::RED));
2169 }
2170
2171 #[test]
2172 fn test_display_hidden_alias() {
2173 assert_eq!(Display::parse("none"), Some(Display::None));
2174 assert_eq!(Display::parse("hidden"), Some(Display::None));
2175 }
2176
2177 #[test]
2178 fn test_position_parsing() {
2179 assert_eq!(Position::parse("relative"), Some(Position::Relative));
2180 assert_eq!(Position::parse("absolute"), Some(Position::Absolute));
2181 assert_eq!(Position::parse("fixed"), Some(Position::Fixed));
2182 assert_eq!(Position::parse("static"), Some(Position::Static));
2183 }
2184
2185 #[test]
2186 fn test_overflow_parsing() {
2187 assert_eq!(Overflow::parse("hidden"), Some(Overflow::Hidden));
2188 assert_eq!(Overflow::parse("scroll"), Some(Overflow::Scroll));
2189 assert_eq!(Overflow::parse("auto"), Some(Overflow::Auto));
2190 assert_eq!(Overflow::parse("visible"), Some(Overflow::Visible));
2191 }
2192
2193 #[test]
2194 fn test_set_property_position_overflow_zindex() {
2195 let mut cs = ComputedStyle::new();
2196 cs.set_property("position", "absolute");
2197 cs.set_property("overflow", "hidden");
2198 cs.set_property("z-index", "10");
2199 assert_eq!(cs.position, Position::Absolute);
2200 assert_eq!(cs.overflow, Overflow::Hidden);
2201 assert_eq!(cs.z_index, Some(10));
2202 }
2203
2204 #[test]
2205 fn test_set_property_transform() {
2206 let mut cs = ComputedStyle::new();
2207 cs.set_property(
2208 "transform",
2209 "translate(10px, 20px) rotate(45deg) scale(1.5)",
2210 );
2211 assert_eq!(
2212 cs.transform.operations,
2213 vec![
2214 TransformOp::Translate(Length::Px(10.0), Length::Px(20.0)),
2215 TransformOp::Rotate(45.0),
2216 TransformOp::Scale(1.5, 1.5),
2217 ]
2218 );
2219 cs.set_property("transform", "translateX(5px) scaleY(2)");
2221 assert_eq!(
2222 cs.transform.operations,
2223 vec![
2224 TransformOp::TranslateX(Length::Px(5.0)),
2225 TransformOp::ScaleY(2.0)
2226 ]
2227 );
2228 cs.set_property("transform", "none");
2229 assert!(cs.transform.is_empty());
2230 }
2231
2232 #[test]
2233 fn test_set_property_box_shadow() {
2234 let mut cs = ComputedStyle::new();
2235 cs.set_property("box-shadow", "2px 3px 4px rgba(0,0,0,0.5)");
2236 let bs = cs.box_shadow.as_ref().expect("box shadow should be set");
2237 assert_eq!(bs.offset_x, Length::Px(2.0));
2238 assert_eq!(bs.offset_y, Length::Px(3.0));
2239 assert_eq!(bs.blur_radius, Length::Px(4.0));
2240 assert!(!bs.inset);
2241 cs.set_property("box-shadow", "inset 0 2px #000");
2243 let bs = cs.box_shadow.as_ref().expect("inset shadow set");
2244 assert!(bs.inset);
2245 assert_eq!(bs.color, Color::BLACK);
2246 cs.set_property("box-shadow", "none");
2248 assert!(cs.box_shadow.is_none());
2249 }
2250
2251 #[test]
2252 fn test_set_property_transition() {
2253 let mut cs = ComputedStyle::new();
2254 cs.set_property("transition", "opacity 0.2s ease-in 0.1s, transform 300ms");
2255 assert_eq!(cs.transitions.len(), 2);
2256 assert_eq!(cs.transitions[0].property, "opacity");
2257 assert_eq!(cs.transitions[0].duration, 0.2);
2258 assert_eq!(cs.transitions[0].timing_function, TimingFunction::EaseIn);
2259 assert_eq!(cs.transitions[0].delay, 0.1);
2260 assert_eq!(cs.transitions[1].property, "transform");
2261 assert_eq!(cs.transitions[1].duration, 0.3);
2262 cs.set_property("transition", "none");
2264 assert!(cs.transitions.is_empty());
2265 }
2266
2267 #[test]
2268 fn test_margin_shorthand_preserves_auto() {
2269 let mut cs = ComputedStyle::new();
2271 cs.set_property("margin", "0 auto");
2272 assert_eq!(cs.margin.top, Length::Zero);
2273 assert_eq!(cs.margin.bottom, Length::Zero);
2274 assert_eq!(cs.margin.left, Length::Auto);
2275 assert_eq!(cs.margin.right, Length::Auto);
2276
2277 let mut cs = ComputedStyle::new();
2279 cs.set_property("margin", "10px auto");
2280 assert_eq!(cs.margin.top, Length::Px(10.0));
2281 assert_eq!(cs.margin.bottom, Length::Px(10.0));
2282 assert_eq!(cs.margin.left, Length::Auto);
2283 assert_eq!(cs.margin.right, Length::Auto);
2284
2285 let mut cs = ComputedStyle::new();
2287 cs.set_property("margin", "1px auto 3px auto");
2288 assert_eq!(cs.margin.top, Length::Px(1.0));
2289 assert_eq!(cs.margin.bottom, Length::Px(3.0));
2290 assert_eq!(cs.margin.left, Length::Auto);
2291 assert_eq!(cs.margin.right, Length::Auto);
2292
2293 let mut cs = ComputedStyle::new();
2295 cs.set_property("margin", "8px");
2296 cs.set_property("margin-left", "auto");
2297 assert_eq!(cs.margin.top, Length::Px(8.0));
2298 assert_eq!(cs.margin.left, Length::Auto);
2299 }
2300
2301 #[test]
2302 fn test_background_shorthand_parses_color() {
2303 let mut cs = ComputedStyle::new();
2304 cs.set_property("background", "#1a1a2e");
2305 assert_eq!(cs.background_color, Color::new(0x1a, 0x1a, 0x2e, 255));
2306
2307 let mut cs = ComputedStyle::new();
2309 cs.set_property("background", "red no-repeat");
2310 assert_eq!(cs.background_color, Color::RED);
2311
2312 let mut cs = ComputedStyle::new();
2314 cs.set_property("background", "url(img.png)");
2315 assert_eq!(cs.background_color, Color::default());
2316 }
2317
2318 #[test]
2319 fn test_border_shorthand_width_style_color() {
2320 let mut cs = ComputedStyle::new();
2321 cs.set_property("border", "1px solid #fff");
2322 assert_eq!(cs.border.width.top, Length::Px(1.0));
2323 assert_eq!(cs.border.width.right, Length::Px(1.0));
2324 assert_eq!(cs.border.style.top, BorderStyle::Solid);
2325 assert_eq!(cs.border.color.top, Color::new(255, 255, 255, 255));
2326
2327 let mut cs = ComputedStyle::new();
2329 cs.set_property("border", "4px solid");
2330 assert_eq!(cs.border.width.top, Length::Px(4.0));
2331 assert_eq!(cs.border.style.top, BorderStyle::Solid);
2332 assert_eq!(cs.border.color.top, Color::BLACK);
2333 }
2334
2335 #[test]
2339 fn border_width_without_a_style_is_not_a_visible_border() {
2340 let mut cs = ComputedStyle::new();
2341 cs.set_property("border", "1px");
2342 assert_eq!(
2343 cs.border.style.top,
2344 BorderStyle::None,
2345 "`border: 1px` must not fabricate a style; the initial value of \
2346 `border-style` is `none`, so a bare width paints nothing"
2347 );
2348 assert_eq!(cs.border.width.top, Length::Px(1.0));
2352
2353 for value in ["1px", "2px", "3px", "1px red", "2px #00ff00"] {
2355 let mut cs = ComputedStyle::new();
2356 cs.set_property("border", value);
2357 assert_eq!(
2358 cs.border.style.top,
2359 BorderStyle::None,
2360 "`border: {value}` carries no style keyword and must stay `none`"
2361 );
2362 }
2363
2364 let mut cs = ComputedStyle::new();
2367 cs.set_property("border", "2px");
2368 cs.set_property("border-style", "solid");
2369 assert_eq!(cs.border.width.top, Length::Px(2.0));
2370 assert_eq!(cs.border.style.top, BorderStyle::Solid);
2371
2372 for (value, expected) in [
2374 ("1px solid red", BorderStyle::Solid),
2375 ("1px dashed", BorderStyle::Dashed),
2376 ("1px dotted", BorderStyle::Dotted),
2377 ("1px none", BorderStyle::None),
2378 ] {
2379 let mut cs = ComputedStyle::new();
2380 cs.set_property("border", value);
2381 assert_eq!(cs.border.style.top, expected, "`border: {value}`");
2382 }
2383 }
2384
2385 #[test]
2386 fn test_resolve_auto_margins_both_sides() {
2387 let mut cs = ComputedStyle::new();
2388 cs.set_property("margin", "0 auto");
2389 let (ml, mr) = resolve_auto_margins(800.0, &cs, Some(200.0));
2390 assert_eq!(ml, 300.0);
2391 assert_eq!(mr, 300.0);
2392 }
2393
2394 #[test]
2395 fn test_resolve_auto_margins_single_side_subtracts_fixed_opposite() {
2396 let mut cs = ComputedStyle::new();
2397 cs.set_property("margin-left", "auto");
2398 cs.set_property("margin-right", "40px");
2399 let (ml, mr) = resolve_auto_margins(800.0, &cs, Some(200.0));
2400 assert_eq!(ml, 560.0); assert_eq!(mr, 40.0);
2402 }
2403
2404 #[test]
2405 fn test_resolve_auto_margins_content_box_subtracts_padding_border_once() {
2406 let mut cs = ComputedStyle::new();
2407 cs.set_property("margin", "0 auto");
2408 cs.set_property("padding", "0 20px");
2409 cs.set_property("border", "2px solid");
2410 let (ml, mr) = resolve_auto_margins(800.0, &cs, Some(200.0));
2412 assert_eq!(ml, 278.0);
2413 assert_eq!(mr, 278.0);
2414 }
2415
2416 #[test]
2417 fn test_resolve_auto_margins_border_box_no_double_subtraction() {
2418 let mut cs = ComputedStyle::new();
2419 cs.set_property("box-sizing", "border-box");
2420 cs.set_property("margin", "0 auto");
2421 cs.set_property("padding", "0 20px");
2422 let (ml, mr) = resolve_auto_margins(800.0, &cs, Some(200.0));
2424 assert_eq!(ml, 300.0);
2425 assert_eq!(mr, 300.0);
2426 }
2427
2428 #[test]
2429 fn test_resolve_auto_margins_declared_none_resolves_zero() {
2430 let mut cs = ComputedStyle::new();
2431 cs.set_property("margin", "0 auto");
2432 let (ml, mr) = resolve_auto_margins(800.0, &cs, None);
2433 assert_eq!((ml, mr), (0.0, 0.0));
2434 }
2435
2436 #[test]
2437 fn test_resolve_auto_margins_negative_free_space_resolves_negative() {
2438 let mut cs = ComputedStyle::new();
2441 cs.set_property("margin", "0 auto");
2442 let (ml, mr) = resolve_auto_margins(800.0, &cs, Some(900.0));
2443 assert_eq!((ml, mr), (-50.0, -50.0)); let mut cs = ComputedStyle::new();
2447 cs.set_property("margin-left", "auto");
2448 let (ml, mr) = resolve_auto_margins(800.0, &cs, Some(900.0));
2449 assert_eq!((ml, mr), (-100.0, 0.0));
2450 }
2451
2452 #[test]
2453 fn test_resolve_auto_margins_without_auto_returns_declared() {
2454 let mut cs = ComputedStyle::new();
2455 cs.set_property("margin", "10px 20px");
2457 let (ml, mr) = resolve_auto_margins(800.0, &cs, Some(200.0));
2458 assert_eq!((ml, mr), (20.0, 20.0));
2459 }
2460
2461 #[test]
2462 fn test_resolve_auto_margins_percent_padding_resolves_against_avail() {
2463 let mut cs = ComputedStyle::new();
2464 cs.set_property("margin", "0 auto");
2465 cs.set_property("padding", "0 10%");
2466 let (ml, mr) = resolve_auto_margins(800.0, &cs, Some(200.0));
2468 assert_eq!(ml, 220.0);
2469 assert_eq!(mr, 220.0);
2470 }
2471}