1use ratatui::{
7 layout::Constraint,
8 widgets::{BorderType, Borders, Padding},
9};
10
11use crate::color::{split_top_comma, Color};
12use crate::error::{CssError, Result};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
20pub struct BoxEdges {
21 pub top: u16,
22 pub right: u16,
23 pub bottom: u16,
24 pub left: u16,
25}
26
27impl BoxEdges {
28 pub const fn uniform(v: u16) -> Self {
29 Self {
30 top: v,
31 right: v,
32 bottom: v,
33 left: v,
34 }
35 }
36
37 pub const fn zero() -> Self {
38 Self {
39 top: 0,
40 right: 0,
41 bottom: 0,
42 left: 0,
43 }
44 }
45
46 pub fn parse(shorthand: &str) -> Result<Self> {
48 let parts: Vec<&str> = shorthand.split_whitespace().collect();
49 let nums: Vec<u16> = parts
50 .iter()
51 .map(|p| {
52 p.trim_end_matches("px")
53 .parse::<u16>()
54 .map_err(|_| CssError::invalid_length(shorthand))
55 })
56 .collect::<Result<Vec<_>>>()?;
57 match nums.len() {
58 0 => Ok(Self::zero()),
59 1 => Ok(Self::uniform(nums[0])),
60 2 => Ok(Self {
61 top: nums[0],
62 bottom: nums[0],
63 left: nums[1],
64 right: nums[1],
65 }),
66 3 => Ok(Self {
67 top: nums[0],
68 left: nums[1],
69 right: nums[1],
70 bottom: nums[2],
71 }),
72 4 => Ok(Self {
74 top: nums[0],
75 right: nums[1],
76 bottom: nums[2],
77 left: nums[3],
78 }),
79 _ => Err(CssError::invalid_length(format!(
81 "box shorthand allows at most 4 values, got {}: {shorthand}",
82 nums.len()
83 ))),
84 }
85 }
86
87 pub fn to_padding(self) -> Padding {
89 Padding::new(self.left, self.right, self.top, self.bottom)
90 }
91
92 pub fn shrink(self, area: ratatui::layout::Rect) -> ratatui::layout::Rect {
94 let x = area.x.saturating_add(self.left);
95 let y = area.y.saturating_add(self.top);
96 let width = area
97 .width
98 .saturating_sub(self.left.saturating_add(self.right));
99 let height = area
100 .height
101 .saturating_sub(self.top.saturating_add(self.bottom));
102 ratatui::layout::Rect::new(x, y, width, height)
103 }
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
112pub enum BorderStyle {
113 #[default]
115 None,
116 Single,
118 Rounded,
120 Double,
122 Thick,
124}
125
126impl BorderStyle {
127 pub fn to_border_type(self) -> Option<BorderType> {
128 match self {
129 Self::None => None,
130 Self::Single => Some(BorderType::Plain),
131 Self::Rounded => Some(BorderType::Rounded),
132 Self::Double => Some(BorderType::Double),
133 Self::Thick => Some(BorderType::Thick),
134 }
135 }
136}
137
138#[derive(Debug, Clone, PartialEq)]
149pub struct BorderSpec {
150 pub style: BorderStyleValue,
151 pub color: Option<Color>,
152 pub edges: Option<Borders>,
153}
154
155impl Default for BorderSpec {
156 fn default() -> Self {
157 Self {
158 style: BorderStyleValue::Fixed(BorderStyle::None),
159 color: None,
160 edges: None,
161 }
162 }
163}
164
165impl BorderSpec {
166 pub fn edges_to_keyword(edges: Borders) -> &'static str {
170 const EDGES_KW: [&str; 16] = [
176 "none", "top", "right", "top|right", "bottom", "top|bottom", "right|bottom", "top|right|bottom", "left", "top|left", "right|left", "top|right|left", "bottom|left", "top|bottom|left", "right|bottom|left", "all", ];
193 let bits = edges.bits() as usize;
194 if bits >= EDGES_KW.len() {
195 return "none";
198 }
199 EDGES_KW[bits]
200 }
201
202 pub fn parse_edges(s: &str) -> Option<Borders> {
207 let lower = s.trim().to_ascii_lowercase();
208 if lower.is_empty() {
209 return None;
210 }
211 let mut acc = Borders::NONE;
212 for part in lower.split('|') {
213 let part = part.trim();
214 acc |= match part {
215 "all" => Borders::ALL,
216 "none" => Borders::NONE,
217 "top" => Borders::TOP,
218 "right" => Borders::RIGHT,
219 "bottom" => Borders::BOTTOM,
220 "left" => Borders::LEFT,
221 "x" => Borders::LEFT | Borders::RIGHT,
222 "y" => Borders::TOP | Borders::BOTTOM,
223 _ => return None,
224 };
225 }
226 Some(acc)
227 }
228
229 pub fn borders(&self) -> Borders {
241 if matches!(self.style.as_fixed(), Some(BorderStyle::None)) {
242 Borders::NONE
243 } else {
244 self.edges.unwrap_or(Borders::ALL)
245 }
246 }
247
248 pub fn border_type(&self) -> BorderType {
249 self.style
250 .as_fixed()
251 .and_then(|s| s.to_border_type())
252 .unwrap_or(BorderType::Plain)
253 }
254
255 pub fn parse_shorthand(s: &str) -> Result<Self> {
266 let mut style: BorderStyleValue = BorderStyleValue::Fixed(BorderStyle::None);
267 let mut color_tokens: Vec<&str> = Vec::new();
268 let lowered = s.to_ascii_lowercase();
269 let bytes = s.as_bytes();
270 let mut i = 0;
271 let mut consumed: Vec<(usize, usize)> = Vec::new();
274 while i < bytes.len() {
275 while i < bytes.len() && bytes[i].is_ascii_whitespace() {
277 i += 1;
278 }
279 if i >= bytes.len() {
280 break;
281 }
282 let start = i;
283 while i < bytes.len() && !bytes[i].is_ascii_whitespace() {
285 i += 1;
286 }
287 let tok = &s[start..i];
288 if lowered[start..i].starts_with("var(") {
290 let mut joined = String::from(tok);
293 while !joined.ends_with(')') && i < bytes.len() {
295 let ws_start = i;
297 while i < bytes.len() && bytes[i].is_ascii_whitespace() {
298 i += 1;
299 }
300 if i >= bytes.len() {
301 break;
302 }
303 let t2_start = i;
304 while i < bytes.len() && !bytes[i].is_ascii_whitespace() {
305 i += 1;
306 }
307 joined.push_str(&s[ws_start..i]);
309 let _ = t2_start; }
311 let style_already_set =
318 !matches!(style, BorderStyleValue::Fixed(BorderStyle::None));
319 if style_already_set {
320 color_tokens.push(&s[start..i]);
326 } else {
327 consumed.push((start, i));
328 style = BorderStyleValue::parse(&joined)?;
329 }
330 } else if tok.ends_with("px") {
331 consumed.push((start, start + tok.len()));
333 } else if let Some(parsed) = BorderStyle::parse_keyword(tok) {
334 consumed.push((start, start + tok.len()));
335 style = BorderStyleValue::Fixed(parsed);
336 } else {
337 color_tokens.push(tok);
339 }
340 }
341 let color = if color_tokens.is_empty() {
342 None
343 } else {
344 Some(Color::parse(&color_tokens.join(" "))?)
345 };
346 Ok(Self {
350 style,
351 color,
352 edges: Some(Borders::ALL),
353 })
354 }
355
356 pub fn merge(&mut self, other: &BorderSpec) {
375 let other_declares_style =
376 !matches!(other.style, BorderStyleValue::Fixed(BorderStyle::None));
377 if other_declares_style {
378 self.style = other.style.clone();
379 }
380 if other.color.is_some() {
381 self.color = other.color.clone();
382 }
383 if let Some(oe) = other.edges {
387 self.edges = Some(self.edges.unwrap_or(Borders::NONE) | oe);
388 }
389 }
390}
391
392impl BorderStyle {
393 pub fn parse_keyword(s: &str) -> Option<Self> {
395 Some(match s.to_ascii_lowercase().as_str() {
396 "none" | "hidden" => Self::None,
397 "single" | "solid" | "plain" => Self::Single,
398 "rounded" => Self::Rounded,
399 "double" => Self::Double,
400 "thick" => Self::Thick,
401 _ => return None,
402 })
403 }
404
405 pub fn as_keyword(self) -> &'static str {
406 match self {
407 Self::None => "none",
408 Self::Single => "single",
409 Self::Rounded => "rounded",
410 Self::Double => "double",
411 Self::Thick => "thick",
412 }
413 }
414}
415
416#[derive(Debug, Clone, PartialEq)]
428pub enum BoxEdgesValue {
429 Edges(BoxEdges),
431 Var {
433 name: String,
434 fallback: Option<Box<BoxEdgesValue>>,
435 },
436}
437
438impl BoxEdgesValue {
439 pub fn parse(s: &str) -> Result<Self> {
447 let s = s.trim();
448 let lower = s.to_ascii_lowercase();
449 if let Some(rest) = lower.strip_prefix("var(") {
450 let inner = rest.strip_suffix(')').unwrap_or(rest);
452 let (name_part, fallback_part) = split_top_comma(inner);
453 let name = name_part.trim().trim_start_matches('-').trim().to_string();
454 if name.is_empty() {
455 return Err(CssError::invalid_length(format!(
456 "var(): empty name in {s}"
457 )));
458 }
459 let fallback = match fallback_part.trim() {
460 "" => None,
461 expr => Some(Box::new(Self::parse(expr)?)),
462 };
463 return Ok(Self::Var { name, fallback });
464 }
465 BoxEdges::parse(s).map(Self::Edges)
466 }
467
468 pub fn is_var(&self) -> bool {
470 matches!(self, Self::Var { .. })
471 }
472
473 pub fn var(name: impl Into<String>) -> Self {
475 Self::Var {
476 name: name.into(),
477 fallback: None,
478 }
479 }
480}
481
482impl From<BoxEdges> for BoxEdgesValue {
483 fn from(e: BoxEdges) -> Self {
484 Self::Edges(e)
485 }
486}
487
488impl std::fmt::Display for BoxEdgesValue {
489 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
490 match self {
491 Self::Edges(e) => {
492 let e = *e;
493 if e.top == e.right && e.right == e.bottom && e.bottom == e.left {
494 write!(f, "{}", e.top)
495 } else {
496 write!(f, "{} {} {} {}", e.top, e.right, e.bottom, e.left)
497 }
498 }
499 Self::Var { name, fallback } => match fallback {
500 Some(fb) => write!(f, "var(--{name}, {fb})"),
501 None => write!(f, "var(--{name})"),
502 },
503 }
504 }
505}
506
507#[derive(Debug, Clone, PartialEq)]
513pub enum BorderStyleValue {
514 Fixed(BorderStyle),
516 Var {
518 name: String,
519 fallback: Option<Box<BorderStyleValue>>,
520 },
521}
522
523impl BorderStyleValue {
524 pub fn parse(s: &str) -> Result<Self> {
533 let s = s.trim();
534 let lower = s.to_ascii_lowercase();
535 if let Some(rest) = lower.strip_prefix("var(") {
536 let inner = rest.strip_suffix(')').unwrap_or(rest);
537 let (name_part, fallback_part) = split_top_comma(inner);
538 let name = name_part.trim().trim_start_matches('-').trim().to_string();
539 if name.is_empty() {
540 return Err(CssError::invalid_length(format!(
541 "var(): empty name in {s}"
542 )));
543 }
544 let fallback = match fallback_part.trim() {
545 "" => None,
546 expr => Some(Box::new(Self::parse(expr)?)),
547 };
548 return Ok(Self::Var { name, fallback });
549 }
550 match BorderStyle::parse_keyword(s) {
551 Some(b) => Ok(Self::Fixed(b)),
552 None => Err(CssError::invalid_length(format!("border-style: {s}"))),
553 }
554 }
555
556 pub fn is_var(&self) -> bool {
558 matches!(self, Self::Var { .. })
559 }
560
561 pub fn as_fixed(&self) -> Option<BorderStyle> {
565 match self {
566 Self::Fixed(b) => Some(*b),
567 Self::Var { .. } => None,
568 }
569 }
570
571 pub fn var(name: impl Into<String>) -> Self {
573 Self::Var {
574 name: name.into(),
575 fallback: None,
576 }
577 }
578}
579
580impl From<BorderStyle> for BorderStyleValue {
581 fn from(b: BorderStyle) -> Self {
582 Self::Fixed(b)
583 }
584}
585
586impl Default for BorderStyleValue {
587 fn default() -> Self {
588 Self::Fixed(BorderStyle::None)
589 }
590}
591
592impl std::fmt::Display for BorderStyleValue {
593 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
594 match self {
595 Self::Fixed(b) => f.write_str(b.as_keyword()),
596 Self::Var { name, fallback } => match fallback {
597 Some(fb) => write!(f, "var(--{name}, {fb})"),
598 None => write!(f, "var(--{name})"),
599 },
600 }
601 }
602}
603
604#[derive(Debug, Clone, PartialEq)]
614pub enum Length {
615 Auto,
617 Cells(u16),
619 Percent(u16),
621 Min(u16),
623 Max(u16),
625 Var {
633 name: String,
634 fallback: Option<Box<Length>>,
635 },
636}
637
638impl Length {
639 pub fn parse(s: &str) -> Result<Self> {
640 let s = s.trim();
641 if let Some(inner) = s
646 .strip_prefix("var(")
647 .or_else(|| s.strip_prefix("VAR("))
648 .or_else(|| s.strip_prefix("Var("))
649 {
650 let inner = inner.strip_suffix(')').unwrap_or(inner);
651 let (name_part, fallback_part) = split_top_comma(inner);
652 let name = name_part.trim().trim_start_matches('-').trim().to_string();
653 if name.is_empty() {
654 return Err(CssError::invalid_length(format!(
655 "var(): empty name in {s}"
656 )));
657 }
658 let fallback = match fallback_part.trim() {
659 "" => None,
660 expr => Some(Box::new(Self::parse(expr)?)),
661 };
662 return Ok(Self::Var { name, fallback });
663 }
664 if s.eq_ignore_ascii_case("auto") || s.is_empty() {
665 return Ok(Self::Auto);
666 }
667 if let Some(rest) = s.strip_prefix("min(").and_then(|r| r.strip_suffix(')')) {
668 return Ok(Self::Min(parse_cells(rest)?));
669 }
670 if let Some(rest) = s.strip_prefix("max(").and_then(|r| r.strip_suffix(')')) {
671 return Ok(Self::Max(parse_cells(rest)?));
672 }
673 if let Some(rest) = s.strip_suffix('%') {
674 return Ok(Self::Percent(
675 rest.parse().map_err(|_| CssError::invalid_length(s))?,
676 ));
677 }
678 Ok(Self::Cells(parse_cells(s)?))
679 }
680
681 pub fn to_constraint(&self) -> Constraint {
682 match self {
683 Self::Auto => Constraint::Min(0),
684 Self::Cells(n) => Constraint::Length(*n),
685 Self::Percent(p) => Constraint::Percentage(*p),
686 Self::Min(n) => Constraint::Min(*n),
687 Self::Max(n) => Constraint::Max(*n),
688 Self::Var {
691 fallback: Some(fb), ..
692 } => fb.to_constraint(),
693 Self::Var { fallback: None, .. } => Constraint::Min(0),
694 }
695 }
696}
697
698fn parse_cells(s: &str) -> Result<u16> {
699 s.trim_end_matches("px")
700 .trim()
701 .parse::<u16>()
702 .map_err(|_| CssError::invalid_length(s))
703}
704
705#[cfg(feature = "serde")]
707fn length_to_css(length: &Length) -> String {
708 match length {
709 Length::Auto => "auto".to_string(),
710 Length::Cells(n) => format!("{n}px"),
711 Length::Percent(p) => format!("{p}%"),
712 Length::Min(n) => format!("min({n})"),
713 Length::Max(n) => format!("max({n})"),
714 Length::Var {
715 name,
716 fallback: None,
717 } => format!("var(--{name})"),
718 Length::Var {
719 name,
720 fallback: Some(fb),
721 } => {
722 format!("var(--{name}, {})", length_to_css(fb))
723 }
724 }
725}
726
727pub trait IntoBoxEdges {
746 fn into_edges(self) -> BoxEdgesValue;
747}
748
749impl IntoBoxEdges for u16 {
750 fn into_edges(self) -> BoxEdgesValue {
751 BoxEdgesValue::Edges(BoxEdges::uniform(self))
752 }
753}
754
755impl IntoBoxEdges for (u16, u16) {
756 fn into_edges(self) -> BoxEdgesValue {
757 let (a, b) = self;
758 BoxEdgesValue::Edges(BoxEdges {
759 top: a,
760 bottom: a,
761 left: b,
762 right: b,
763 })
764 }
765}
766
767impl IntoBoxEdges for (u16, u16, u16, u16) {
768 fn into_edges(self) -> BoxEdgesValue {
769 let (top, right, bottom, left) = self;
770 BoxEdgesValue::Edges(BoxEdges {
771 top,
772 right,
773 bottom,
774 left,
775 })
776 }
777}
778
779impl IntoBoxEdges for &str {
780 fn into_edges(self) -> BoxEdgesValue {
781 BoxEdgesValue::parse(self).expect(
782 "invalid padding/margin shorthand — pass a u16 or tuple for infallible construction",
783 )
784 }
785}
786
787impl IntoBoxEdges for BoxEdges {
788 fn into_edges(self) -> BoxEdgesValue {
789 BoxEdgesValue::Edges(self)
790 }
791}
792
793impl IntoBoxEdges for BoxEdgesValue {
794 fn into_edges(self) -> BoxEdgesValue {
795 self
796 }
797}
798
799pub trait IntoBorderSpec {
810 fn into_spec(self) -> BorderSpec;
811}
812
813impl IntoBorderSpec for BorderStyle {
814 fn into_spec(self) -> BorderSpec {
815 BorderSpec {
817 style: BorderStyleValue::Fixed(self),
818 color: None,
819 edges: None,
820 }
821 }
822}
823
824impl<C: Into<Color>> IntoBorderSpec for (BorderStyle, C) {
825 fn into_spec(self) -> BorderSpec {
826 let (style, color) = self;
827 BorderSpec {
828 style: BorderStyleValue::Fixed(style),
829 color: Some(color.into()),
830 edges: None,
831 }
832 }
833}
834
835impl IntoBorderSpec for &str {
836 fn into_spec(self) -> BorderSpec {
837 BorderSpec::parse_shorthand(self)
838 .expect("invalid border shorthand — pass a BorderStyle / (BorderStyle, color) for infallible construction")
839 }
840}
841
842impl IntoBorderSpec for BorderSpec {
843 fn into_spec(self) -> BorderSpec {
844 self
845 }
846}
847
848#[cfg(test)]
849mod tests {
850 use super::*;
851
852 #[test]
853 fn border_spec_merge_keeps_declared_subfields() {
854 use ratatui::style::Color as RC;
855 let mut a = BorderSpec {
858 style: BorderStyleValue::Fixed(BorderStyle::Rounded),
859 color: None,
860 edges: None,
861 };
862 let b = BorderSpec {
863 style: BorderStyleValue::Fixed(BorderStyle::None),
864 color: Some(Color::literal(RC::Blue)),
865 edges: None,
866 };
867 a.merge(&b);
868 assert_eq!(a.style, BorderStyleValue::Fixed(BorderStyle::Rounded)); assert_eq!(a.color, Some(Color::literal(RC::Blue))); let mut c = BorderSpec {
874 style: BorderStyleValue::Fixed(BorderStyle::Double),
875 color: None,
876 edges: None,
877 };
878 c.merge(&BorderSpec::default());
879 assert_eq!(c.style, BorderStyleValue::Fixed(BorderStyle::Double));
880 }
881
882 #[test]
883 fn edges_shorthand() {
884 assert_eq!(BoxEdges::parse("1").unwrap(), BoxEdges::uniform(1));
885 let e = BoxEdges::parse("1 2").unwrap();
886 assert_eq!((e.top, e.right, e.bottom, e.left), (1, 2, 1, 2));
887 let e = BoxEdges::parse("1 2 3 4").unwrap();
888 assert_eq!((e.top, e.right, e.bottom, e.left), (1, 2, 3, 4));
889 }
890
891 #[test]
892 fn edges_shorthand_rejects_more_than_four() {
893 assert!(BoxEdges::parse("1 2 3 4 5").is_err());
895 assert!(BoxEdges::parse("1 2 3 4 5 6").is_err());
896 assert!(BoxEdges::parse("1 2 3 4").is_ok());
898 }
899
900 #[test]
901 fn edges_shrink() {
902 let area = ratatui::layout::Rect::new(0, 0, 10, 10);
903 let inner = BoxEdges::uniform(1).shrink(area);
904 assert_eq!((inner.x, inner.y, inner.width, inner.height), (1, 1, 8, 8));
905 }
906
907 #[test]
908 fn length_parse() {
909 assert_eq!(Length::parse("auto").unwrap(), Length::Auto);
910 assert_eq!(Length::parse("10px").unwrap(), Length::Cells(10));
911 assert_eq!(Length::parse("50%").unwrap(), Length::Percent(50));
912 assert_eq!(Length::parse("min(3)").unwrap(), Length::Min(3));
913 }
914
915 #[test]
916 fn length_var_parse() {
917 assert_eq!(
918 Length::parse("var(--w)").unwrap(),
919 Length::Var {
920 name: "w".into(),
921 fallback: None
922 }
923 );
924 assert_eq!(Length::parse("10").unwrap(), Length::Cells(10));
926 assert_eq!(Length::parse("50%").unwrap(), Length::Percent(50));
927 assert_eq!(
929 Length::parse("var(--w, 10)").unwrap(),
930 Length::Var {
931 name: "w".into(),
932 fallback: Some(Box::new(Length::Cells(10)))
933 }
934 );
935 assert_eq!(
937 Length::parse("var(--w, 50%)").unwrap(),
938 Length::Var {
939 name: "w".into(),
940 fallback: Some(Box::new(Length::Percent(50)))
941 }
942 );
943 assert!(Length::parse("var(--)").is_err());
945 }
946
947 #[test]
948 fn length_var_degrades_to_min_zero() {
949 assert_eq!(
951 Length::Var {
952 name: "x".into(),
953 fallback: None
954 }
955 .to_constraint(),
956 Constraint::Min(0)
957 );
958 assert_eq!(
960 Length::Var {
961 name: "x".into(),
962 fallback: Some(Box::new(Length::Cells(7)))
963 }
964 .to_constraint(),
965 Constraint::Length(7)
966 );
967 }
968
969 #[test]
970 fn into_box_edges_uniform() {
971 let e: BoxEdgesValue = 1u16.into_edges();
972 assert_eq!(e, BoxEdgesValue::Edges(BoxEdges::uniform(1)));
973 }
974
975 #[test]
976 fn into_box_edges_pair() {
977 let e: BoxEdgesValue = (0u16, 2u16).into_edges();
978 match e {
979 BoxEdgesValue::Edges(e) => {
980 assert_eq!((e.top, e.right, e.bottom, e.left), (0, 2, 0, 2));
981 }
982 other => panic!("expected Edges, got {other:?}"),
983 }
984 }
985
986 #[test]
987 fn into_box_edges_quad() {
988 let e: BoxEdgesValue = (1u16, 2u16, 3u16, 4u16).into_edges();
989 match e {
990 BoxEdgesValue::Edges(e) => {
991 assert_eq!((e.top, e.right, e.bottom, e.left), (1, 2, 3, 4));
992 }
993 other => panic!("expected Edges, got {other:?}"),
994 }
995 }
996
997 #[test]
998 fn into_box_edges_string_matches_pair() {
999 let typed = (0u16, 2u16).into_edges();
1000 let from_str: BoxEdgesValue = "0 2".into_edges();
1001 assert_eq!(typed, from_str);
1002 }
1003
1004 #[test]
1005 fn into_border_spec_style_only() {
1006 let spec = BorderStyle::Rounded.into_spec();
1007 assert_eq!(spec.style, BorderStyleValue::Fixed(BorderStyle::Rounded));
1008 assert_eq!(spec.color, None);
1009 }
1010
1011 #[test]
1012 fn into_border_spec_with_color() {
1013 use ratatui::style::Color as RC;
1014 let spec = (BorderStyle::Double, "#ff0000").into_spec();
1015 assert_eq!(spec.style, BorderStyleValue::Fixed(BorderStyle::Double));
1016 assert_eq!(spec.color, Some(Color::literal(RC::Rgb(255, 0, 0))));
1017 }
1018
1019 #[test]
1020 fn into_border_spec_string_matches() {
1021 let typed = BorderStyle::Single.into_spec();
1022 let from_str: BorderSpec = "single".into_spec();
1023 assert_eq!(typed.style, from_str.style);
1024 assert_eq!(typed.color, from_str.color);
1025 }
1026
1027 #[test]
1032 fn border_full_shorthand_all_edges() {
1033 let spec = BorderSpec::parse_shorthand("rounded").unwrap();
1035 assert_eq!(spec.style, BorderStyleValue::Fixed(BorderStyle::Rounded));
1036 assert_eq!(spec.edges, Some(Borders::ALL));
1037 assert_eq!(spec.borders(), Borders::ALL);
1038 }
1039
1040 #[test]
1041 fn border_style_only_legacy_all() {
1042 let spec = BorderSpec {
1045 style: BorderStyleValue::Fixed(BorderStyle::Rounded),
1046 color: None,
1047 edges: None,
1048 };
1049 assert_eq!(spec.borders(), Borders::ALL);
1050 }
1051
1052 #[test]
1053 fn border_none_style_draws_nothing_even_with_edges() {
1054 let spec = BorderSpec {
1056 style: BorderStyleValue::Fixed(BorderStyle::None),
1057 color: None,
1058 edges: Some(Borders::BOTTOM),
1059 };
1060 assert_eq!(spec.borders(), Borders::NONE);
1061 }
1062
1063 #[test]
1064 fn per_edge_merge_accumulates() {
1065 let mut a = BorderSpec {
1068 style: BorderStyleValue::Fixed(BorderStyle::Rounded),
1069 color: None,
1070 edges: Some(Borders::TOP),
1071 };
1072 let b = BorderSpec {
1073 style: BorderStyleValue::Fixed(BorderStyle::None),
1074 color: None,
1075 edges: Some(Borders::BOTTOM),
1076 };
1077 a.merge(&b);
1078 assert_eq!(a.style, BorderStyleValue::Fixed(BorderStyle::Rounded)); assert_eq!(a.edges, Some(Borders::TOP | Borders::BOTTOM));
1080 assert_eq!(a.borders(), Borders::TOP | Borders::BOTTOM);
1081 }
1082
1083 #[test]
1084 fn per_edge_merge_legacy_none_edges_not_touched() {
1085 let mut a = BorderSpec {
1088 style: BorderStyleValue::Fixed(BorderStyle::Rounded),
1089 color: None,
1090 edges: Some(Borders::TOP),
1091 };
1092 let legacy = BorderSpec {
1093 style: BorderStyleValue::Fixed(BorderStyle::None),
1094 color: None,
1095 edges: None,
1096 };
1097 a.merge(&legacy);
1098 assert_eq!(a.edges, Some(Borders::TOP)); }
1100
1101 #[test]
1102 fn per_edge_full_shorthand_then_edge_widens() {
1103 let mut a = BorderSpec {
1107 style: BorderStyleValue::Fixed(BorderStyle::Rounded),
1108 color: None,
1109 edges: Some(Borders::ALL),
1110 };
1111 let b = BorderSpec {
1112 style: BorderStyleValue::Fixed(BorderStyle::None),
1113 color: None,
1114 edges: Some(Borders::BOTTOM),
1115 };
1116 a.merge(&b);
1117 assert_eq!(a.edges, Some(Borders::ALL));
1118 }
1119
1120 #[test]
1121 fn edges_keyword_roundtrip() {
1122 for (keyword, edges) in [
1126 ("all", Borders::ALL),
1127 ("none", Borders::NONE),
1128 ("top", Borders::TOP),
1129 ("bottom", Borders::BOTTOM),
1130 ("top|bottom", Borders::TOP | Borders::BOTTOM),
1131 ("right|left", Borders::LEFT | Borders::RIGHT),
1132 ] {
1133 assert_eq!(
1134 BorderSpec::parse_edges(keyword),
1135 Some(edges),
1136 "parse {keyword}"
1137 );
1138 assert_eq!(
1139 BorderSpec::edges_to_keyword(edges),
1140 keyword,
1141 "emit {keyword}"
1142 );
1143 }
1144 assert_eq!(
1146 BorderSpec::parse_edges("left|right"),
1147 Some(Borders::LEFT | Borders::RIGHT)
1148 );
1149 assert_eq!(
1151 BorderSpec::parse_edges("x"),
1152 Some(Borders::LEFT | Borders::RIGHT)
1153 );
1154 assert_eq!(
1155 BorderSpec::parse_edges("y"),
1156 Some(Borders::TOP | Borders::BOTTOM)
1157 );
1158 }
1159
1160 #[test]
1161 fn edges_to_keyword_is_leak_free_and_covers_all_16() {
1162 let combos: [(Borders, &str); 16] = [
1167 (Borders::NONE, "none"),
1168 (Borders::TOP, "top"),
1169 (Borders::RIGHT, "right"),
1170 (Borders::TOP | Borders::RIGHT, "top|right"),
1171 (Borders::BOTTOM, "bottom"),
1172 (Borders::TOP | Borders::BOTTOM, "top|bottom"),
1173 (Borders::RIGHT | Borders::BOTTOM, "right|bottom"),
1174 (
1175 Borders::TOP | Borders::RIGHT | Borders::BOTTOM,
1176 "top|right|bottom",
1177 ),
1178 (Borders::LEFT, "left"),
1179 (Borders::TOP | Borders::LEFT, "top|left"),
1180 (Borders::RIGHT | Borders::LEFT, "right|left"),
1181 (
1182 Borders::TOP | Borders::RIGHT | Borders::LEFT,
1183 "top|right|left",
1184 ),
1185 (Borders::BOTTOM | Borders::LEFT, "bottom|left"),
1186 (
1187 Borders::TOP | Borders::BOTTOM | Borders::LEFT,
1188 "top|bottom|left",
1189 ),
1190 (
1191 Borders::RIGHT | Borders::BOTTOM | Borders::LEFT,
1192 "right|bottom|left",
1193 ),
1194 (Borders::ALL, "all"),
1195 ];
1196 for (edges, expected) in combos {
1197 let kw = BorderSpec::edges_to_keyword(edges);
1198 assert_eq!(kw, expected, "bits {:#06b}", edges.bits());
1199 assert_eq!(BorderSpec::parse_edges(kw), Some(edges), "roundtrip {kw}");
1201 }
1202 }
1203
1204 #[test]
1209 fn box_edges_value_parse_var_no_fallback() {
1210 assert_eq!(
1211 BoxEdgesValue::parse("var(--pad)").unwrap(),
1212 BoxEdgesValue::Var {
1213 name: "pad".into(),
1214 fallback: None,
1215 }
1216 );
1217 assert_eq!(
1219 BoxEdgesValue::parse("VAR(--pad)").unwrap(),
1220 BoxEdgesValue::Var {
1221 name: "pad".into(),
1222 fallback: None,
1223 }
1224 );
1225 }
1226
1227 #[test]
1228 fn box_edges_value_parse_concrete() {
1229 let e = BoxEdgesValue::parse("1 2").unwrap();
1230 match e {
1231 BoxEdgesValue::Edges(e) => {
1232 assert_eq!((e.top, e.right, e.bottom, e.left), (1, 2, 1, 2));
1233 }
1234 other => panic!("expected Edges, got {other:?}"),
1235 }
1236 }
1237
1238 #[test]
1239 fn box_edges_value_parse_var_with_fallback() {
1240 let v = BoxEdgesValue::parse("var(--pad, 1)").unwrap();
1241 match v {
1242 BoxEdgesValue::Var {
1243 name,
1244 fallback: Some(fb),
1245 } => {
1246 assert_eq!(name, "pad");
1247 assert_eq!(*fb, BoxEdgesValue::Edges(BoxEdges::uniform(1)));
1248 }
1249 other => panic!("expected Var with fallback, got {other:?}"),
1250 }
1251 let v = BoxEdgesValue::parse("var(--pad, 1 2 3 4)").unwrap();
1253 match v {
1254 BoxEdgesValue::Var { fallback: Some(fb), .. } => match *fb {
1255 BoxEdgesValue::Edges(e) => {
1256 assert_eq!((e.top, e.right, e.bottom, e.left), (1, 2, 3, 4));
1257 }
1258 other => panic!("expected Edges fallback, got {other:?}"),
1259 },
1260 other => panic!("expected Var, got {other:?}"),
1261 }
1262 }
1263
1264 #[test]
1265 fn box_edges_value_empty_name_errors() {
1266 assert!(BoxEdgesValue::parse("var(--)").is_err());
1267 }
1268
1269 #[test]
1270 fn box_edges_value_display_roundtrip() {
1271 let v = BoxEdgesValue::Edges(BoxEdges::uniform(3));
1273 assert_eq!(v.to_string(), "3");
1274 let v = BoxEdgesValue::Edges(BoxEdges {
1276 top: 1,
1277 right: 2,
1278 bottom: 3,
1279 left: 4,
1280 });
1281 assert_eq!(v.to_string(), "1 2 3 4");
1282 let v = BoxEdgesValue::var("pad");
1284 assert_eq!(v.to_string(), "var(--pad)");
1285 let v = BoxEdgesValue::Var {
1287 name: "pad".into(),
1288 fallback: Some(Box::new(BoxEdgesValue::Edges(BoxEdges::uniform(1)))),
1289 };
1290 assert_eq!(v.to_string(), "var(--pad, 1)");
1291 }
1292
1293 #[test]
1294 fn border_style_value_parse_var() {
1295 assert_eq!(
1296 BorderStyleValue::parse("var(--bs)").unwrap(),
1297 BorderStyleValue::Var {
1298 name: "bs".into(),
1299 fallback: None,
1300 }
1301 );
1302 }
1303
1304 #[test]
1305 fn border_style_value_parse_keyword() {
1306 assert_eq!(
1307 BorderStyleValue::parse("rounded").unwrap(),
1308 BorderStyleValue::Fixed(BorderStyle::Rounded)
1309 );
1310 assert_eq!(
1311 BorderStyleValue::parse("none").unwrap(),
1312 BorderStyleValue::Fixed(BorderStyle::None)
1313 );
1314 }
1315
1316 #[test]
1317 fn border_style_value_garbage_errors() {
1318 assert!(BorderStyleValue::parse("banana").is_err());
1319 }
1320
1321 #[test]
1322 fn border_style_value_parse_var_with_fallback() {
1323 let v = BorderStyleValue::parse("var(--bs, rounded)").unwrap();
1324 match v {
1325 BorderStyleValue::Var {
1326 name,
1327 fallback: Some(fb),
1328 } => {
1329 assert_eq!(name, "bs");
1330 assert_eq!(*fb, BorderStyleValue::Fixed(BorderStyle::Rounded));
1331 }
1332 other => panic!("expected Var with fallback, got {other:?}"),
1333 }
1334 }
1335
1336 #[test]
1337 fn border_style_value_display_roundtrip() {
1338 assert_eq!(
1339 BorderStyleValue::Fixed(BorderStyle::Rounded).to_string(),
1340 "rounded"
1341 );
1342 assert_eq!(BorderStyleValue::var("bs").to_string(), "var(--bs)");
1343 }
1344
1345 #[test]
1346 fn border_shorthand_accepts_var_style_component() {
1347 let spec = BorderSpec::parse_shorthand("var(--bs)").unwrap();
1349 assert_eq!(spec.style, BorderStyleValue::var("bs"));
1350 assert_eq!(spec.edges, Some(Borders::ALL));
1351 let spec = BorderSpec::parse_shorthand("var(--bs) #f00").unwrap();
1353 assert_eq!(spec.style, BorderStyleValue::var("bs"));
1354 use ratatui::style::Color as RC;
1355 assert_eq!(spec.color, Some(Color::literal(RC::Rgb(0xff, 0, 0))));
1356 }
1357
1358 #[test]
1359 fn border_shorthand_var_with_fallback_in_style() {
1360 let spec = BorderSpec::parse_shorthand("var(--bs, rounded) #f00").unwrap();
1362 assert_eq!(
1363 spec.style,
1364 BorderStyleValue::Var {
1365 name: "bs".into(),
1366 fallback: Some(Box::new(BorderStyleValue::Fixed(BorderStyle::Rounded))),
1367 }
1368 );
1369 }
1370
1371 #[cfg(feature = "serde")]
1372 #[test]
1373 fn box_edges_value_serde_roundtrip() {
1374 let v = BoxEdgesValue::Edges(BoxEdges::uniform(2));
1375 let json = serde_json::to_string(&v).unwrap();
1376 let back: BoxEdgesValue = serde_json::from_str(&json).unwrap();
1377 assert_eq!(back, v);
1378 let v = BoxEdgesValue::var("pad");
1380 let json = serde_json::to_string(&v).unwrap();
1381 assert!(json.contains("var(--pad)"), "serialize var: {json}");
1382 let back: BoxEdgesValue = serde_json::from_str(&json).unwrap();
1383 assert_eq!(back, v);
1384 }
1385
1386 #[cfg(feature = "serde")]
1387 #[test]
1388 fn border_style_value_serde_roundtrip() {
1389 let v = BorderStyleValue::Fixed(BorderStyle::Rounded);
1390 let json = serde_json::to_string(&v).unwrap();
1391 let back: BorderStyleValue = serde_json::from_str(&json).unwrap();
1392 assert_eq!(back, v);
1393 let v = BorderStyleValue::var("bs");
1394 let json = serde_json::to_string(&v).unwrap();
1395 assert!(json.contains("var(--bs)"), "serialize var: {json}");
1396 let back: BorderStyleValue = serde_json::from_str(&json).unwrap();
1397 assert_eq!(back, v);
1398 }
1399}
1400
1401#[cfg(feature = "serde")]
1406mod serde_impl {
1407 use super::{
1408 length_to_css, BorderSpec, BorderStyle, BorderStyleValue, BoxEdges, BoxEdgesValue, Length,
1409 };
1410 use crate::color::Color;
1411 use ratatui::widgets::Borders;
1412 use serde::{
1413 de::{self, MapAccess, Visitor},
1414 Deserialize, Deserializer, Serialize, Serializer,
1415 };
1416 use std::fmt;
1417
1418 impl<'de> Deserialize<'de> for BoxEdges {
1425 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1426 struct BoxEdgesVisitor;
1427
1428 impl<'de> Visitor<'de> for BoxEdgesVisitor {
1429 type Value = BoxEdges;
1430
1431 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1432 f.write_str("a CSS box shorthand (number or string)")
1433 }
1434
1435 fn visit_i64<E: de::Error>(self, v: i64) -> Result<BoxEdges, E> {
1436 Ok(BoxEdges::uniform(v.max(0) as u16))
1437 }
1438 fn visit_u64<E: de::Error>(self, v: u64) -> Result<BoxEdges, E> {
1439 Ok(BoxEdges::uniform(v as u16))
1440 }
1441 fn visit_f64<E: de::Error>(self, v: f64) -> Result<BoxEdges, E> {
1442 Ok(BoxEdges::uniform(v.max(0.0) as u16))
1443 }
1444 fn visit_str<E: de::Error>(self, v: &str) -> Result<BoxEdges, E> {
1445 BoxEdges::parse(v).map_err(E::custom)
1446 }
1447 fn visit_string<E: de::Error>(self, v: String) -> Result<BoxEdges, E> {
1448 BoxEdges::parse(&v).map_err(E::custom)
1449 }
1450 }
1451
1452 d.deserialize_any(BoxEdgesVisitor)
1453 }
1454 }
1455 impl Serialize for BoxEdges {
1456 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
1457 if self.top == self.right && self.right == self.bottom && self.bottom == self.left {
1458 s.serialize_u64(self.top as u64)
1459 } else {
1460 s.serialize_str(&format!(
1461 "{} {} {} {}",
1462 self.top, self.right, self.bottom, self.left
1463 ))
1464 }
1465 }
1466 }
1467
1468 impl<'de> Deserialize<'de> for Length {
1473 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1474 struct LengthVisitor;
1475
1476 impl<'de> Visitor<'de> for LengthVisitor {
1477 type Value = Length;
1478
1479 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1480 f.write_str("a CSS length (number or string)")
1481 }
1482
1483 fn visit_i64<E: de::Error>(self, v: i64) -> Result<Length, E> {
1484 Ok(Length::Cells(v.max(0) as u16))
1485 }
1486 fn visit_u64<E: de::Error>(self, v: u64) -> Result<Length, E> {
1487 Ok(Length::Cells(v as u16))
1488 }
1489 fn visit_f64<E: de::Error>(self, v: f64) -> Result<Length, E> {
1490 Ok(Length::Cells(v.max(0.0) as u16))
1491 }
1492 fn visit_str<E: de::Error>(self, v: &str) -> Result<Length, E> {
1493 Length::parse(v).map_err(E::custom)
1494 }
1495 fn visit_string<E: de::Error>(self, v: String) -> Result<Length, E> {
1496 Length::parse(&v).map_err(E::custom)
1497 }
1498 }
1499
1500 d.deserialize_any(LengthVisitor)
1501 }
1502 }
1503 impl Serialize for Length {
1504 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
1505 match self {
1506 Length::Auto => s.serialize_str("auto"),
1507 Length::Cells(n) => s.serialize_str(&format!("{n}px")),
1508 Length::Percent(p) => s.serialize_str(&format!("{p}%")),
1509 Length::Min(n) => s.serialize_str(&format!("min({n})")),
1510 Length::Max(n) => s.serialize_str(&format!("max({n})")),
1511 Length::Var {
1512 name,
1513 fallback: None,
1514 } => s.serialize_str(&format!("var(--{name})")),
1515 Length::Var {
1516 name,
1517 fallback: Some(fb),
1518 } => s.serialize_str(&format!("var(--{name}, {})", length_to_css(fb))),
1519 }
1520 }
1521 }
1522
1523 impl<'de> Deserialize<'de> for BorderStyle {
1528 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1529 struct BorderStyleVisitor;
1530
1531 impl<'de> Visitor<'de> for BorderStyleVisitor {
1532 type Value = BorderStyle;
1533
1534 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1535 f.write_str("a border style keyword")
1536 }
1537
1538 fn visit_str<E: de::Error>(self, v: &str) -> Result<BorderStyle, E> {
1539 BorderStyle::parse_keyword(v)
1540 .ok_or_else(|| E::custom(format!("invalid border style: {v}")))
1541 }
1542
1543 fn visit_string<E: de::Error>(self, v: String) -> Result<BorderStyle, E> {
1544 BorderStyle::parse_keyword(&v)
1545 .ok_or_else(|| E::custom(format!("invalid border style: {v}")))
1546 }
1547 }
1548
1549 d.deserialize_str(BorderStyleVisitor)
1550 }
1551 }
1552 impl Serialize for BorderStyle {
1553 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
1554 s.serialize_str(self.as_keyword())
1555 }
1556 }
1557
1558 impl<'de> Deserialize<'de> for BoxEdgesValue {
1566 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1567 struct BoxEdgesValueVisitor;
1568
1569 impl<'de> Visitor<'de> for BoxEdgesValueVisitor {
1570 type Value = BoxEdgesValue;
1571
1572 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1573 f.write_str("a CSS box shorthand or var() string (number or string)")
1574 }
1575
1576 fn visit_i64<E: de::Error>(self, v: i64) -> Result<BoxEdgesValue, E> {
1577 Ok(BoxEdgesValue::Edges(BoxEdges::uniform(v.max(0) as u16)))
1578 }
1579 fn visit_u64<E: de::Error>(self, v: u64) -> Result<BoxEdgesValue, E> {
1580 Ok(BoxEdgesValue::Edges(BoxEdges::uniform(v as u16)))
1581 }
1582 fn visit_f64<E: de::Error>(self, v: f64) -> Result<BoxEdgesValue, E> {
1583 Ok(BoxEdgesValue::Edges(BoxEdges::uniform(v.max(0.0) as u16)))
1584 }
1585 fn visit_str<E: de::Error>(self, v: &str) -> Result<BoxEdgesValue, E> {
1586 BoxEdgesValue::parse(v).map_err(E::custom)
1587 }
1588 fn visit_string<E: de::Error>(self, v: String) -> Result<BoxEdgesValue, E> {
1589 BoxEdgesValue::parse(&v).map_err(E::custom)
1590 }
1591 }
1592
1593 d.deserialize_any(BoxEdgesValueVisitor)
1594 }
1595 }
1596 impl Serialize for BoxEdgesValue {
1597 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
1598 match self {
1599 BoxEdgesValue::Edges(e) => {
1600 if e.top == e.right && e.right == e.bottom && e.bottom == e.left {
1601 s.serialize_u64(e.top as u64)
1602 } else {
1603 s.serialize_str(&format!(
1604 "{} {} {} {}",
1605 e.top, e.right, e.bottom, e.left
1606 ))
1607 }
1608 }
1609 BoxEdgesValue::Var { .. } => s.serialize_str(&self.to_string()),
1610 }
1611 }
1612 }
1613
1614 impl<'de> Deserialize<'de> for BorderStyleValue {
1619 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1620 struct BorderStyleValueVisitor;
1621
1622 impl<'de> Visitor<'de> for BorderStyleValueVisitor {
1623 type Value = BorderStyleValue;
1624
1625 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1626 f.write_str("a border style keyword or var() string")
1627 }
1628
1629 fn visit_str<E: de::Error>(self, v: &str) -> Result<BorderStyleValue, E> {
1630 BorderStyleValue::parse(v).map_err(E::custom)
1631 }
1632 fn visit_string<E: de::Error>(self, v: String) -> Result<BorderStyleValue, E> {
1633 BorderStyleValue::parse(&v).map_err(E::custom)
1634 }
1635 }
1636
1637 d.deserialize_str(BorderStyleValueVisitor)
1638 }
1639 }
1640 impl Serialize for BorderStyleValue {
1641 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
1642 s.serialize_str(&self.to_string())
1643 }
1644 }
1645
1646 enum EdgesInput {
1653 None,
1654 Some(Borders),
1655 }
1656
1657 impl<'de> Deserialize<'de> for EdgesInput {
1658 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1659 struct EdgesVisitor;
1660
1661 impl<'de> Visitor<'de> for EdgesVisitor {
1662 type Value = EdgesInput;
1663
1664 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1665 f.write_str("an edges keyword string or a bit integer")
1666 }
1667
1668 fn visit_unit<E: de::Error>(self) -> Result<EdgesInput, E> {
1669 Ok(EdgesInput::None)
1670 }
1671 fn visit_none<E: de::Error>(self) -> Result<EdgesInput, E> {
1672 Ok(EdgesInput::None)
1673 }
1674 fn visit_i64<E: de::Error>(self, v: i64) -> Result<EdgesInput, E> {
1675 let bits = v as u8;
1676 Ok(EdgesInput::Some(
1677 Borders::from_bits(bits).unwrap_or(Borders::NONE),
1678 ))
1679 }
1680 fn visit_u64<E: de::Error>(self, v: u64) -> Result<EdgesInput, E> {
1681 let bits = v as u8;
1682 Ok(EdgesInput::Some(
1683 Borders::from_bits(bits).unwrap_or(Borders::NONE),
1684 ))
1685 }
1686 fn visit_f64<E: de::Error>(self, v: f64) -> Result<EdgesInput, E> {
1687 let bits = v as u8;
1688 Ok(EdgesInput::Some(
1689 Borders::from_bits(bits).unwrap_or(Borders::NONE),
1690 ))
1691 }
1692 fn visit_str<E: de::Error>(self, v: &str) -> Result<EdgesInput, E> {
1693 BorderSpec::parse_edges(v)
1694 .map(EdgesInput::Some)
1695 .ok_or_else(|| E::custom(format!("invalid edges: {v}")))
1696 }
1697 fn visit_string<E: de::Error>(self, v: String) -> Result<EdgesInput, E> {
1698 BorderSpec::parse_edges(&v)
1699 .map(EdgesInput::Some)
1700 .ok_or_else(|| E::custom(format!("invalid edges: {v}")))
1701 }
1702 }
1703
1704 d.deserialize_any(EdgesVisitor)
1705 }
1706 }
1707
1708 enum ColorInput {
1714 None,
1715 Some(Color),
1716 }
1717
1718 impl<'de> Deserialize<'de> for ColorInput {
1719 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1720 struct ColorInputVisitor;
1721
1722 impl<'de> Visitor<'de> for ColorInputVisitor {
1723 type Value = ColorInput;
1724
1725 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1726 f.write_str("a CSS color string or null")
1727 }
1728
1729 fn visit_unit<E: de::Error>(self) -> Result<ColorInput, E> {
1730 Ok(ColorInput::None)
1731 }
1732 fn visit_none<E: de::Error>(self) -> Result<ColorInput, E> {
1733 Ok(ColorInput::None)
1734 }
1735 fn visit_str<E: de::Error>(self, v: &str) -> Result<ColorInput, E> {
1736 Color::parse(v).map(ColorInput::Some).map_err(E::custom)
1737 }
1738 fn visit_string<E: de::Error>(self, v: String) -> Result<ColorInput, E> {
1739 Color::parse(&v).map(ColorInput::Some).map_err(E::custom)
1740 }
1741 }
1742
1743 d.deserialize_any(ColorInputVisitor)
1744 }
1745 }
1746
1747 impl<'de> Deserialize<'de> for BorderSpec {
1755 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1756 struct BorderSpecVisitor;
1757
1758 impl<'de> Visitor<'de> for BorderSpecVisitor {
1759 type Value = BorderSpec;
1760
1761 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1762 f.write_str("a border shorthand string or a border object")
1763 }
1764
1765 fn visit_str<E: de::Error>(self, v: &str) -> Result<BorderSpec, E> {
1766 BorderSpec::parse_shorthand(v).map_err(E::custom)
1767 }
1768
1769 fn visit_string<E: de::Error>(self, v: String) -> Result<BorderSpec, E> {
1770 BorderSpec::parse_shorthand(&v).map_err(E::custom)
1771 }
1772
1773 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<BorderSpec, A::Error> {
1774 let mut style: Option<BorderStyleValue> = None;
1775 let mut color: Option<Color> = None;
1776 let mut edges: Option<Borders> = None;
1777 while let Some(key) = map.next_key::<String>()? {
1778 match key.as_str() {
1779 "style" => {
1780 style = Some(map.next_value()?);
1781 }
1782 "color" => match map.next_value::<ColorInput>()? {
1783 ColorInput::Some(c) => color = Some(c),
1784 ColorInput::None => {}
1785 },
1786 "edges" => match map.next_value::<EdgesInput>()? {
1787 EdgesInput::Some(e) => edges = Some(e),
1788 EdgesInput::None => {}
1789 },
1790 _ => {
1792 let _: de::IgnoredAny = map.next_value()?;
1793 }
1794 }
1795 }
1796 Ok(BorderSpec {
1797 style: style.unwrap_or_default(),
1798 color,
1799 edges,
1800 })
1801 }
1802 }
1803
1804 d.deserialize_any(BorderSpecVisitor)
1805 }
1806 }
1807 impl Serialize for BorderSpec {
1808 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
1809 use serde::ser::SerializeStruct;
1810 let mut st = s.serialize_struct("BorderSpec", 3)?;
1811 st.serialize_field("style", &self.style)?;
1812 st.serialize_field("color", &self.color)?;
1813 match self.edges {
1815 None => st.serialize_field("edges", &None::<&str>)?,
1816 Some(e) => st.serialize_field("edges", BorderSpec::edges_to_keyword(e))?,
1817 }
1818 st.end()
1819 }
1820 }
1821}