Skip to main content

ratatui_style/
box_model.rs

1//! Box-model value types: padding, margin, border, and sizing lengths.
2//!
3//! These are *descriptors*; they are projected onto ratatui primitives
4//! (`Padding`, `Borders`/`BorderType`, `Constraint`) by methods in `style.rs`.
5
6use 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// ---------------------------------------------------------------------------
15// Padding / margin
16// ---------------------------------------------------------------------------
17
18/// One value per edge, in terminal cells (top, right, bottom, left).
19#[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    /// Parse a CSS shorthand: `1`, `1 2`, `1 2 3`, or `1 2 3 4`.
47    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 values (the CSS shorthand maximum): top, right, bottom, left.
73            4 => Ok(Self {
74                top: nums[0],
75                right: nums[1],
76                bottom: nums[2],
77                left: nums[3],
78            }),
79            // CSS shorthand allows at most 4 values.
80            _ => Err(CssError::invalid_length(format!(
81                "box shorthand allows at most 4 values, got {}: {shorthand}",
82                nums.len()
83            ))),
84        }
85    }
86
87    /// Project onto a ratatui `Padding` (used for `Block::padding`).
88    pub fn to_padding(self) -> Padding {
89        Padding::new(self.left, self.right, self.top, self.bottom)
90    }
91
92    /// Shrink a `Rect` outward by these edges (for `margin`).
93    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// ---------------------------------------------------------------------------
107// Border
108// ---------------------------------------------------------------------------
109
110/// Border drawing style. Width is implicit in the terminal (always 1 cell).
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
112pub enum BorderStyle {
113    /// No border.
114    #[default]
115    None,
116    /// A plain single-line border.
117    Single,
118    /// A rounded single-line border (`border-radius`).
119    Rounded,
120    /// A double-line border.
121    Double,
122    /// A thick single-line border.
123    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/// A full border declaration: style + optional color + optional per-edge set.
139///
140/// The `edges` field is the per-edge control point:
141/// - `None` (the default) means "not explicitly declared". For backwards
142///   compatibility a spec with a non-`None` style but `edges == None` still
143///   draws **all four** edges (the legacy `.rounded` behavior) — see
144///   [`BorderSpec::borders`].
145/// - `Some(set)` selects exactly which edges (`Borders::TOP`, `LEFT`, etc.) are
146///   drawn. This is set by the `border-top`/`border-right`/… declarations and by
147///   the full `border` shorthand (which forces `Some(Borders::ALL)`).
148#[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    /// Render an edges set as a human-readable CSS-ish keyword string:
167    /// `all`, `none`, `top`, `top|bottom`, `left|right`, etc. Edges are emitted
168    /// in a stable order (top, right, bottom, left) joined by `|`.
169    pub fn edges_to_keyword(edges: Borders) -> &'static str {
170        // Borders is a 4-bit bitset (TOP=1, RIGHT=2, BOTTOM=4, LEFT=8), so
171        // there are exactly 16 distinct combinations. Pre-compute every one
172        // into a fixed table of `&'static str` literals — no allocation, no
173        // `Box::leak`. Index by the raw bits (0..=15). Edges in each entry are
174        // emitted in reading order: top, right, bottom, left.
175        const EDGES_KW: [&str; 16] = [
176            "none",              // 0000
177            "top",               // 0001  TOP
178            "right",             // 0010  RIGHT
179            "top|right",         // 0011  TOP | RIGHT
180            "bottom",            // 0100  BOTTOM
181            "top|bottom",        // 0101  TOP | BOTTOM
182            "right|bottom",      // 0110  RIGHT | BOTTOM
183            "top|right|bottom",  // 0111  TOP | RIGHT | BOTTOM
184            "left",              // 1000  LEFT
185            "top|left",          // 1001  TOP | LEFT
186            "right|left",        // 1010  RIGHT | LEFT
187            "top|right|left",    // 1011  TOP | RIGHT | LEFT
188            "bottom|left",       // 1100  BOTTOM | LEFT
189            "top|bottom|left",   // 1101  TOP | BOTTOM | LEFT
190            "right|bottom|left", // 1110  RIGHT | BOTTOM | LEFT
191            "all",               // 1111  ALL
192        ];
193        let bits = edges.bits() as usize;
194        if bits >= EDGES_KW.len() {
195            // Defensive: a malformed bitset outside 0..=15. "none" is the
196            // closest sensible keyword (no edges declared).
197            return "none";
198        }
199        EDGES_KW[bits]
200    }
201
202    /// Parse an edges keyword string (the inverse of [`Self::edges_to_keyword`])
203    /// into a `Borders` set. Accepts `all`, `none`, any of the single edges
204    /// (`top`/`right`/`bottom`/`left`), and `x`/`y` convenience aliases, plus
205    /// `|`-separated combinations. Whitespace is tolerated.
206    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    /// The ratatui `Borders` set this spec draws.
230    ///
231    /// A `BorderStyle::None` style draws nothing. Otherwise the explicit
232    /// `edges` set is used, defaulting to `Borders::ALL` when `edges` is `None`
233    /// (the legacy "style set without a per-edge declaration draws all four
234    /// sides" semantics, kept so existing `.rounded { border-style: rounded }`
235    /// rules keep drawing a full box).
236    ///
237    /// A `BorderStyleValue::Var` that has not yet been resolved counts as
238    /// "not None" (the edges are kept) — after cascade resolution no `Var`
239    /// should survive, but this keeps the pre-resolution `borders()` sensible.
240    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    /// Parse a CSS shorthand: `none` / `single` / `rounded` / `double` / `thick`
256    /// (or a `var(--name)` reference), optionally with a width (`1px`) and a
257    /// color (`rounded #f00`, `var(--bs) #f00`).
258    ///
259    /// A token starting with `var(` (case-insensitive) becomes a
260    /// [`BorderStyleValue::Var`]; to support `var(--name, fallback)` with a
261    /// fallback that may contain spaces, a `var(` token is re-joined with the
262    /// remaining tokens up to the matching `)`. Otherwise the token is parsed
263    /// as a [`BorderStyle`] keyword, and anything that is neither a keyword nor
264    /// a `px` width becomes the color.
265    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        // Track consumed byte ranges so we can slice the original `s` for color
272        // tokens that the whitespace split also visits.
273        let mut consumed: Vec<(usize, usize)> = Vec::new();
274        while i < bytes.len() {
275            // Skip whitespace.
276            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            // Find the end of this token (next whitespace).
284            while i < bytes.len() && !bytes[i].is_ascii_whitespace() {
285                i += 1;
286            }
287            let tok = &s[start..i];
288            // Case-insensitive `var(` prefix check on the lowered source.
289            if lowered[start..i].starts_with("var(") {
290                // Re-join tokens until the matching ')'. The `)` may be on a
291                // later token, so consume forward.
292                let mut joined = String::from(tok);
293                // If this token doesn't end with ')', pull in more.
294                while !joined.ends_with(')') && i < bytes.len() {
295                    // Skip exactly one whitespace run and append it + next token.
296                    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                    // Append the whitespace we skipped + the token.
308                    joined.push_str(&s[ws_start..i]);
309                    let _ = t2_start; // for clarity; the slice already covers it
310                }
311                // Ambiguity: a `var()` token could be either the style or the
312                // color component of the shorthand. We treat it as a style ONLY
313                // when no style has been set yet (neither a keyword nor a prior
314                // var-style); once a style is set, a later `var()` is the color.
315                // This makes both `var(--bs) #f00` (var as style) and
316                // `rounded var(--rim)` (var as color) parse correctly.
317                let style_already_set =
318                    !matches!(style, BorderStyleValue::Fixed(BorderStyle::None));
319                if style_already_set {
320                    // Treat as a color token — push the FULL rejoined var(...)
321                    // expression (which may span multiple whitespace-separated
322                    // tokens) so a fallback like `var(--nope, #00ff00)` survives.
323                    // `[start..i]` is a slice of the original `s` that covers the
324                    // whole expression, so it satisfies the `&str` borrow.
325                    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                // width — present-but-ignored (terminal borders are always 1 cell).
332                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 token — handled in the second pass below.
338                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        // The full `border` shorthand declares a *complete* border: edges are
347        // set to ALL so that, e.g., `border: rounded` draws all four sides.
348        // (Per-edge declarations like `border-bottom` set a subset instead.)
349        Ok(Self {
350            style,
351            color,
352            edges: Some(Borders::ALL),
353        })
354    }
355
356    /// Merge another spec's *declared* sub-fields into this one in place.
357    ///
358    /// - `style` and `color` follow the existing sentinel rule (a non-`None`
359    ///   style or `Some` color overrides; see below).
360    /// - `edges` **accumulates** by OR when the other spec declares any: this
361    ///   lets `.border-top` and `.border-bottom` compose into a top+bottom set
362    ///   rather than one clobbering the other, mirroring how `.rounded` +
363    ///   `.border-slate-700` compose on style/color.
364    ///
365    /// A sub-field counts as declared when its style is not
366    /// [`BorderStyle::None`] (the default, reused as a "not declared"
367    /// sentinel) or its color is `Some`. This is the per-declaration step of
368    /// the cascade that lets two atomic rules — e.g. `.rounded` (style only)
369    /// and `.border-slate-700` (color only) — compose into one border instead
370    /// of one clobbering the other.
371    ///
372    /// A `BorderStyleValue::Var` counts as declared (it may resolve to a
373    /// non-`None` style), so it overrides any existing style.
374    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        // Per-edge declarations accumulate: `border-top` + `border-bottom`
384        // → TOP | BOTTOM. A spec that never declares edges (the legacy
385        // `border-style`/`border-color` path) leaves `self.edges` untouched.
386        if let Some(oe) = other.edges {
387            self.edges = Some(self.edges.unwrap_or(Borders::NONE) | oe);
388        }
389    }
390}
391
392impl BorderStyle {
393    /// Parse a single keyword, case-insensitive.
394    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// ---------------------------------------------------------------------------
417// var()-carrying wrappers: BoxEdgesValue / BorderStyleValue
418// ---------------------------------------------------------------------------
419
420/// A `BoxEdges` value **or** a `var(--name)` reference, for `padding`/`margin`.
421///
422/// `BoxEdges` itself is `Copy` (4× `u16`) and cannot carry a heap `String`, so
423/// the var name lives in this wrapper enum. The cascade
424/// ([`crate::cascade`]) resolves `Var` against the token table; an unresolved
425/// `Var` degrades to zero edges (mirroring how an unresolved color `Var`
426/// degrades to `Reset`).
427#[derive(Debug, Clone, PartialEq)]
428pub enum BoxEdgesValue {
429    /// A concrete set of edges.
430    Edges(BoxEdges),
431    /// A `var(--name[, fallback])` reference, resolved during the cascade.
432    Var {
433        name: String,
434        fallback: Option<Box<BoxEdgesValue>>,
435    },
436}
437
438impl BoxEdgesValue {
439    /// Parse a CSS shorthand that may be a `var(--name)` reference.
440    ///
441    /// If `s` starts with `var(` (case-insensitive), the name and optional
442    /// fallback are split on the first top-level comma (mirroring `Length::parse`
443    /// / `Color::parse_var`); the fallback recurses via
444    /// [`BoxEdgesValue::parse`]. Otherwise `s` is parsed as a concrete
445    /// [`BoxEdges`] shorthand.
446    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            // `rest` still has the trailing ')'.
451            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    /// `true` if this is a `var()` reference that needs token resolution.
469    pub fn is_var(&self) -> bool {
470        matches!(self, Self::Var { .. })
471    }
472
473    /// Shortcut for `var(--name)` with no fallback.
474    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/// A `BorderStyle` value **or** a `var(--name)` reference, for `border-style`.
508///
509/// `BorderStyle` itself is `Copy` and cannot carry a heap `String`, so the var
510/// name lives in this wrapper enum. The cascade resolves `Var` against the
511/// token table; an unresolved `Var` degrades to `BorderStyle::None`.
512#[derive(Debug, Clone, PartialEq)]
513pub enum BorderStyleValue {
514    /// A concrete border style.
515    Fixed(BorderStyle),
516    /// A `var(--name[, fallback])` reference, resolved during the cascade.
517    Var {
518        name: String,
519        fallback: Option<Box<BorderStyleValue>>,
520    },
521}
522
523impl BorderStyleValue {
524    /// Parse a keyword or a `var(--name)` reference.
525    ///
526    /// If `s` starts with `var(` (case-insensitive), the name and optional
527    /// fallback are split on the first top-level comma (mirroring
528    /// [`BoxEdgesValue::parse`]); the fallback recurses via
529    /// [`BorderStyleValue::parse`]. Otherwise `s` must be a valid
530    /// [`BorderStyle`] keyword (error if not — mirroring the current
531    /// `border-style` declaration behavior).
532    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    /// `true` if this is a `var()` reference that needs token resolution.
557    pub fn is_var(&self) -> bool {
558        matches!(self, Self::Var { .. })
559    }
560
561    /// Return the concrete [`BorderStyle`] if this is `Fixed`, else `None`
562    /// (for an unresolved `Var`). Used by projections that need a concrete
563    /// style; a `Var` degrades to `None` (treated as `BorderStyle::None`).
564    pub fn as_fixed(&self) -> Option<BorderStyle> {
565        match self {
566            Self::Fixed(b) => Some(*b),
567            Self::Var { .. } => None,
568        }
569    }
570
571    /// Shortcut for `var(--name)` with no fallback.
572    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// ---------------------------------------------------------------------------
605// Length / sizing
606// ---------------------------------------------------------------------------
607
608/// A one-dimensional size, mapped to a ratatui `Constraint`.
609///
610/// Not `Copy`: the [`Length::Var`] variant carries a heap-allocated name, so a
611/// `Length` must be `.clone()`-d when duplicated (which is rare outside the
612/// cascade, where `var()` references have already been resolved away).
613#[derive(Debug, Clone, PartialEq)]
614pub enum Length {
615    /// `auto` — let the layout engine decide (becomes `Min(0)`).
616    Auto,
617    /// A fixed cell count (`10`, `10px`).
618    Cells(u16),
619    /// A percentage of the available space (`50%`).
620    Percent(u16),
621    /// `min(n)` — at least `n` cells, grow if room.
622    Min(u16),
623    /// `max(n)` — at most `n` cells.
624    Max(u16),
625    /// A `var(--name)` reference, resolved against the token table during the
626    /// cascade. A `Length::Var` should never survive into `to_constraint` — if
627    /// one does (e.g. an unresolved variable in lenient mode), it degrades to
628    /// `Min(0)` (same as [`Length::Auto`]) rather than panicking.
629    ///
630    /// Fallback (`var(--x, 10)`) is not yet supported: if a fallback is present
631    /// it is currently ignored and only the name is captured.
632    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        // var(--name) — recognized first, before any numeric/keyword logic.
642        // A fallback (e.g. `var(--x, 10)`) is split on the FIRST top-level comma
643        // (honoring nested parens) and parsed as a Length — mirroring how
644        // `Color::parse_var` handles the color var() fallback.
645        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            // Should have been resolved during the cascade; if it reaches here,
689            // prefer a fallback's constraint, else degrade like Auto (Min(0)).
690            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/// Render a [`Length`] to its CSS string form (used by serde Serialize).
706#[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
727// ---------------------------------------------------------------------------
728// Conversion traits — typed (infallible) or string-shorthand input for the
729// `CssStyle::padding` / `margin` / `border` builders.
730// ---------------------------------------------------------------------------
731
732/// Input accepted by [`crate::style::CssStyle::padding`] /
733/// [`crate::style::CssStyle::margin`]: a typed value (zero panic) or a CSS
734/// shorthand string (panics on a malformed literal).
735///
736/// - `u16` → uniform edges on all four sides.
737/// - `(u16, u16)` → CSS two-value shorthand: `top = bottom = a`, `left = right = b`.
738/// - `(u16, u16, u16, u16)` → `(top, right, bottom, left)`.
739/// - `&str` → CSS shorthand (`"1"`, `"1 2"`, `"1 2 3"`, `"1 2 3 4"`); a bad
740///   literal **panics**. Only use the string form for compile-time-known
741///   literals — pass a `u16` or tuple for infallible construction.
742/// - [`BoxEdges`] / [`BoxEdgesValue`] → passed through (identity). A
743///   `BoxEdgesValue::Var` can be passed to declare a `var()` padding/margin
744///   programmatically.
745pub 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
799/// Input accepted by [`crate::style::CssStyle::border`]: a typed value (zero
800/// panic) or a CSS shorthand string (panics on a malformed literal).
801///
802/// - [`BorderStyle`] → spec with that style and no color.
803/// - `(BorderStyle, C) where C: Into<Color>` → spec with that style and color;
804///   e.g. `(BorderStyle::Rounded, "#00d4ff")` or
805///   `(BorderStyle::Rounded, RColor::Cyan)`.
806/// - `&str` → CSS shorthand (`"rounded"`, `"rounded #f00"`, …); a bad literal
807///   **panics**. Only use the string form for compile-time-known literals —
808///   pass a `BorderStyle` or `(BorderStyle, color)` for infallible construction.
809pub trait IntoBorderSpec {
810    fn into_spec(self) -> BorderSpec;
811}
812
813impl IntoBorderSpec for BorderStyle {
814    fn into_spec(self) -> BorderSpec {
815        // edges: None → borders() falls back to ALL (legacy behavior).
816        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        // `.rounded` (style only) + `.border-blue` (color only) compose into
856        // one spec rather than one clobbering the other.
857        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)); // survived
869        assert_eq!(a.color, Some(Color::literal(RC::Blue))); // applied
870
871        // An all-default other (style=None, no color) declares nothing → merge
872        // leaves the existing spec untouched.
873        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        // CSS shorthand allows at most 4 values; 5 must be an error.
894        assert!(BoxEdges::parse("1 2 3 4 5").is_err());
895        assert!(BoxEdges::parse("1 2 3 4 5 6").is_err());
896        // 4 is still fine.
897        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        // Numeric/percent still parse as before.
925        assert_eq!(Length::parse("10").unwrap(), Length::Cells(10));
926        assert_eq!(Length::parse("50%").unwrap(), Length::Percent(50));
927        // A fallback is now captured and parsed as a Length.
928        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        // A percent fallback parses to Percent.
936        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        // Empty name is an error.
944        assert!(Length::parse("var(--)").is_err());
945    }
946
947    #[test]
948    fn length_var_degrades_to_min_zero() {
949        // A Var without a fallback that reaches to_constraint degrades like Auto.
950        assert_eq!(
951            Length::Var {
952                name: "x".into(),
953                fallback: None
954            }
955            .to_constraint(),
956            Constraint::Min(0)
957        );
958        // A Var WITH a fallback uses the fallback's constraint.
959        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    // -----------------------------------------------------------------
1028    // Per-edge border
1029    // -----------------------------------------------------------------
1030
1031    #[test]
1032    fn border_full_shorthand_all_edges() {
1033        // The full `border` shorthand declares edges == ALL.
1034        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        // A spec built the legacy way (style set, edges == None) still draws
1043        // all four edges — this is the regression-protected `.rounded` path.
1044        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        // A None style short-circuits to NONE regardless of edges.
1055        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        // `.border-top` + `.border-bottom` compose into TOP | BOTTOM via merge,
1066        // mirroring how `.rounded` + `.border-color` compose on style/color.
1067        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)); // survived
1079        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        // A legacy spec (edges == None) merged into a per-edge spec must NOT
1086        // clobber the accumulated edges — merge only ORs when other declares.
1087        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)); // unchanged
1099    }
1100
1101    #[test]
1102    fn per_edge_full_shorthand_then_edge_widens() {
1103        // A full `border: rounded` (edges=ALL) followed by a `border-bottom`
1104        // declaration: merge ORs ALL | BOTTOM == ALL (no narrowing). And a full
1105        // shorthand after edges keeps ALL.
1106        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        // edges_to_keyword emits in a fixed reading order (top, right, bottom,
1123        // left); parse_edges accepts that same order AND the reverse, so the
1124        // round-trip pairs below match the emit order exactly.
1125        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        // The reverse order parses back to the same set.
1145        assert_eq!(
1146            BorderSpec::parse_edges("left|right"),
1147            Some(Borders::LEFT | Borders::RIGHT)
1148        );
1149        // x / y convenience aliases parse but emit as right|left / top|bottom.
1150        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        // The function returns &'static str literals from a fixed 16-entry
1163        // table (no Box::leak). Verify every possible 4-bit Borders set emits
1164        // the right keyword (reading order: top, right, bottom, left) and that
1165        // each keyword round-trips back through parse_edges.
1166        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            // The keyword must round-trip back to the same set.
1200            assert_eq!(BorderSpec::parse_edges(kw), Some(edges), "roundtrip {kw}");
1201        }
1202    }
1203
1204    // -----------------------------------------------------------------
1205    // BoxEdgesValue / BorderStyleValue (var() wrappers)
1206    // -----------------------------------------------------------------
1207
1208    #[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        // Case-insensitive VAR( prefix.
1218        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        // 4-value fallback parses as a full BoxEdges.
1252        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        // Concrete uniform → single number.
1272        let v = BoxEdgesValue::Edges(BoxEdges::uniform(3));
1273        assert_eq!(v.to_string(), "3");
1274        // Concrete multi → "1 2 3 4".
1275        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        // Var no fallback.
1283        let v = BoxEdgesValue::var("pad");
1284        assert_eq!(v.to_string(), "var(--pad)");
1285        // Var with fallback.
1286        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        // `border: var(--bs)` — the style component is a var.
1348        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        // `border: var(--bs) #f00` — var style + a literal color.
1352        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        // A var with a fallback that is itself a keyword: "var(--bs, rounded)".
1361        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        // Var form.
1379        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// ---------------------------------------------------------------------------
1402// Optional serde
1403// ---------------------------------------------------------------------------
1404
1405#[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    // -------------------------------------------------------------------------
1419    // BoxEdges — a bare integer (uniform) OR a CSS shorthand string. Driven
1420    // via `deserialize_any` so the same impl works for JSON, TOML, and YAML
1421    // without ever materializing a `serde_json::Value`.
1422    // -------------------------------------------------------------------------
1423
1424    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    // -------------------------------------------------------------------------
1469    // Length — same pattern as BoxEdges: number → Cells, string → parse.
1470    // -------------------------------------------------------------------------
1471
1472    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    // -------------------------------------------------------------------------
1524    // BorderStyle — a keyword string. Format-agnostic str visitor.
1525    // -------------------------------------------------------------------------
1526
1527    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    // -------------------------------------------------------------------------
1559    // BoxEdgesValue — a number/shorthand string (→ Edges) OR a `var(...)` /
1560    // `"1 2"` string. Mirrors BoxEdges' deserialize_any so an integer uniform
1561    // value stays an integer in JSON/TOML/YAML, while a multi-value or var
1562    // form is a string.
1563    // -------------------------------------------------------------------------
1564
1565    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    // -------------------------------------------------------------------------
1615    // BorderStyleValue — a keyword string OR a `var(...)` string.
1616    // -------------------------------------------------------------------------
1617
1618    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    // -------------------------------------------------------------------------
1647    // EdgesInput — accepts either an edges keyword string ("top", "all",
1648    // "top|left", …) or a raw bit integer. Format-agnostic via deserialize_any.
1649    // Used by the BorderSpec map branch for the `edges` field.
1650    // -------------------------------------------------------------------------
1651
1652    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    // -------------------------------------------------------------------------
1709    // ColorInput — Color or null. The map branch reads `color` as this, so a
1710    // JSON null / YAML nil stays None without forcing Color to accept null.
1711    // -------------------------------------------------------------------------
1712
1713    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    // -------------------------------------------------------------------------
1748    // BorderSpec — a shorthand string OR a `{style, color, edges}` map. The
1749    // map branch drives a MapAccess walk and deserializes each value directly
1750    // into its typed leaf — no serde_json::Value is materialized, so the same
1751    // code path serves JSON, TOML tables, and YAML mappings.
1752    // -------------------------------------------------------------------------
1753
1754    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                            // Unknown keys: forward-compat, read & discard the value.
1791                            _ => {
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            // edges as a readable keyword string (None stays null).
1814            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}