1use std::collections::HashMap;
4use std::fmt::Write;
5use std::hash::{Hash, Hasher};
6
7pub trait ToCss {
9 fn to_css(&self, buf: &mut String);
11
12 fn to_css_string(&self) -> String {
14 let mut buf = String::new();
15 self.to_css(&mut buf);
16 buf
17 }
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
22pub struct StyleId(pub u32);
23
24impl StyleId {
25 pub const DEFAULT: StyleId = StyleId(0);
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
31pub struct FontWeight(pub u16);
32
33impl FontWeight {
34 pub const NORMAL: FontWeight = FontWeight(400);
35 pub const BOLD: FontWeight = FontWeight(700);
36}
37
38impl ToCss for FontWeight {
39 fn to_css(&self, buf: &mut String) {
40 match self.0 {
41 400 => buf.push_str("normal"),
42 700 => buf.push_str("bold"),
43 w => write!(buf, "{}", w).unwrap(),
44 }
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
50pub enum FontStyle {
51 #[default]
52 Normal,
53 Italic,
54 Oblique,
55}
56
57impl ToCss for FontStyle {
58 fn to_css(&self, buf: &mut String) {
59 buf.push_str(match self {
60 FontStyle::Normal => "normal",
61 FontStyle::Italic => "italic",
62 FontStyle::Oblique => "oblique",
63 });
64 }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
69pub enum FontVariant {
70 #[default]
71 Normal,
72 SmallCaps,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
77pub enum TextTransform {
78 #[default]
79 None,
80 Uppercase,
81 Lowercase,
82 Capitalize,
83}
84
85impl ToCss for TextTransform {
86 fn to_css(&self, buf: &mut String) {
87 buf.push_str(match self {
88 TextTransform::None => "none",
89 TextTransform::Uppercase => "uppercase",
90 TextTransform::Lowercase => "lowercase",
91 TextTransform::Capitalize => "capitalize",
92 });
93 }
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
99pub enum Hyphens {
100 Auto,
101 #[default]
102 Manual,
103 None,
104}
105
106impl ToCss for Hyphens {
107 fn to_css(&self, buf: &mut String) {
108 buf.push_str(match self {
109 Hyphens::Auto => "auto",
110 Hyphens::Manual => "manual",
111 Hyphens::None => "none",
112 });
113 }
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
118pub enum DecorationStyle {
119 #[default]
120 None,
121 Solid,
122 Dotted,
123 Dashed,
124 Double,
125}
126
127impl ToCss for DecorationStyle {
128 fn to_css(&self, buf: &mut String) {
129 buf.push_str(match self {
130 DecorationStyle::None => "none",
131 DecorationStyle::Solid => "solid",
132 DecorationStyle::Dotted => "dotted",
133 DecorationStyle::Dashed => "dashed",
134 DecorationStyle::Double => "double",
135 });
136 }
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
141pub enum Float {
142 #[default]
143 None,
144 Left,
145 Right,
146}
147
148impl ToCss for Float {
149 fn to_css(&self, buf: &mut String) {
150 buf.push_str(match self {
151 Float::None => "none",
152 Float::Left => "left",
153 Float::Right => "right",
154 });
155 }
156}
157
158#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
160pub enum BreakValue {
161 #[default]
162 Auto,
163 Always,
164 Avoid,
165 Column,
167}
168
169impl ToCss for BreakValue {
170 fn to_css(&self, buf: &mut String) {
171 buf.push_str(match self {
172 BreakValue::Auto => "auto",
173 BreakValue::Always => "always",
174 BreakValue::Avoid => "avoid",
175 BreakValue::Column => "column",
176 });
177 }
178}
179
180#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
182pub enum BorderStyle {
183 #[default]
184 None,
185 Solid,
186 Dotted,
187 Dashed,
188 Double,
189 Groove,
190 Ridge,
191 Inset,
192 Outset,
193}
194
195impl ToCss for BorderStyle {
196 fn to_css(&self, buf: &mut String) {
197 buf.push_str(match self {
198 BorderStyle::None => "none",
199 BorderStyle::Solid => "solid",
200 BorderStyle::Dotted => "dotted",
201 BorderStyle::Dashed => "dashed",
202 BorderStyle::Double => "double",
203 BorderStyle::Groove => "groove",
204 BorderStyle::Ridge => "ridge",
205 BorderStyle::Inset => "inset",
206 BorderStyle::Outset => "outset",
207 });
208 }
209}
210
211#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
213pub enum ListStylePosition {
214 #[default]
215 Outside,
216 Inside,
217}
218
219impl ToCss for ListStylePosition {
220 fn to_css(&self, buf: &mut String) {
221 buf.push_str(match self {
222 ListStylePosition::Outside => "outside",
223 ListStylePosition::Inside => "inside",
224 });
225 }
226}
227
228#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
230pub enum Visibility {
231 #[default]
232 Visible,
233 Hidden,
234 Collapse,
235}
236
237impl ToCss for Visibility {
238 fn to_css(&self, buf: &mut String) {
239 buf.push_str(match self {
240 Visibility::Visible => "visible",
241 Visibility::Hidden => "hidden",
242 Visibility::Collapse => "collapse",
243 });
244 }
245}
246
247#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
249pub enum BoxSizing {
250 #[default]
252 ContentBox,
253 BorderBox,
255}
256
257impl ToCss for BoxSizing {
258 fn to_css(&self, buf: &mut String) {
259 buf.push_str(match self {
260 BoxSizing::ContentBox => "content-box",
261 BoxSizing::BorderBox => "border-box",
262 });
263 }
264}
265
266impl ToCss for FontVariant {
267 fn to_css(&self, buf: &mut String) {
268 buf.push_str(match self {
269 FontVariant::Normal => "normal",
270 FontVariant::SmallCaps => "small-caps",
271 });
272 }
273}
274
275#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
277pub enum TextAlign {
278 #[default]
279 Start,
280 End,
281 Left,
282 Right,
283 Center,
284 Justify,
285}
286
287impl ToCss for TextAlign {
288 fn to_css(&self, buf: &mut String) {
289 buf.push_str(match self {
290 TextAlign::Start => "start",
291 TextAlign::End => "end",
292 TextAlign::Left => "left",
293 TextAlign::Right => "right",
294 TextAlign::Center => "center",
295 TextAlign::Justify => "justify",
296 });
297 }
298}
299
300#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
302pub enum Display {
303 #[default]
304 Block,
305 Inline,
306 None,
307 ListItem,
308 TableCell,
309 TableRow,
310}
311
312#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
314pub enum ListStyleType {
315 #[default]
317 None,
318 Disc,
320 Circle,
322 Square,
324 Decimal,
326 LowerAlpha,
328 UpperAlpha,
330 LowerRoman,
332 UpperRoman,
334}
335
336impl ToCss for ListStyleType {
337 fn to_css(&self, buf: &mut String) {
338 buf.push_str(match self {
339 ListStyleType::None => "none",
340 ListStyleType::Disc => "disc",
341 ListStyleType::Circle => "circle",
342 ListStyleType::Square => "square",
343 ListStyleType::Decimal => "decimal",
344 ListStyleType::LowerAlpha => "lower-alpha",
345 ListStyleType::UpperAlpha => "upper-alpha",
346 ListStyleType::LowerRoman => "lower-roman",
347 ListStyleType::UpperRoman => "upper-roman",
348 });
349 }
350}
351
352impl ToCss for Display {
353 fn to_css(&self, buf: &mut String) {
354 buf.push_str(match self {
355 Display::Block => "block",
356 Display::Inline => "inline",
357 Display::None => "none",
358 Display::ListItem => "list-item",
359 Display::TableCell => "table-cell",
360 Display::TableRow => "table-row",
361 });
362 }
363}
364
365#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
367pub struct Color {
368 pub r: u8,
369 pub g: u8,
370 pub b: u8,
371 pub a: u8,
372}
373
374impl Color {
375 pub const BLACK: Color = Color {
376 r: 0,
377 g: 0,
378 b: 0,
379 a: 255,
380 };
381 pub const WHITE: Color = Color {
382 r: 255,
383 g: 255,
384 b: 255,
385 a: 255,
386 };
387 pub const TRANSPARENT: Color = Color {
388 r: 0,
389 g: 0,
390 b: 0,
391 a: 0,
392 };
393
394 pub fn rgb(r: u8, g: u8, b: u8) -> Self {
396 Self { r, g, b, a: 255 }
397 }
398
399 pub fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
401 Self { r, g, b, a }
402 }
403}
404
405impl ToCss for Color {
406 fn to_css(&self, buf: &mut String) {
407 if self.a == 255 {
408 write!(buf, "#{:02x}{:02x}{:02x}", self.r, self.g, self.b).unwrap();
410 } else if self.a == 0 {
411 buf.push_str("transparent");
412 } else {
413 let alpha = self.a as f32 / 255.0;
415 write!(buf, "rgba({},{},{},{:.2})", self.r, self.g, self.b, alpha).unwrap();
416 }
417 }
418}
419
420#[derive(Debug, Clone, Copy, PartialEq, Default)]
422pub enum Length {
423 #[default]
424 Auto,
425 Px(f32),
426 Em(f32),
427 Rem(f32),
428 Percent(f32),
429}
430
431impl Eq for Length {}
432
433impl Hash for Length {
434 fn hash<H: Hasher>(&self, state: &mut H) {
435 match self {
436 Length::Auto => 0u8.hash(state),
437 Length::Px(v) => {
438 1u8.hash(state);
439 v.to_bits().hash(state);
440 }
441 Length::Em(v) => {
442 2u8.hash(state);
443 v.to_bits().hash(state);
444 }
445 Length::Rem(v) => {
446 3u8.hash(state);
447 v.to_bits().hash(state);
448 }
449 Length::Percent(v) => {
450 4u8.hash(state);
451 v.to_bits().hash(state);
452 }
453 }
454 }
455}
456
457impl ToCss for Length {
458 fn to_css(&self, buf: &mut String) {
459 match self {
460 Length::Auto => buf.push_str("auto"),
461 Length::Px(v) => {
462 if *v == 0.0 {
463 buf.push('0');
464 } else {
465 write!(buf, "{}px", v).unwrap();
466 }
467 }
468 Length::Em(v) => write!(buf, "{}em", v).unwrap(),
469 Length::Rem(v) => write!(buf, "{}rem", v).unwrap(),
470 Length::Percent(v) => write!(buf, "{}%", v).unwrap(),
471 }
472 }
473}
474
475#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
477pub struct ComputedStyle {
478 pub font_family: Option<String>,
480 pub font_size: Length,
481 pub font_weight: FontWeight,
482 pub font_style: FontStyle,
483
484 pub color: Option<Color>,
486 pub background_color: Option<Color>,
487
488 pub text_align: TextAlign,
490 pub text_indent: Length,
491 pub line_height: Length,
492 pub text_decoration_underline: bool,
493 pub text_decoration_line_through: bool,
494
495 pub display: Display,
497 pub margin_top: Length,
498 pub margin_bottom: Length,
499 pub margin_left: Length,
500 pub margin_right: Length,
501 pub padding_top: Length,
502 pub padding_bottom: Length,
503 pub padding_left: Length,
504 pub padding_right: Length,
505
506 pub vertical_align_super: bool,
508 pub vertical_align_sub: bool,
509
510 pub list_style_type: ListStyleType,
512
513 pub font_variant: FontVariant,
515
516 pub letter_spacing: Length,
518 pub word_spacing: Length,
519
520 pub text_transform: TextTransform,
522
523 pub hyphens: Hyphens,
525
526 pub no_break: bool,
528
529 pub underline_style: DecorationStyle,
531 pub overline: bool,
532 pub underline_color: Option<Color>,
533
534 pub width: Length,
536 pub height: Length,
537 pub max_width: Length,
538 pub min_height: Length,
539 pub float: Float,
540
541 pub break_before: BreakValue,
543 pub break_after: BreakValue,
544 pub break_inside: BreakValue,
545
546 pub border_style_top: BorderStyle,
548 pub border_style_right: BorderStyle,
549 pub border_style_bottom: BorderStyle,
550 pub border_style_left: BorderStyle,
551 pub border_width_top: Length,
552 pub border_width_right: Length,
553 pub border_width_bottom: Length,
554 pub border_width_left: Length,
555 pub border_color_top: Option<Color>,
556 pub border_color_right: Option<Color>,
557 pub border_color_bottom: Option<Color>,
558 pub border_color_left: Option<Color>,
559 pub border_radius_top_left: Length,
561 pub border_radius_top_right: Length,
562 pub border_radius_bottom_left: Length,
563 pub border_radius_bottom_right: Length,
564
565 pub list_style_position: ListStylePosition,
567
568 pub language: Option<String>,
570 pub visibility: Visibility,
571 pub box_sizing: BoxSizing,
572}
573
574impl ComputedStyle {
575 pub fn is_default(&self) -> bool {
577 *self == ComputedStyle::default()
578 }
579
580 #[inline]
584 pub fn is_bold(&self) -> bool {
585 self.font_weight.0 >= 700
586 }
587
588 #[inline]
590 pub fn is_italic(&self) -> bool {
591 matches!(self.font_style, FontStyle::Italic | FontStyle::Oblique)
592 }
593
594 #[inline]
596 pub fn is_underline(&self) -> bool {
597 self.text_decoration_underline
598 }
599
600 #[inline]
602 pub fn is_strikethrough(&self) -> bool {
603 self.text_decoration_line_through
604 }
605
606 #[inline]
608 pub fn is_superscript(&self) -> bool {
609 self.vertical_align_super
610 }
611
612 #[inline]
614 pub fn is_subscript(&self) -> bool {
615 self.vertical_align_sub
616 }
617
618 pub fn is_monospace(&self) -> bool {
620 self.font_family
621 .as_ref()
622 .map(|f| {
623 let lower = f.to_lowercase();
624 lower.contains("mono")
625 || lower.contains("courier")
626 || lower.contains("consolas")
627 || lower.contains("menlo")
628 })
629 .unwrap_or(false)
630 }
631
632 pub fn is_ordered_list(&self) -> bool {
634 matches!(
635 self.list_style_type,
636 ListStyleType::Decimal
637 | ListStyleType::LowerAlpha
638 | ListStyleType::UpperAlpha
639 | ListStyleType::LowerRoman
640 | ListStyleType::UpperRoman
641 )
642 }
643
644 #[inline]
646 pub fn is_small_caps(&self) -> bool {
647 matches!(self.font_variant, FontVariant::SmallCaps)
648 }
649}
650
651impl ToCss for ComputedStyle {
652 fn to_css(&self, buf: &mut String) {
653 let default = ComputedStyle::default();
654
655 if let Some(ref family) = self.font_family {
657 write!(buf, "font-family: {}; ", family).unwrap();
658 }
659 if self.font_size != default.font_size {
660 buf.push_str("font-size: ");
661 self.font_size.to_css(buf);
662 buf.push_str("; ");
663 }
664 if self.font_weight != default.font_weight {
665 buf.push_str("font-weight: ");
666 self.font_weight.to_css(buf);
667 buf.push_str("; ");
668 }
669 if self.font_style != default.font_style {
670 buf.push_str("font-style: ");
671 self.font_style.to_css(buf);
672 buf.push_str("; ");
673 }
674
675 if let Some(color) = self.color {
677 buf.push_str("color: ");
678 color.to_css(buf);
679 buf.push_str("; ");
680 }
681 if let Some(bg) = self.background_color {
682 buf.push_str("background-color: ");
683 bg.to_css(buf);
684 buf.push_str("; ");
685 }
686
687 if self.text_align != default.text_align {
689 buf.push_str("text-align: ");
690 self.text_align.to_css(buf);
691 buf.push_str("; ");
692 }
693 if self.text_indent != default.text_indent {
694 buf.push_str("text-indent: ");
695 self.text_indent.to_css(buf);
696 buf.push_str("; ");
697 }
698 if self.line_height != default.line_height {
699 buf.push_str("line-height: ");
700 self.line_height.to_css(buf);
701 buf.push_str("; ");
702 }
703
704 let mut decorations = Vec::new();
706 if self.text_decoration_underline {
707 decorations.push("underline");
708 }
709 if self.text_decoration_line_through {
710 decorations.push("line-through");
711 }
712 if !decorations.is_empty() {
713 write!(buf, "text-decoration: {}; ", decorations.join(" ")).unwrap();
714 }
715
716 if self.display != default.display {
718 buf.push_str("display: ");
719 self.display.to_css(buf);
720 buf.push_str("; ");
721 }
722
723 if self.margin_top != default.margin_top {
725 buf.push_str("margin-top: ");
726 self.margin_top.to_css(buf);
727 buf.push_str("; ");
728 }
729 if self.margin_bottom != default.margin_bottom {
730 buf.push_str("margin-bottom: ");
731 self.margin_bottom.to_css(buf);
732 buf.push_str("; ");
733 }
734 if self.margin_left != default.margin_left {
735 buf.push_str("margin-left: ");
736 self.margin_left.to_css(buf);
737 buf.push_str("; ");
738 }
739 if self.margin_right != default.margin_right {
740 buf.push_str("margin-right: ");
741 self.margin_right.to_css(buf);
742 buf.push_str("; ");
743 }
744
745 if self.padding_top != default.padding_top {
747 buf.push_str("padding-top: ");
748 self.padding_top.to_css(buf);
749 buf.push_str("; ");
750 }
751 if self.padding_bottom != default.padding_bottom {
752 buf.push_str("padding-bottom: ");
753 self.padding_bottom.to_css(buf);
754 buf.push_str("; ");
755 }
756 if self.padding_left != default.padding_left {
757 buf.push_str("padding-left: ");
758 self.padding_left.to_css(buf);
759 buf.push_str("; ");
760 }
761 if self.padding_right != default.padding_right {
762 buf.push_str("padding-right: ");
763 self.padding_right.to_css(buf);
764 buf.push_str("; ");
765 }
766
767 if self.vertical_align_super {
769 buf.push_str("vertical-align: super; ");
770 } else if self.vertical_align_sub {
771 buf.push_str("vertical-align: sub; ");
772 }
773
774 if self.list_style_type != default.list_style_type {
776 buf.push_str("list-style-type: ");
777 self.list_style_type.to_css(buf);
778 buf.push_str("; ");
779 }
780
781 if self.font_variant != FontVariant::Normal {
783 buf.push_str("font-variant: ");
784 self.font_variant.to_css(buf);
785 buf.push_str("; ");
786 }
787
788 if self.letter_spacing != default.letter_spacing {
790 buf.push_str("letter-spacing: ");
791 self.letter_spacing.to_css(buf);
792 buf.push_str("; ");
793 }
794
795 if self.word_spacing != default.word_spacing {
797 buf.push_str("word-spacing: ");
798 self.word_spacing.to_css(buf);
799 buf.push_str("; ");
800 }
801
802 if self.text_transform != default.text_transform {
804 buf.push_str("text-transform: ");
805 self.text_transform.to_css(buf);
806 buf.push_str("; ");
807 }
808
809 if self.hyphens != default.hyphens {
811 buf.push_str("hyphens: ");
812 self.hyphens.to_css(buf);
813 buf.push_str("; ");
814 }
815
816 if self.no_break {
818 buf.push_str("white-space: nowrap; ");
819 }
820
821 if self.underline_style != default.underline_style {
823 buf.push_str("text-decoration-style: ");
824 self.underline_style.to_css(buf);
825 buf.push_str("; ");
826 }
827
828 if self.overline {
830 buf.push_str("text-decoration-line: overline; ");
831 }
832
833 if let Some(color) = self.underline_color {
835 buf.push_str("text-decoration-color: ");
836 color.to_css(buf);
837 buf.push_str("; ");
838 }
839
840 if self.width != default.width {
842 buf.push_str("width: ");
843 self.width.to_css(buf);
844 buf.push_str("; ");
845 }
846
847 if self.height != default.height {
849 buf.push_str("height: ");
850 self.height.to_css(buf);
851 buf.push_str("; ");
852 }
853
854 if self.max_width != default.max_width {
856 buf.push_str("max-width: ");
857 self.max_width.to_css(buf);
858 buf.push_str("; ");
859 }
860
861 if self.min_height != default.min_height {
863 buf.push_str("min-height: ");
864 self.min_height.to_css(buf);
865 buf.push_str("; ");
866 }
867
868 if self.float != default.float {
870 buf.push_str("float: ");
871 self.float.to_css(buf);
872 buf.push_str("; ");
873 }
874
875 if self.break_before != default.break_before {
877 buf.push_str("break-before: ");
878 self.break_before.to_css(buf);
879 buf.push_str("; ");
880 }
881
882 if self.break_after != default.break_after {
884 buf.push_str("break-after: ");
885 self.break_after.to_css(buf);
886 buf.push_str("; ");
887 }
888
889 if self.break_inside != default.break_inside {
891 buf.push_str("break-inside: ");
892 self.break_inside.to_css(buf);
893 buf.push_str("; ");
894 }
895
896 if self.border_style_top != default.border_style_top {
898 buf.push_str("border-top-style: ");
899 self.border_style_top.to_css(buf);
900 buf.push_str("; ");
901 }
902 if self.border_style_right != default.border_style_right {
903 buf.push_str("border-right-style: ");
904 self.border_style_right.to_css(buf);
905 buf.push_str("; ");
906 }
907 if self.border_style_bottom != default.border_style_bottom {
908 buf.push_str("border-bottom-style: ");
909 self.border_style_bottom.to_css(buf);
910 buf.push_str("; ");
911 }
912 if self.border_style_left != default.border_style_left {
913 buf.push_str("border-left-style: ");
914 self.border_style_left.to_css(buf);
915 buf.push_str("; ");
916 }
917
918 if self.border_width_top != default.border_width_top {
920 buf.push_str("border-top-width: ");
921 self.border_width_top.to_css(buf);
922 buf.push_str("; ");
923 }
924 if self.border_width_right != default.border_width_right {
925 buf.push_str("border-right-width: ");
926 self.border_width_right.to_css(buf);
927 buf.push_str("; ");
928 }
929 if self.border_width_bottom != default.border_width_bottom {
930 buf.push_str("border-bottom-width: ");
931 self.border_width_bottom.to_css(buf);
932 buf.push_str("; ");
933 }
934 if self.border_width_left != default.border_width_left {
935 buf.push_str("border-left-width: ");
936 self.border_width_left.to_css(buf);
937 buf.push_str("; ");
938 }
939
940 if let Some(color) = self.border_color_top {
942 buf.push_str("border-top-color: ");
943 color.to_css(buf);
944 buf.push_str("; ");
945 }
946 if let Some(color) = self.border_color_right {
947 buf.push_str("border-right-color: ");
948 color.to_css(buf);
949 buf.push_str("; ");
950 }
951 if let Some(color) = self.border_color_bottom {
952 buf.push_str("border-bottom-color: ");
953 color.to_css(buf);
954 buf.push_str("; ");
955 }
956 if let Some(color) = self.border_color_left {
957 buf.push_str("border-left-color: ");
958 color.to_css(buf);
959 buf.push_str("; ");
960 }
961
962 if self.border_radius_top_left != default.border_radius_top_left {
964 buf.push_str("border-top-left-radius: ");
965 self.border_radius_top_left.to_css(buf);
966 buf.push_str("; ");
967 }
968 if self.border_radius_top_right != default.border_radius_top_right {
969 buf.push_str("border-top-right-radius: ");
970 self.border_radius_top_right.to_css(buf);
971 buf.push_str("; ");
972 }
973 if self.border_radius_bottom_left != default.border_radius_bottom_left {
974 buf.push_str("border-bottom-left-radius: ");
975 self.border_radius_bottom_left.to_css(buf);
976 buf.push_str("; ");
977 }
978 if self.border_radius_bottom_right != default.border_radius_bottom_right {
979 buf.push_str("border-bottom-right-radius: ");
980 self.border_radius_bottom_right.to_css(buf);
981 buf.push_str("; ");
982 }
983
984 if self.list_style_position != default.list_style_position {
986 buf.push_str("list-style-position: ");
987 self.list_style_position.to_css(buf);
988 buf.push_str("; ");
989 }
990
991 if self.visibility != default.visibility {
993 buf.push_str("visibility: ");
994 self.visibility.to_css(buf);
995 buf.push_str("; ");
996 }
997
998 }
1001}
1002
1003#[derive(Clone)]
1008pub struct StylePool {
1009 styles: Vec<ComputedStyle>,
1011 intern_map: HashMap<ComputedStyle, StyleId>,
1013}
1014
1015impl Default for StylePool {
1016 fn default() -> Self {
1017 Self::new()
1018 }
1019}
1020
1021impl StylePool {
1022 pub fn new() -> Self {
1024 let default_style = ComputedStyle::default();
1025 let mut intern_map = HashMap::new();
1026 intern_map.insert(default_style.clone(), StyleId::DEFAULT);
1027
1028 Self {
1029 styles: vec![default_style],
1030 intern_map,
1031 }
1032 }
1033
1034 pub fn intern(&mut self, style: ComputedStyle) -> StyleId {
1039 if let Some(&id) = self.intern_map.get(&style) {
1040 return id;
1041 }
1042
1043 let id = StyleId(self.styles.len() as u32);
1044 self.intern_map.insert(style.clone(), id);
1045 self.styles.push(style);
1046 id
1047 }
1048
1049 pub fn get(&self, id: StyleId) -> Option<&ComputedStyle> {
1051 self.styles.get(id.0 as usize)
1052 }
1053
1054 pub fn len(&self) -> usize {
1056 self.styles.len()
1057 }
1058
1059 pub fn is_empty(&self) -> bool {
1061 self.styles.is_empty()
1062 }
1063
1064 pub fn iter(&self) -> impl Iterator<Item = (StyleId, &ComputedStyle)> {
1066 self.styles
1067 .iter()
1068 .enumerate()
1069 .map(|(i, s)| (StyleId(i as u32), s))
1070 }
1071}
1072
1073impl std::fmt::Debug for StylePool {
1074 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1075 f.debug_struct("StylePool")
1076 .field("count", &self.styles.len())
1077 .finish()
1078 }
1079}
1080
1081#[cfg(test)]
1082#[allow(clippy::field_reassign_with_default)]
1083mod tests {
1084 use super::*;
1085
1086 #[test]
1087 fn test_color_to_css_opaque() {
1088 assert_eq!(Color::BLACK.to_css_string(), "#000000");
1089 assert_eq!(Color::WHITE.to_css_string(), "#ffffff");
1090 assert_eq!(Color::rgb(255, 0, 0).to_css_string(), "#ff0000");
1091 assert_eq!(Color::rgb(0, 128, 255).to_css_string(), "#0080ff");
1092 }
1093
1094 #[test]
1095 fn test_color_to_css_transparent() {
1096 assert_eq!(Color::TRANSPARENT.to_css_string(), "transparent");
1097 }
1098
1099 #[test]
1100 fn test_color_to_css_alpha() {
1101 let color = Color::rgba(255, 0, 0, 128);
1102 let css = color.to_css_string();
1103 assert!(css.starts_with("rgba(255,0,0,"));
1104 assert!(css.contains("0.50")); }
1106
1107 #[test]
1108 fn test_length_to_css() {
1109 assert_eq!(Length::Auto.to_css_string(), "auto");
1110 assert_eq!(Length::Px(0.0).to_css_string(), "0");
1111 assert_eq!(Length::Px(16.0).to_css_string(), "16px");
1112 assert_eq!(Length::Em(1.5).to_css_string(), "1.5em");
1113 assert_eq!(Length::Rem(2.0).to_css_string(), "2rem");
1114 assert_eq!(Length::Percent(50.0).to_css_string(), "50%");
1115 }
1116
1117 #[test]
1118 fn test_font_weight_to_css() {
1119 assert_eq!(FontWeight::NORMAL.to_css_string(), "normal");
1120 assert_eq!(FontWeight::BOLD.to_css_string(), "bold");
1121 assert_eq!(FontWeight(300).to_css_string(), "300");
1122 assert_eq!(FontWeight(600).to_css_string(), "600");
1123 }
1124
1125 #[test]
1126 fn test_font_style_to_css() {
1127 assert_eq!(FontStyle::Normal.to_css_string(), "normal");
1128 assert_eq!(FontStyle::Italic.to_css_string(), "italic");
1129 assert_eq!(FontStyle::Oblique.to_css_string(), "oblique");
1130 }
1131
1132 #[test]
1133 fn test_text_align_to_css() {
1134 assert_eq!(TextAlign::Left.to_css_string(), "left");
1135 assert_eq!(TextAlign::Center.to_css_string(), "center");
1136 assert_eq!(TextAlign::Justify.to_css_string(), "justify");
1137 }
1138
1139 #[test]
1140 fn test_display_to_css() {
1141 assert_eq!(Display::Block.to_css_string(), "block");
1142 assert_eq!(Display::Inline.to_css_string(), "inline");
1143 assert_eq!(Display::None.to_css_string(), "none");
1144 }
1145
1146 #[test]
1147 fn test_computed_style_to_css_default() {
1148 let style = ComputedStyle::default();
1149 assert_eq!(style.to_css_string(), "");
1151 }
1152
1153 #[test]
1154 fn test_computed_style_to_css_bold() {
1155 let mut style = ComputedStyle::default();
1156 style.font_weight = FontWeight::BOLD;
1157 let css = style.to_css_string();
1158 assert!(css.contains("font-weight: bold;"));
1159 }
1160
1161 #[test]
1162 fn test_computed_style_to_css_multiple() {
1163 let mut style = ComputedStyle::default();
1164 style.font_weight = FontWeight::BOLD;
1165 style.font_style = FontStyle::Italic;
1166 style.color = Some(Color::rgb(255, 0, 0));
1167 style.text_align = TextAlign::Center;
1168
1169 let css = style.to_css_string();
1170 assert!(css.contains("font-weight: bold;"));
1171 assert!(css.contains("font-style: italic;"));
1172 assert!(css.contains("color: #ff0000;"));
1173 assert!(css.contains("text-align: center;"));
1174 }
1175
1176 #[test]
1177 fn test_computed_style_to_css_decorations() {
1178 let mut style = ComputedStyle::default();
1179 style.text_decoration_underline = true;
1180 style.text_decoration_line_through = true;
1181
1182 let css = style.to_css_string();
1183 assert!(css.contains("text-decoration: underline line-through;"));
1184 }
1185
1186 #[test]
1187 fn test_style_pool_interning() {
1188 let mut pool = StylePool::new();
1189
1190 let mut style1 = ComputedStyle::default();
1191 style1.font_weight = FontWeight::BOLD;
1192
1193 let id1 = pool.intern(style1.clone());
1194 let id2 = pool.intern(style1);
1195
1196 assert_eq!(id1, id2);
1198 assert_eq!(pool.len(), 2); }
1200
1201 #[test]
1202 fn test_style_pool_iter() {
1203 let mut pool = StylePool::new();
1204
1205 let mut style = ComputedStyle::default();
1206 style.font_weight = FontWeight::BOLD;
1207 pool.intern(style);
1208
1209 let ids: Vec<StyleId> = pool.iter().map(|(id, _)| id).collect();
1210 assert_eq!(ids, vec![StyleId(0), StyleId(1)]);
1211 }
1212}