Skip to main content

slt/
style.rs

1//! Visual styling primitives.
2//!
3//! Colors, themes, borders, padding, margin, constraints, alignment, and
4//! text modifiers. Every widget inherits these through [`Theme`] automatically.
5
6mod color;
7mod theme;
8#[cfg(feature = "serde")]
9mod theme_io;
10pub use color::{Color, ColorDepth, ColorParseError};
11pub use theme::{Spacing, SyntaxPalette, Theme, ThemeBuilder, ThemeColor};
12#[cfg(feature = "theme-watch")]
13pub use theme_io::ThemeWatcher;
14#[cfg(feature = "serde")]
15pub use theme_io::{ThemeFile, ThemeLoadError};
16
17/// Terminal size breakpoint for responsive layouts.
18///
19/// Based on the current terminal width. Use [`crate::Context::breakpoint`] to
20/// get the active breakpoint.
21#[non_exhaustive]
22#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
23pub enum Breakpoint {
24    /// Width < 40 columns (phone-sized)
25    Xs,
26    /// Width 40-79 columns (small terminal)
27    Sm,
28    /// Width 80-119 columns (standard terminal)
29    Md,
30    /// Width 120-159 columns (wide terminal)
31    Lg,
32    /// Width >= 160 columns (ultra-wide)
33    Xl,
34}
35
36/// Border style for containers.
37///
38/// Pass to `Context::bordered()` to draw a box around a container.
39/// Each variant uses a different set of Unicode box-drawing characters.
40#[non_exhaustive]
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
43pub enum Border {
44    /// Single-line box: `┌─┐│└─┘`
45    Single,
46    /// Double-line box: `╔═╗║╚═╝`
47    Double,
48    /// Rounded corners: `╭─╮│╰─╯`
49    Rounded,
50    /// Thick single-line box: `┏━┓┃┗━┛`
51    Thick,
52    /// Dashed border using light dash characters: ┄╌┄╌
53    Dashed,
54    /// Heavy dashed border: ┅╍┅╍
55    DashedThick,
56}
57
58/// Character set for a specific border style.
59///
60/// Returned by [`Border::chars`]. Contains the six box-drawing characters
61/// needed to render a complete border: four corners and two line segments.
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
63pub struct BorderChars {
64    /// Top-left corner character.
65    pub tl: char,
66    /// Top-right corner character.
67    pub tr: char,
68    /// Bottom-left corner character.
69    pub bl: char,
70    /// Bottom-right corner character.
71    pub br: char,
72    /// Horizontal line character.
73    pub h: char,
74    /// Vertical line character.
75    pub v: char,
76}
77
78/// Controls which sides of a border are visible.
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
80#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
81pub struct BorderSides {
82    /// Top border visible.
83    pub top: bool,
84    /// Right border visible.
85    pub right: bool,
86    /// Bottom border visible.
87    pub bottom: bool,
88    /// Left border visible.
89    pub left: bool,
90}
91
92impl BorderSides {
93    /// All four sides visible (default).
94    pub const fn all() -> Self {
95        Self {
96            top: true,
97            right: true,
98            bottom: true,
99            left: true,
100        }
101    }
102
103    /// No sides visible.
104    pub const fn none() -> Self {
105        Self {
106            top: false,
107            right: false,
108            bottom: false,
109            left: false,
110        }
111    }
112
113    /// Top and bottom sides only.
114    pub const fn horizontal() -> Self {
115        Self {
116            top: true,
117            right: false,
118            bottom: true,
119            left: false,
120        }
121    }
122
123    /// Left and right sides only.
124    pub const fn vertical() -> Self {
125        Self {
126            top: false,
127            right: true,
128            bottom: false,
129            left: true,
130        }
131    }
132
133    /// Returns true if top or bottom is visible.
134    pub fn has_horizontal(&self) -> bool {
135        self.top || self.bottom
136    }
137
138    /// Returns true if left or right is visible.
139    pub fn has_vertical(&self) -> bool {
140        self.left || self.right
141    }
142}
143
144impl Default for BorderSides {
145    fn default() -> Self {
146        Self::all()
147    }
148}
149
150impl Border {
151    /// Return the [`BorderChars`] for this border style.
152    pub const fn chars(self) -> BorderChars {
153        match self {
154            Self::Single => BorderChars {
155                tl: '┌',
156                tr: '┐',
157                bl: '└',
158                br: '┘',
159                h: '─',
160                v: '│',
161            },
162            Self::Double => BorderChars {
163                tl: '╔',
164                tr: '╗',
165                bl: '╚',
166                br: '╝',
167                h: '═',
168                v: '║',
169            },
170            Self::Rounded => BorderChars {
171                tl: '╭',
172                tr: '╮',
173                bl: '╰',
174                br: '╯',
175                h: '─',
176                v: '│',
177            },
178            Self::Thick => BorderChars {
179                tl: '┏',
180                tr: '┓',
181                bl: '┗',
182                br: '┛',
183                h: '━',
184                v: '┃',
185            },
186            Self::Dashed => BorderChars {
187                tl: '┌',
188                tr: '┐',
189                bl: '└',
190                br: '┘',
191                h: '┄',
192                v: '┆',
193            },
194            Self::DashedThick => BorderChars {
195                tl: '┏',
196                tr: '┓',
197                bl: '┗',
198                br: '┛',
199                h: '┅',
200                v: '┇',
201            },
202        }
203    }
204}
205
206/// Padding inside a container border.
207///
208/// Shrinks the content area inward from each edge. All values are in terminal
209/// columns/rows.
210#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
211#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
212pub struct Padding {
213    /// Padding on the top edge.
214    pub top: u32,
215    /// Padding on the right edge.
216    pub right: u32,
217    /// Padding on the bottom edge.
218    pub bottom: u32,
219    /// Padding on the left edge.
220    pub left: u32,
221}
222
223impl Padding {
224    /// Create uniform padding on all four sides.
225    pub const fn all(v: u32) -> Self {
226        Self::new(v, v, v, v)
227    }
228
229    /// Create padding with `x` on left/right and `y` on top/bottom.
230    pub const fn xy(x: u32, y: u32) -> Self {
231        Self::new(y, x, y, x)
232    }
233
234    /// Create padding with explicit values for each side.
235    pub const fn new(top: u32, right: u32, bottom: u32, left: u32) -> Self {
236        Self {
237            top,
238            right,
239            bottom,
240            left,
241        }
242    }
243
244    /// Total horizontal padding (`left + right`).
245    pub const fn horizontal(self) -> u32 {
246        self.left + self.right
247    }
248
249    /// Total vertical padding (`top + bottom`).
250    pub const fn vertical(self) -> u32 {
251        self.top + self.bottom
252    }
253}
254
255/// Margin outside a container.
256///
257/// Adds space around the outside of a container's border. All values are in
258/// terminal columns/rows.
259#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
260#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
261pub struct Margin {
262    /// Margin on the top edge.
263    pub top: u32,
264    /// Margin on the right edge.
265    pub right: u32,
266    /// Margin on the bottom edge.
267    pub bottom: u32,
268    /// Margin on the left edge.
269    pub left: u32,
270}
271
272impl Margin {
273    /// Create uniform margin on all four sides.
274    pub const fn all(v: u32) -> Self {
275        Self::new(v, v, v, v)
276    }
277
278    /// Create margin with `x` on left/right and `y` on top/bottom.
279    pub const fn xy(x: u32, y: u32) -> Self {
280        Self::new(y, x, y, x)
281    }
282
283    /// Create margin with explicit values for each side.
284    pub const fn new(top: u32, right: u32, bottom: u32, left: u32) -> Self {
285        Self {
286            top,
287            right,
288            bottom,
289            left,
290        }
291    }
292
293    /// Total horizontal margin (`left + right`).
294    pub const fn horizontal(self) -> u32 {
295        self.left + self.right
296    }
297
298    /// Total vertical margin (`top + bottom`).
299    pub const fn vertical(self) -> u32 {
300        self.top + self.bottom
301    }
302}
303
304/// Width specification for a flexbox item.
305///
306/// Replaces the previous trio of `Option`-typed fields (`min_width`,
307/// `max_width`, `width_pct`) with a single tagged enum. Resolution at
308/// layout time dispatches on the variant.
309///
310/// `Constraints::default()` produces [`WidthSpec::Auto`].
311///
312/// # Variant semantics
313///
314/// - [`Auto`](Self::Auto) — no width constraint; the element sizes from
315///   content and available space.
316/// - [`Fixed(n)`](Self::Fixed) — exact cell width. Equivalent to
317///   `MinMax { min: Some(n), max: Some(n) }`.
318/// - [`Pct(p)`](Self::Pct) — percentage of parent width (clamped to 0..=100).
319/// - [`Ratio(num, den)`](Self::Ratio) — exact integer fraction. For example
320///   `Ratio(1, 3)` produces `area / 3`. Floor division: `area = 80, num = 1,
321///   den = 3` → `26`. A `den` of `0` is treated as no constraint.
322/// - [`MinMax { min, max }`](Self::MinMax) — bounds on each side independently.
323///
324/// # Example
325///
326/// ```
327/// use slt::{Constraints, WidthSpec};
328///
329/// let c = Constraints::default().w_ratio(1, 3);
330/// assert_eq!(c.width, WidthSpec::Ratio(1, 3));
331/// ```
332#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
333#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
334#[non_exhaustive]
335pub enum WidthSpec {
336    /// Unconstrained — sizes from content and available space.
337    Auto,
338    /// Exact cell width.
339    Fixed(u32),
340    /// Percentage of parent width (`0..=100`).
341    Pct(u8),
342    /// Exact integer fraction of parent (numerator, denominator).
343    ///
344    /// `Ratio(1, 3)` produces `area / 3`. Floor division — for
345    /// `area = 80, num = 1, den = 3` → `26`. A `den` of `0` is treated as
346    /// no constraint.
347    Ratio(u16, u16),
348    /// Min and/or max bounds. Sentinels are used so that the variant fits
349    /// in 12 bytes (24 bytes total for the two-axis [`Constraints`] struct):
350    ///
351    /// - `min = 0` means "no minimum" (equivalent to `Option::None`); since
352    ///   a min of 0 is the same as no minimum, using `0` as the sentinel
353    ///   does not lose any expressible state.
354    /// - `max = u32::MAX` means "no maximum" (the natural `infinity`).
355    ///
356    /// Use the [`Constraints::min_w`] / [`Constraints::max_w`] /
357    /// [`Constraints::w_minmax`] builders to construct this variant
358    /// without thinking about sentinels.
359    MinMax {
360        /// Minimum width. `0` means unbounded below.
361        min: u32,
362        /// Maximum width. `u32::MAX` means unbounded above.
363        max: u32,
364    },
365}
366
367impl Default for WidthSpec {
368    #[inline]
369    fn default() -> Self {
370        Self::Auto
371    }
372}
373
374/// Height specification for a flexbox item.
375///
376/// Mirror of [`WidthSpec`] for the cross axis. See [`WidthSpec`] for full
377/// variant semantics, including the sentinel encoding of [`Self::MinMax`].
378#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
379#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
380#[non_exhaustive]
381pub enum HeightSpec {
382    /// Unconstrained — sizes from content and available space.
383    Auto,
384    /// Exact cell height.
385    Fixed(u32),
386    /// Percentage of parent height (`0..=100`).
387    Pct(u8),
388    /// Exact integer fraction of parent (numerator, denominator).
389    ///
390    /// `Ratio(1, 3)` produces `area / 3`. Floor division — for
391    /// `area = 80, num = 1, den = 3` → `26`. A `den` of `0` is treated as
392    /// no constraint.
393    Ratio(u16, u16),
394    /// Min and/or max bounds. Sentinels: `min = 0` and `max = u32::MAX`
395    /// represent "no bound". See [`WidthSpec::MinMax`] for full rationale.
396    MinMax {
397        /// Minimum height. `0` means unbounded below.
398        min: u32,
399        /// Maximum height. `u32::MAX` means unbounded above.
400        max: u32,
401    },
402}
403
404impl Default for HeightSpec {
405    #[inline]
406    fn default() -> Self {
407        Self::Auto
408    }
409}
410
411/// Size constraints for layout computation.
412///
413/// Holds a [`WidthSpec`] and a [`HeightSpec`] for the two axes. Use the
414/// builder methods on `Constraints` to set individual bounds in a fluent
415/// style; the builders pick the appropriate variant for you.
416///
417/// # Example
418///
419/// ```
420/// use slt::Constraints;
421///
422/// let c = Constraints::default().min_w(10).max_w(40);
423/// ```
424#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
425#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
426#[must_use = "configure constraints using the returned value"]
427pub struct Constraints {
428    /// Width specification.
429    pub width: WidthSpec,
430    /// Height specification.
431    pub height: HeightSpec,
432}
433
434/// Compile-time regression guard for `Constraints` size.
435///
436/// The unified `WidthSpec`/`HeightSpec` representation is required to fit
437/// in 24 bytes (12 + 12) — half the size of the v0.19 representation
438/// (36 bytes). Layout state stores this struct on every `LayoutNode`, so
439/// the cache footprint compounds.
440const _ASSERT_CONSTRAINTS_SIZE: () = assert!(
441    std::mem::size_of::<Constraints>() == 24,
442    "Constraints must be 24 bytes"
443);
444
445impl Constraints {
446    // ─── builder methods (preserved from v0.19, dispatch into enum) ───
447
448    /// Set the minimum width constraint.
449    ///
450    /// If the current variant is [`WidthSpec::MinMax`], updates only the
451    /// `min` side. Otherwise replaces the variant with `MinMax { min:
452    /// min_width, max: u32::MAX }`.
453    pub const fn min_w(mut self, min_width: u32) -> Self {
454        let max = match self.width {
455            WidthSpec::MinMax { max, .. } => max,
456            WidthSpec::Fixed(v) => v,
457            _ => u32::MAX,
458        };
459        self.width = WidthSpec::MinMax {
460            min: min_width,
461            max,
462        };
463        self
464    }
465
466    /// Set the maximum width constraint.
467    ///
468    /// If the current variant is [`WidthSpec::MinMax`], updates only the
469    /// `max` side. Otherwise replaces the variant with `MinMax { min: 0,
470    /// max: max_width }`.
471    pub const fn max_w(mut self, max_width: u32) -> Self {
472        let min = match self.width {
473            WidthSpec::MinMax { min, .. } => min,
474            WidthSpec::Fixed(v) => v,
475            _ => 0,
476        };
477        self.width = WidthSpec::MinMax {
478            min,
479            max: max_width,
480        };
481        self
482    }
483
484    /// Set the minimum height constraint.
485    ///
486    /// If the current variant is [`HeightSpec::MinMax`], updates only the
487    /// `min` side. Otherwise replaces the variant with `MinMax { min:
488    /// min_height, max: u32::MAX }`.
489    pub const fn min_h(mut self, min_height: u32) -> Self {
490        let max = match self.height {
491            HeightSpec::MinMax { max, .. } => max,
492            HeightSpec::Fixed(v) => v,
493            _ => u32::MAX,
494        };
495        self.height = HeightSpec::MinMax {
496            min: min_height,
497            max,
498        };
499        self
500    }
501
502    /// Set the maximum height constraint.
503    ///
504    /// If the current variant is [`HeightSpec::MinMax`], updates only the
505    /// `max` side. Otherwise replaces the variant with `MinMax { min: 0,
506    /// max: max_height }`.
507    pub const fn max_h(mut self, max_height: u32) -> Self {
508        let min = match self.height {
509            HeightSpec::MinMax { min, .. } => min,
510            HeightSpec::Fixed(v) => v,
511            _ => 0,
512        };
513        self.height = HeightSpec::MinMax {
514            min,
515            max: max_height,
516        };
517        self
518    }
519
520    /// Set min and max width together.
521    ///
522    /// Equivalent to chaining `min_w(min)` and `max_w(max)` but in a single
523    /// call, replacing the variant with `WidthSpec::MinMax`.
524    pub const fn w_minmax(mut self, min: u32, max: u32) -> Self {
525        self.width = WidthSpec::MinMax { min, max };
526        self
527    }
528
529    /// Set min and max height together.
530    pub const fn h_minmax(mut self, min: u32, max: u32) -> Self {
531        self.height = HeightSpec::MinMax { min, max };
532        self
533    }
534
535    /// Set a fixed width (replaces any existing width spec).
536    pub const fn w(mut self, width: u32) -> Self {
537        self.width = WidthSpec::Fixed(width);
538        self
539    }
540
541    /// Set a fixed height (replaces any existing height spec).
542    pub const fn h(mut self, height: u32) -> Self {
543        self.height = HeightSpec::Fixed(height);
544        self
545    }
546
547    /// Set width as a percentage (`0..=100`) of the parent container.
548    pub const fn w_pct(mut self, pct: u8) -> Self {
549        self.width = WidthSpec::Pct(pct);
550        self
551    }
552
553    /// Set height as a percentage (`0..=100`) of the parent container.
554    pub const fn h_pct(mut self, pct: u8) -> Self {
555        self.height = HeightSpec::Pct(pct);
556        self
557    }
558
559    /// Set width as an exact integer fraction of the parent (numerator, denominator).
560    ///
561    /// `w_ratio(1, 3)` produces `area / 3` — floor division. For `area = 80,
562    /// num = 1, den = 3` → `26`.
563    pub const fn w_ratio(mut self, num: u16, den: u16) -> Self {
564        self.width = WidthSpec::Ratio(num, den);
565        self
566    }
567
568    /// Set height as an exact integer fraction of the parent (numerator, denominator).
569    ///
570    /// `h_ratio(1, 3)` produces `area / 3` — floor division.
571    pub const fn h_ratio(mut self, num: u16, den: u16) -> Self {
572        self.height = HeightSpec::Ratio(num, den);
573        self
574    }
575
576    // ─── derived accessors used by layout & widget code ────────────────
577
578    /// Minimum width derived from the current [`WidthSpec`].
579    ///
580    /// Returns `Some(n)` for [`WidthSpec::Fixed`] (both min and max are `n`)
581    /// and for [`WidthSpec::MinMax`] when the `min` side is non-zero.
582    /// Returns `None` for [`WidthSpec::Auto`], [`WidthSpec::Pct`],
583    /// [`WidthSpec::Ratio`], and for `MinMax { min: 0, .. }` (sentinel for
584    /// "no minimum").
585    pub const fn min_width(&self) -> Option<u32> {
586        match self.width {
587            WidthSpec::Fixed(v) => Some(v),
588            WidthSpec::MinMax { min, .. } if min > 0 => Some(min),
589            _ => None,
590        }
591    }
592
593    /// Maximum width derived from the current [`WidthSpec`].
594    ///
595    /// Returns `Some(n)` for [`WidthSpec::Fixed`] and for
596    /// [`WidthSpec::MinMax`] when the `max` side is not the sentinel
597    /// `u32::MAX`. Returns `None` otherwise.
598    pub const fn max_width(&self) -> Option<u32> {
599        match self.width {
600            WidthSpec::Fixed(v) => Some(v),
601            WidthSpec::MinMax { max, .. } if max < u32::MAX => Some(max),
602            _ => None,
603        }
604    }
605
606    /// Minimum height derived from the current [`HeightSpec`].
607    ///
608    /// Mirror of [`min_width`](Self::min_width) for the cross axis.
609    pub const fn min_height(&self) -> Option<u32> {
610        match self.height {
611            HeightSpec::Fixed(v) => Some(v),
612            HeightSpec::MinMax { min, .. } if min > 0 => Some(min),
613            _ => None,
614        }
615    }
616
617    /// Maximum height derived from the current [`HeightSpec`].
618    ///
619    /// Mirror of [`max_width`](Self::max_width) for the cross axis.
620    pub const fn max_height(&self) -> Option<u32> {
621        match self.height {
622            HeightSpec::Fixed(v) => Some(v),
623            HeightSpec::MinMax { max, .. } if max < u32::MAX => Some(max),
624            _ => None,
625        }
626    }
627
628    /// Width percentage if the variant is [`WidthSpec::Pct`].
629    pub const fn width_pct(&self) -> Option<u8> {
630        match self.width {
631            WidthSpec::Pct(p) => Some(p),
632            _ => None,
633        }
634    }
635
636    /// Height percentage if the variant is [`HeightSpec::Pct`].
637    pub const fn height_pct(&self) -> Option<u8> {
638        match self.height {
639            HeightSpec::Pct(p) => Some(p),
640            _ => None,
641        }
642    }
643
644    // ─── imperative setters ─────────────────────────────────────────────
645    //
646    // These mutate `&mut Constraints` in-place. They exist alongside the
647    // owning builder methods (`min_w`, `max_w`, …) for call sites that hold
648    // a mutable borrow to a `Constraints` field embedded in a larger struct
649    // — for those the builder's `mut self -> Self` shape would force a
650    // `*c = c.min_w(v)` deref-assign. The setters keep that ergonomic.
651    //
652    // # Compatibility
653    //
654    // Public for downstream callers that adopted these from v0.19. New code
655    // that owns a `Constraints` value should prefer the chainable builders
656    // (`Constraints::default().min_w(10).max_w(40)`).
657
658    /// Set the minimum width as `Option<u32>`.
659    ///
660    /// Promotes the variant to [`WidthSpec::MinMax`] preserving any existing
661    /// `max` side. Passing `None` clears the minimum (sets it to `0`); if the
662    /// resulting `MinMax` has no effective bounds (`min == 0` and
663    /// `max == u32::MAX`) the variant collapses back to [`WidthSpec::Auto`].
664    ///
665    /// Prefer [`Constraints::min_w`] when you own the value; this setter is
666    /// for in-place mutation through `&mut Constraints`.
667    pub fn set_min_width(&mut self, value: Option<u32>) {
668        let max = match self.width {
669            WidthSpec::MinMax { max, .. } => max,
670            WidthSpec::Fixed(v) => v,
671            _ => u32::MAX,
672        };
673        let min = value.unwrap_or(0);
674        self.width = if min == 0 && max == u32::MAX {
675            WidthSpec::Auto
676        } else {
677            WidthSpec::MinMax { min, max }
678        };
679    }
680
681    /// Set the maximum width as `Option<u32>`.
682    pub fn set_max_width(&mut self, value: Option<u32>) {
683        let min = match self.width {
684            WidthSpec::MinMax { min, .. } => min,
685            WidthSpec::Fixed(v) => v,
686            _ => 0,
687        };
688        let max = value.unwrap_or(u32::MAX);
689        self.width = if min == 0 && max == u32::MAX {
690            WidthSpec::Auto
691        } else {
692            WidthSpec::MinMax { min, max }
693        };
694    }
695
696    /// Set the minimum height as `Option<u32>`.
697    pub fn set_min_height(&mut self, value: Option<u32>) {
698        let max = match self.height {
699            HeightSpec::MinMax { max, .. } => max,
700            HeightSpec::Fixed(v) => v,
701            _ => u32::MAX,
702        };
703        let min = value.unwrap_or(0);
704        self.height = if min == 0 && max == u32::MAX {
705            HeightSpec::Auto
706        } else {
707            HeightSpec::MinMax { min, max }
708        };
709    }
710
711    /// Set the maximum height as `Option<u32>`.
712    pub fn set_max_height(&mut self, value: Option<u32>) {
713        let min = match self.height {
714            HeightSpec::MinMax { min, .. } => min,
715            HeightSpec::Fixed(v) => v,
716            _ => 0,
717        };
718        let max = value.unwrap_or(u32::MAX);
719        self.height = if min == 0 && max == u32::MAX {
720            HeightSpec::Auto
721        } else {
722            HeightSpec::MinMax { min, max }
723        };
724    }
725
726    /// Set the width percentage as `Option<u8>`.
727    pub fn set_width_pct(&mut self, value: Option<u8>) {
728        self.width = match value {
729            Some(p) => WidthSpec::Pct(p),
730            None => WidthSpec::Auto,
731        };
732    }
733
734    /// Set the height percentage as `Option<u8>`.
735    pub fn set_height_pct(&mut self, value: Option<u8>) {
736        self.height = match value {
737            Some(p) => HeightSpec::Pct(p),
738            None => HeightSpec::Auto,
739        };
740    }
741}
742
743/// Cross-axis alignment within a container.
744///
745/// Controls how children are positioned along the axis perpendicular to the
746/// container's main axis. For a `row()`, this is vertical alignment; for a
747/// `col()`, this is horizontal alignment.
748#[non_exhaustive]
749#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
750#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
751pub enum Align {
752    /// Align children to the start of the cross axis (default).
753    ///
754    /// Unlike CSS `flex-start`, this variant fills the full cross-axis
755    /// (equivalent to CSS `stretch`). Children are sized to the container's
756    /// cross-axis dimension. Use [`Align::Center`] or [`Align::End`] to
757    /// size children by their natural dimensions instead.
758    #[default]
759    Start,
760    /// Center children on the cross axis.
761    Center,
762    /// Align children to the end of the cross axis.
763    End,
764}
765
766/// Main-axis content distribution within a container.
767///
768/// Controls how children are distributed along the main axis. For a `row()`,
769/// this is horizontal distribution; for a `col()`, this is vertical.
770///
771/// When children have `grow > 0`, they consume remaining space before justify
772/// distribution applies. Justify modes only affect the leftover space after
773/// flex-grow allocation.
774#[non_exhaustive]
775#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
776#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
777pub enum Justify {
778    /// Pack children at the start (default). Uses `gap` for spacing.
779    #[default]
780    Start,
781    /// Center children along the main axis with `gap` spacing.
782    Center,
783    /// Pack children at the end with `gap` spacing.
784    End,
785    /// First child at start, last at end, equal space between.
786    SpaceBetween,
787    /// Equal space around each child (half-size space at edges).
788    SpaceAround,
789    /// Equal space between all children and at both edges.
790    SpaceEvenly,
791}
792
793/// Text modifier bitflags stored as a `u8`.
794///
795/// Combine modifiers with `|` or [`Modifiers::insert`]. Check membership with
796/// [`Modifiers::contains`].
797#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
798#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
799#[cfg_attr(feature = "serde", serde(transparent))]
800pub struct Modifiers(pub u8);
801
802impl Modifiers {
803    /// No modifiers set.
804    pub const NONE: Self = Self(0);
805    /// Enable bold text.
806    pub const BOLD: Self = Self(1 << 0);
807    /// Enable dimmed/faint text.
808    pub const DIM: Self = Self(1 << 1);
809    /// Enable italic text.
810    pub const ITALIC: Self = Self(1 << 2);
811    /// Enable underlined text.
812    pub const UNDERLINE: Self = Self(1 << 3);
813    /// Enable reversed foreground/background colors.
814    pub const REVERSED: Self = Self(1 << 4);
815    /// Enable strikethrough text.
816    pub const STRIKETHROUGH: Self = Self(1 << 5);
817    /// Enable slow blinking text (SGR 5).
818    pub const BLINK: Self = Self(1 << 6);
819    /// Enable an overline above the text (SGR 53).
820    pub const OVERLINE: Self = Self(1 << 7);
821
822    /// Returns `true` if all bits in `other` are set in `self`.
823    #[inline]
824    pub fn contains(self, other: Self) -> bool {
825        (self.0 & other.0) == other.0
826    }
827
828    /// Set all bits from `other` into `self`.
829    #[inline]
830    pub fn insert(&mut self, other: Self) {
831        self.0 |= other.0;
832    }
833
834    /// Unset all bits from `other`.
835    ///
836    /// # Example
837    ///
838    /// ```
839    /// use slt::Modifiers;
840    ///
841    /// let mut m = Modifiers::BOLD | Modifiers::ITALIC;
842    /// m.remove(Modifiers::BOLD);
843    /// assert!(!m.contains(Modifiers::BOLD));
844    /// assert!(m.contains(Modifiers::ITALIC));
845    /// ```
846    #[inline]
847    pub fn remove(&mut self, other: Self) {
848        self.0 &= !other.0;
849    }
850
851    /// Returns `true` if no modifiers are set.
852    #[inline]
853    pub fn is_empty(self) -> bool {
854        self.0 == 0
855    }
856}
857
858impl std::ops::BitOr for Modifiers {
859    type Output = Self;
860    #[inline]
861    fn bitor(self, rhs: Self) -> Self {
862        Self(self.0 | rhs.0)
863    }
864}
865
866impl std::ops::BitOrAssign for Modifiers {
867    #[inline]
868    fn bitor_assign(&mut self, rhs: Self) {
869        self.0 |= rhs.0;
870    }
871}
872
873/// Underline rendering style, emitted as the `CSI 4:Nm` subparameter.
874///
875/// `Straight` is the plain `SGR 4` default. The other variants require a
876/// terminal that supports the Kitty/VTE underline-style extension
877/// (Kitty, WezTerm, foot, recent VTE-based terminals); terminals without
878/// support fall back to a plain underline.
879///
880/// # Example
881///
882/// ```
883/// use slt::{Color, Style, UnderlineStyle};
884///
885/// // A red curly "error" underline, like a spell-checker squiggle.
886/// let err = Style::new()
887///     .underline_style(UnderlineStyle::Curly)
888///     .underline_color(Color::Red);
889/// assert_eq!(err.underline_style, UnderlineStyle::Curly);
890/// ```
891#[non_exhaustive]
892#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
893#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
894pub enum UnderlineStyle {
895    /// Plain straight underline (`CSI 4:1m`, the `SGR 4` default).
896    #[default]
897    Straight,
898    /// Double underline (`CSI 4:2m`).
899    Double,
900    /// Curly / "squiggly" underline (`CSI 4:3m`), commonly used for errors.
901    Curly,
902    /// Dotted underline (`CSI 4:4m`).
903    Dotted,
904    /// Dashed underline (`CSI 4:5m`).
905    Dashed,
906}
907
908/// Visual style for a terminal cell (foreground, background, modifiers).
909///
910/// Styles are applied to text via the builder methods on `Context` widget
911/// calls (e.g., `.bold()`, `.fg(Color::Cyan)`). All fields are optional;
912/// `None` means "inherit from the terminal default."
913///
914/// # Example
915///
916/// ```
917/// use slt::{Style, Color};
918///
919/// let style = Style::new().fg(Color::Cyan).bold();
920/// ```
921#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
922#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
923#[must_use = "build and pass the returned Style value"]
924pub struct Style {
925    /// Foreground color, or `None` to use the terminal default.
926    pub fg: Option<Color>,
927    /// Background color, or `None` to use the terminal default.
928    pub bg: Option<Color>,
929    /// Text modifiers (bold, italic, underline, etc.).
930    pub modifiers: Modifiers,
931    /// Underline color, or `None` to use the same color as the foreground.
932    ///
933    /// Emitted as `SGR 58` when set and `SGR 59` (reset to foreground) when
934    /// cleared. Requires a terminal that supports separate underline colors.
935    pub underline_color: Option<Color>,
936    /// Underline rendering style (straight, curly, dotted, etc.).
937    ///
938    /// Defaults to [`UnderlineStyle::Straight`]. Non-straight styles are
939    /// emitted as `CSI 4:Nm` and require terminal support.
940    pub underline_style: UnderlineStyle,
941}
942
943impl Style {
944    /// Create a new style with no color or modifiers set.
945    pub const fn new() -> Self {
946        Self {
947            fg: None,
948            bg: None,
949            modifiers: Modifiers::NONE,
950            underline_color: None,
951            underline_style: UnderlineStyle::Straight,
952        }
953    }
954
955    /// Set the foreground color.
956    pub const fn fg(mut self, color: Color) -> Self {
957        self.fg = Some(color);
958        self
959    }
960
961    /// Set the background color.
962    pub const fn bg(mut self, color: Color) -> Self {
963        self.bg = Some(color);
964        self
965    }
966
967    /// Add the bold modifier.
968    pub fn bold(mut self) -> Self {
969        self.modifiers |= Modifiers::BOLD;
970        self
971    }
972
973    /// Add the dim modifier.
974    pub fn dim(mut self) -> Self {
975        self.modifiers |= Modifiers::DIM;
976        self
977    }
978
979    /// Add the italic modifier.
980    pub fn italic(mut self) -> Self {
981        self.modifiers |= Modifiers::ITALIC;
982        self
983    }
984
985    /// Add the underline modifier.
986    pub fn underline(mut self) -> Self {
987        self.modifiers |= Modifiers::UNDERLINE;
988        self
989    }
990
991    /// Add the reversed (inverted colors) modifier.
992    pub fn reversed(mut self) -> Self {
993        self.modifiers |= Modifiers::REVERSED;
994        self
995    }
996
997    /// Add the strikethrough modifier.
998    pub fn strikethrough(mut self) -> Self {
999        self.modifiers |= Modifiers::STRIKETHROUGH;
1000        self
1001    }
1002
1003    /// Add the slow-blink modifier (SGR 5).
1004    pub fn blink(mut self) -> Self {
1005        self.modifiers |= Modifiers::BLINK;
1006        self
1007    }
1008
1009    /// Add the overline modifier (SGR 53).
1010    pub fn overline(mut self) -> Self {
1011        self.modifiers |= Modifiers::OVERLINE;
1012        self
1013    }
1014
1015    /// Set a separate underline color.
1016    ///
1017    /// Setting this implies the [`Modifiers::UNDERLINE`] bit so the
1018    /// underline is actually rendered. Emitted as `SGR 58`.
1019    ///
1020    /// # Example
1021    ///
1022    /// ```
1023    /// use slt::{Color, Modifiers, Style};
1024    ///
1025    /// let s = Style::new().underline_color(Color::Red);
1026    /// assert!(s.modifiers.contains(Modifiers::UNDERLINE));
1027    /// ```
1028    pub const fn underline_color(mut self, color: Color) -> Self {
1029        self.underline_color = Some(color);
1030        self.modifiers = Modifiers(self.modifiers.0 | Modifiers::UNDERLINE.0);
1031        self
1032    }
1033
1034    /// Set the underline rendering style (straight, curly, dotted, etc.).
1035    ///
1036    /// Setting a style implies the [`Modifiers::UNDERLINE`] bit so the
1037    /// underline is actually rendered. Emitted as `CSI 4:Nm`.
1038    ///
1039    /// # Example
1040    ///
1041    /// ```
1042    /// use slt::{Modifiers, Style, UnderlineStyle};
1043    ///
1044    /// let s = Style::new().underline_style(UnderlineStyle::Curly);
1045    /// assert!(s.modifiers.contains(Modifiers::UNDERLINE));
1046    /// ```
1047    pub const fn underline_style(mut self, style: UnderlineStyle) -> Self {
1048        self.underline_style = style;
1049        self.modifiers = Modifiers(self.modifiers.0 | Modifiers::UNDERLINE.0);
1050        self
1051    }
1052}
1053
1054/// Reusable container style recipe.
1055///
1056/// Define once, apply anywhere with [`crate::ContainerBuilder::apply`]. All fields
1057/// are optional — only set fields override the builder's current values.
1058/// Styles compose: apply multiple recipes in sequence, last write wins.
1059///
1060/// # Example
1061///
1062/// ```ignore
1063/// use slt::{ContainerStyle, Border, Color};
1064///
1065/// const CARD: ContainerStyle = ContainerStyle::new()
1066///     .border(Border::Rounded)
1067///     .p(1)
1068///     .bg(Color::Indexed(236));
1069///
1070/// const DANGER: ContainerStyle = ContainerStyle::new()
1071///     .bg(Color::Red);
1072///
1073/// // Apply one or compose multiple:
1074/// ui.container().apply(&CARD).col(|ui| { ... });
1075/// ui.container().apply(&CARD).apply(&DANGER).col(|ui| { ... });
1076/// ```
1077#[derive(Debug, Clone, Copy, Default)]
1078pub struct ContainerStyle {
1079    /// Border style for the container.
1080    pub border: Option<Border>,
1081    /// Which sides of the border are visible.
1082    pub border_sides: Option<BorderSides>,
1083    /// Style (color and modifiers) for the border.
1084    pub border_style: Option<Style>,
1085    /// Background color.
1086    pub bg: Option<Color>,
1087    /// Foreground (text) color.
1088    pub text_color: Option<Color>,
1089    /// Background color in dark mode.
1090    pub dark_bg: Option<Color>,
1091    /// Border style in dark mode.
1092    pub dark_border_style: Option<Style>,
1093    /// Padding inside the container.
1094    pub padding: Option<Padding>,
1095    /// Margin outside the container.
1096    pub margin: Option<Margin>,
1097    /// Gap between children (both row and column).
1098    pub gap: Option<u32>,
1099    /// Gap between rows.
1100    pub row_gap: Option<u32>,
1101    /// Gap between columns.
1102    pub col_gap: Option<u32>,
1103    /// Flex grow factor.
1104    pub grow: Option<u16>,
1105    /// Cross-axis alignment.
1106    pub align: Option<Align>,
1107    /// Self alignment (overrides parent align).
1108    pub align_self: Option<Align>,
1109    /// Main-axis content distribution.
1110    pub justify: Option<Justify>,
1111    /// Fixed width.
1112    pub w: Option<u32>,
1113    /// Fixed height.
1114    pub h: Option<u32>,
1115    /// Minimum width.
1116    pub min_w: Option<u32>,
1117    /// Maximum width.
1118    pub max_w: Option<u32>,
1119    /// Minimum height.
1120    pub min_h: Option<u32>,
1121    /// Maximum height.
1122    pub max_h: Option<u32>,
1123    /// Width as percentage of parent.
1124    pub w_pct: Option<u8>,
1125    /// Height as percentage of parent.
1126    pub h_pct: Option<u8>,
1127    /// Theme-aware background color. Takes precedence over [`Self::bg`] when set.
1128    pub theme_bg: Option<ThemeColor>,
1129    /// Theme-aware text color. Takes precedence over [`Self::text_color`] when set.
1130    pub theme_text_color: Option<ThemeColor>,
1131    /// Theme-aware border foreground color. Takes precedence over
1132    /// [`Self::border_style`]'s foreground when set.
1133    pub theme_border_fg: Option<ThemeColor>,
1134    /// Base style to inherit from. Fields in the base are applied first,
1135    /// then overridden by any `Some` fields in this style.
1136    ///
1137    /// Use [`ContainerStyle::extending`] to create a style that inherits.
1138    pub extends: Option<&'static ContainerStyle>,
1139}
1140
1141impl ContainerStyle {
1142    /// Create an empty container style with no overrides.
1143    pub const fn new() -> Self {
1144        Self {
1145            border: None,
1146            border_sides: None,
1147            border_style: None,
1148            bg: None,
1149            text_color: None,
1150            dark_bg: None,
1151            dark_border_style: None,
1152            padding: None,
1153            margin: None,
1154            gap: None,
1155            row_gap: None,
1156            col_gap: None,
1157            grow: None,
1158            align: None,
1159            align_self: None,
1160            justify: None,
1161            w: None,
1162            h: None,
1163            min_w: None,
1164            max_w: None,
1165            min_h: None,
1166            max_h: None,
1167            w_pct: None,
1168            h_pct: None,
1169            theme_bg: None,
1170            theme_text_color: None,
1171            theme_border_fg: None,
1172            extends: None,
1173        }
1174    }
1175
1176    /// Create a style that inherits all fields from a base style.
1177    ///
1178    /// Only the fields you set on the returned style will override the base.
1179    /// The base must be a `&'static ContainerStyle` (typically a `const`).
1180    ///
1181    /// # Example
1182    ///
1183    /// ```ignore
1184    /// use slt::{ContainerStyle, Border, ThemeColor};
1185    ///
1186    /// const BUTTON: ContainerStyle = ContainerStyle::new()
1187    ///     .border(Border::Rounded)
1188    ///     .p(1);
1189    ///
1190    /// const BUTTON_DANGER: ContainerStyle = ContainerStyle::extending(&BUTTON)
1191    ///     .theme_bg(ThemeColor::Error);
1192    /// ```
1193    pub const fn extending(base: &'static ContainerStyle) -> Self {
1194        let mut s = Self::new();
1195        s.extends = Some(base);
1196        s
1197    }
1198
1199    /// Set the border style.
1200    pub const fn border(mut self, border: Border) -> Self {
1201        self.border = Some(border);
1202        self
1203    }
1204
1205    /// Set which border sides to render.
1206    pub const fn border_sides(mut self, sides: BorderSides) -> Self {
1207        self.border_sides = Some(sides);
1208        self
1209    }
1210
1211    /// Set the background color.
1212    pub const fn bg(mut self, color: Color) -> Self {
1213        self.bg = Some(color);
1214        self
1215    }
1216
1217    /// Set default text color inherited by child text widgets.
1218    pub const fn text_color(mut self, color: Color) -> Self {
1219        self.text_color = Some(color);
1220        self
1221    }
1222
1223    /// Set the dark-mode background color.
1224    pub const fn dark_bg(mut self, color: Color) -> Self {
1225        self.dark_bg = Some(color);
1226        self
1227    }
1228
1229    /// Set uniform padding on all sides.
1230    pub const fn p(mut self, value: u32) -> Self {
1231        self.padding = Some(Padding {
1232            top: value,
1233            bottom: value,
1234            left: value,
1235            right: value,
1236        });
1237        self
1238    }
1239
1240    /// Set horizontal padding.
1241    pub const fn px(mut self, value: u32) -> Self {
1242        let p = match self.padding {
1243            Some(p) => Padding {
1244                left: value,
1245                right: value,
1246                ..p
1247            },
1248            None => Padding {
1249                top: 0,
1250                bottom: 0,
1251                left: value,
1252                right: value,
1253            },
1254        };
1255        self.padding = Some(p);
1256        self
1257    }
1258
1259    /// Set vertical padding.
1260    pub const fn py(mut self, value: u32) -> Self {
1261        let p = match self.padding {
1262            Some(p) => Padding {
1263                top: value,
1264                bottom: value,
1265                ..p
1266            },
1267            None => Padding {
1268                top: value,
1269                bottom: value,
1270                left: 0,
1271                right: 0,
1272            },
1273        };
1274        self.padding = Some(p);
1275        self
1276    }
1277
1278    /// Set uniform margin on all sides.
1279    pub const fn m(mut self, value: u32) -> Self {
1280        self.margin = Some(Margin {
1281            top: value,
1282            bottom: value,
1283            left: value,
1284            right: value,
1285        });
1286        self
1287    }
1288
1289    /// Set horizontal margin (left + right). Top and bottom are preserved if
1290    /// margin was previously set, otherwise default to 0.
1291    ///
1292    /// ```
1293    /// use slt::ContainerStyle;
1294    /// let s = ContainerStyle::new().mx(2).py(1);
1295    /// assert_eq!(s.margin.unwrap().left, 2);
1296    /// assert_eq!(s.margin.unwrap().right, 2);
1297    /// assert_eq!(s.margin.unwrap().top, 0);
1298    /// ```
1299    pub const fn mx(mut self, value: u32) -> Self {
1300        let m = match self.margin {
1301            Some(m) => Margin {
1302                left: value,
1303                right: value,
1304                ..m
1305            },
1306            None => Margin {
1307                top: 0,
1308                bottom: 0,
1309                left: value,
1310                right: value,
1311            },
1312        };
1313        self.margin = Some(m);
1314        self
1315    }
1316
1317    /// Set vertical margin (top + bottom). Left and right are preserved if
1318    /// margin was previously set, otherwise default to 0.
1319    pub const fn my(mut self, value: u32) -> Self {
1320        let m = match self.margin {
1321            Some(m) => Margin {
1322                top: value,
1323                bottom: value,
1324                ..m
1325            },
1326            None => Margin {
1327                top: value,
1328                bottom: value,
1329                left: 0,
1330                right: 0,
1331            },
1332        };
1333        self.margin = Some(m);
1334        self
1335    }
1336
1337    /// Set the gap between children.
1338    pub const fn gap(mut self, value: u32) -> Self {
1339        self.gap = Some(value);
1340        self
1341    }
1342
1343    /// Set row gap for column layouts.
1344    pub const fn row_gap(mut self, value: u32) -> Self {
1345        self.row_gap = Some(value);
1346        self
1347    }
1348
1349    /// Set column gap for row layouts.
1350    pub const fn col_gap(mut self, value: u32) -> Self {
1351        self.col_gap = Some(value);
1352        self
1353    }
1354
1355    /// Set the flex-grow factor.
1356    pub const fn grow(mut self, value: u16) -> Self {
1357        self.grow = Some(value);
1358        self
1359    }
1360
1361    /// Set fixed width.
1362    pub const fn w(mut self, value: u32) -> Self {
1363        self.w = Some(value);
1364        self
1365    }
1366
1367    /// Set fixed height.
1368    pub const fn h(mut self, value: u32) -> Self {
1369        self.h = Some(value);
1370        self
1371    }
1372
1373    /// Set minimum width.
1374    pub const fn min_w(mut self, value: u32) -> Self {
1375        self.min_w = Some(value);
1376        self
1377    }
1378
1379    /// Set maximum width.
1380    pub const fn max_w(mut self, value: u32) -> Self {
1381        self.max_w = Some(value);
1382        self
1383    }
1384
1385    /// Set cross-axis alignment.
1386    pub const fn align(mut self, value: Align) -> Self {
1387        self.align = Some(value);
1388        self
1389    }
1390
1391    /// Set per-child cross-axis alignment override.
1392    pub const fn align_self(mut self, value: Align) -> Self {
1393        self.align_self = Some(value);
1394        self
1395    }
1396
1397    /// Set main-axis justification.
1398    pub const fn justify(mut self, value: Justify) -> Self {
1399        self.justify = Some(value);
1400        self
1401    }
1402
1403    /// Set minimum height.
1404    pub const fn min_h(mut self, value: u32) -> Self {
1405        self.min_h = Some(value);
1406        self
1407    }
1408
1409    /// Set maximum height.
1410    pub const fn max_h(mut self, value: u32) -> Self {
1411        self.max_h = Some(value);
1412        self
1413    }
1414
1415    /// Set width as percentage of parent (1-100).
1416    pub const fn w_pct(mut self, value: u8) -> Self {
1417        self.w_pct = Some(value);
1418        self
1419    }
1420
1421    /// Set height as percentage of parent (1-100).
1422    pub const fn h_pct(mut self, value: u8) -> Self {
1423        self.h_pct = Some(value);
1424        self
1425    }
1426
1427    /// Set a theme-aware background color that resolves at apply time.
1428    ///
1429    /// Takes precedence over [`Self::bg`] when set. The color is resolved
1430    /// against the active theme when [`crate::ContainerBuilder::apply`] is called.
1431    pub const fn theme_bg(mut self, color: ThemeColor) -> Self {
1432        self.theme_bg = Some(color);
1433        self
1434    }
1435
1436    /// Set a theme-aware text color that resolves at apply time.
1437    ///
1438    /// Takes precedence over [`Self::text_color`] when set.
1439    pub const fn theme_text_color(mut self, color: ThemeColor) -> Self {
1440        self.theme_text_color = Some(color);
1441        self
1442    }
1443
1444    /// Set a theme-aware border foreground color that resolves at apply time.
1445    ///
1446    /// Takes precedence over [`Self::border_style`]'s foreground when set.
1447    pub const fn theme_border_fg(mut self, color: ThemeColor) -> Self {
1448        self.theme_border_fg = Some(color);
1449        self
1450    }
1451}
1452
1453#[derive(Debug, Clone, Copy, Default)]
1454#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1455#[cfg_attr(feature = "serde", serde(default))]
1456/// Per-widget color overrides that fall back to the active theme.
1457///
1458/// Literal [`Color`] fields and [`ThemeColor`] fields can be set independently.
1459/// Resolution order: `theme_*` field > literal field > theme default.
1460pub struct WidgetColors {
1461    /// Foreground color override.
1462    pub fg: Option<Color>,
1463    /// Background color override.
1464    pub bg: Option<Color>,
1465    /// Border color override.
1466    pub border: Option<Color>,
1467    /// Accent color override.
1468    pub accent: Option<Color>,
1469    /// Theme-aware foreground (takes precedence over [`Self::fg`]).
1470    pub theme_fg: Option<ThemeColor>,
1471    /// Theme-aware background (takes precedence over [`Self::bg`]).
1472    pub theme_bg: Option<ThemeColor>,
1473    /// Theme-aware border (takes precedence over [`Self::border`]).
1474    pub theme_border: Option<ThemeColor>,
1475    /// Theme-aware accent (takes precedence over [`Self::accent`]).
1476    pub theme_accent: Option<ThemeColor>,
1477}
1478
1479impl WidgetColors {
1480    /// Create a new WidgetColors with all fields set to None (theme defaults).
1481    pub const fn new() -> Self {
1482        Self {
1483            fg: None,
1484            bg: None,
1485            border: None,
1486            accent: None,
1487            theme_fg: None,
1488            theme_bg: None,
1489            theme_border: None,
1490            theme_accent: None,
1491        }
1492    }
1493
1494    /// Set the foreground color override.
1495    pub const fn fg(mut self, color: Color) -> Self {
1496        self.fg = Some(color);
1497        self
1498    }
1499
1500    /// Set the background color override.
1501    pub const fn bg(mut self, color: Color) -> Self {
1502        self.bg = Some(color);
1503        self
1504    }
1505
1506    /// Set the border color override.
1507    pub const fn border(mut self, color: Color) -> Self {
1508        self.border = Some(color);
1509        self
1510    }
1511
1512    /// Set the accent color override.
1513    pub const fn accent(mut self, color: Color) -> Self {
1514        self.accent = Some(color);
1515        self
1516    }
1517
1518    /// Set a theme-aware foreground color.
1519    pub const fn theme_fg(mut self, color: ThemeColor) -> Self {
1520        self.theme_fg = Some(color);
1521        self
1522    }
1523
1524    /// Set a theme-aware background color.
1525    pub const fn theme_bg(mut self, color: ThemeColor) -> Self {
1526        self.theme_bg = Some(color);
1527        self
1528    }
1529
1530    /// Set a theme-aware border color.
1531    pub const fn theme_border(mut self, color: ThemeColor) -> Self {
1532        self.theme_border = Some(color);
1533        self
1534    }
1535
1536    /// Set a theme-aware accent color.
1537    pub const fn theme_accent(mut self, color: ThemeColor) -> Self {
1538        self.theme_accent = Some(color);
1539        self
1540    }
1541
1542    /// Resolve the foreground color, preferring theme color, then literal, then fallback.
1543    pub fn resolve_fg(&self, theme: &Theme, fallback: Color) -> Color {
1544        self.theme_fg
1545            .map(|tc| theme.resolve(tc))
1546            .or(self.fg)
1547            .unwrap_or(fallback)
1548    }
1549
1550    /// Resolve the background color, preferring theme color, then literal, then fallback.
1551    pub fn resolve_bg(&self, theme: &Theme, fallback: Color) -> Color {
1552        self.theme_bg
1553            .map(|tc| theme.resolve(tc))
1554            .or(self.bg)
1555            .unwrap_or(fallback)
1556    }
1557
1558    /// Resolve the border color, preferring theme color, then literal, then fallback.
1559    pub fn resolve_border(&self, theme: &Theme, fallback: Color) -> Color {
1560        self.theme_border
1561            .map(|tc| theme.resolve(tc))
1562            .or(self.border)
1563            .unwrap_or(fallback)
1564    }
1565
1566    /// Resolve the accent color, preferring theme color, then literal, then fallback.
1567    pub fn resolve_accent(&self, theme: &Theme, fallback: Color) -> Color {
1568        self.theme_accent
1569            .map(|tc| theme.resolve(tc))
1570            .or(self.accent)
1571            .unwrap_or(fallback)
1572    }
1573}
1574
1575/// Default widget colors applied to all instances of a widget type.
1576///
1577/// Set via [`crate::RunConfig::widget_theme`]. Each widget type reads its
1578/// defaults from this struct, then falls back to the active [`Theme`].
1579/// Per-callsite `_colored()` overrides still take precedence.
1580///
1581/// # Example
1582///
1583/// ```
1584/// use slt::{WidgetTheme, WidgetColors, Color};
1585///
1586/// let wt = WidgetTheme::new()
1587///     .button(WidgetColors::new().fg(Color::Cyan));
1588/// ```
1589#[derive(Debug, Clone, Copy, Default)]
1590#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1591#[cfg_attr(feature = "serde", serde(default))]
1592pub struct WidgetTheme {
1593    /// Default colors for buttons.
1594    pub button: WidgetColors,
1595    /// Default colors for tables.
1596    pub table: WidgetColors,
1597    /// Default colors for lists.
1598    pub list: WidgetColors,
1599    /// Default colors for tabs.
1600    pub tabs: WidgetColors,
1601    /// Default colors for select dropdowns.
1602    pub select: WidgetColors,
1603    /// Default colors for radio groups.
1604    pub radio: WidgetColors,
1605    /// Default colors for checkboxes.
1606    pub checkbox: WidgetColors,
1607    /// Default colors for toggles.
1608    pub toggle: WidgetColors,
1609    /// Default colors for text inputs.
1610    pub text_input: WidgetColors,
1611    /// Default colors for color pickers.
1612    pub color_picker: WidgetColors,
1613}
1614
1615impl WidgetTheme {
1616    /// Create a WidgetTheme with all defaults (no overrides).
1617    pub const fn new() -> Self {
1618        Self {
1619            button: WidgetColors::new(),
1620            table: WidgetColors::new(),
1621            list: WidgetColors::new(),
1622            tabs: WidgetColors::new(),
1623            select: WidgetColors::new(),
1624            radio: WidgetColors::new(),
1625            checkbox: WidgetColors::new(),
1626            toggle: WidgetColors::new(),
1627            text_input: WidgetColors::new(),
1628            color_picker: WidgetColors::new(),
1629        }
1630    }
1631
1632    /// Set default button colors.
1633    pub const fn button(mut self, colors: WidgetColors) -> Self {
1634        self.button = colors;
1635        self
1636    }
1637
1638    /// Set default table colors.
1639    pub const fn table(mut self, colors: WidgetColors) -> Self {
1640        self.table = colors;
1641        self
1642    }
1643
1644    /// Set default list colors.
1645    pub const fn list(mut self, colors: WidgetColors) -> Self {
1646        self.list = colors;
1647        self
1648    }
1649
1650    /// Set default tabs colors.
1651    pub const fn tabs(mut self, colors: WidgetColors) -> Self {
1652        self.tabs = colors;
1653        self
1654    }
1655
1656    /// Set default select colors.
1657    pub const fn select(mut self, colors: WidgetColors) -> Self {
1658        self.select = colors;
1659        self
1660    }
1661
1662    /// Set default radio colors.
1663    pub const fn radio(mut self, colors: WidgetColors) -> Self {
1664        self.radio = colors;
1665        self
1666    }
1667
1668    /// Set default checkbox colors.
1669    pub const fn checkbox(mut self, colors: WidgetColors) -> Self {
1670        self.checkbox = colors;
1671        self
1672    }
1673
1674    /// Set default toggle colors.
1675    pub const fn toggle(mut self, colors: WidgetColors) -> Self {
1676        self.toggle = colors;
1677        self
1678    }
1679
1680    /// Set default text input colors.
1681    pub const fn text_input(mut self, colors: WidgetColors) -> Self {
1682        self.text_input = colors;
1683        self
1684    }
1685
1686    /// Set default color picker colors.
1687    pub const fn color_picker(mut self, colors: WidgetColors) -> Self {
1688        self.color_picker = colors;
1689        self
1690    }
1691}
1692
1693#[cfg(test)]
1694mod tests {
1695    use super::*;
1696
1697    #[test]
1698    fn style_new_is_default() {
1699        let style = Style::new();
1700        assert_eq!(style.fg, None);
1701        assert_eq!(style.bg, None);
1702        assert_eq!(style.modifiers, Modifiers::NONE);
1703        assert_eq!(style, Style::default());
1704    }
1705
1706    #[test]
1707    fn style_bold_and_fg_set_expected_fields() {
1708        let style = Style::new().bold().fg(Color::Red);
1709        assert_eq!(style.fg, Some(Color::Red));
1710        assert_eq!(style.bg, None);
1711        assert!(style.modifiers.contains(Modifiers::BOLD));
1712    }
1713
1714    #[test]
1715    fn style_multiple_modifiers_accumulate() {
1716        let style = Style::new().italic().underline().dim();
1717        assert!(style.modifiers.contains(Modifiers::ITALIC));
1718        assert!(style.modifiers.contains(Modifiers::UNDERLINE));
1719        assert!(style.modifiers.contains(Modifiers::DIM));
1720    }
1721
1722    #[test]
1723    fn modifiers_blink_overline_occupy_high_bits() {
1724        assert_eq!(Modifiers::BLINK.0, 1 << 6);
1725        assert_eq!(Modifiers::OVERLINE.0, 1 << 7);
1726        // No collision with any existing bit.
1727        let existing = [
1728            Modifiers::BOLD,
1729            Modifiers::DIM,
1730            Modifiers::ITALIC,
1731            Modifiers::UNDERLINE,
1732            Modifiers::REVERSED,
1733            Modifiers::STRIKETHROUGH,
1734        ];
1735        for m in existing {
1736            assert_eq!(m.0 & Modifiers::BLINK.0, 0);
1737            assert_eq!(m.0 & Modifiers::OVERLINE.0, 0);
1738        }
1739        assert_eq!(Modifiers::BLINK.0 & Modifiers::OVERLINE.0, 0);
1740    }
1741
1742    #[test]
1743    fn style_blink_and_overline_accumulate() {
1744        let style = Style::new().blink().overline();
1745        assert!(style.modifiers.contains(Modifiers::BLINK));
1746        assert!(style.modifiers.contains(Modifiers::OVERLINE));
1747    }
1748
1749    #[test]
1750    fn underline_style_default_is_straight() {
1751        assert_eq!(UnderlineStyle::default(), UnderlineStyle::Straight);
1752        let s = Style::default();
1753        assert_eq!(s.underline_style, UnderlineStyle::Straight);
1754        assert_eq!(s.underline_color, None);
1755    }
1756
1757    #[test]
1758    fn underline_color_sets_field_and_implies_underline() {
1759        let s = Style::new().underline_color(Color::Red);
1760        assert_eq!(s.underline_color, Some(Color::Red));
1761        assert!(s.modifiers.contains(Modifiers::UNDERLINE));
1762    }
1763
1764    #[test]
1765    fn underline_style_sets_field_and_implies_underline() {
1766        let s = Style::new().underline_style(UnderlineStyle::Curly);
1767        assert_eq!(s.underline_style, UnderlineStyle::Curly);
1768        assert!(s.modifiers.contains(Modifiers::UNDERLINE));
1769    }
1770
1771    #[test]
1772    fn style_repeated_fg_overrides_previous_color() {
1773        let style = Style::new().fg(Color::Blue).fg(Color::Green);
1774        assert_eq!(style.fg, Some(Color::Green));
1775    }
1776
1777    #[test]
1778    fn style_repeated_bg_overrides_previous_color() {
1779        let style = Style::new().bg(Color::Blue).bg(Color::Green);
1780        assert_eq!(style.bg, Some(Color::Green));
1781    }
1782
1783    #[test]
1784    fn style_override_preserves_existing_modifiers() {
1785        let style = Style::new().bold().fg(Color::Red).fg(Color::Yellow);
1786        assert_eq!(style.fg, Some(Color::Yellow));
1787        assert!(style.modifiers.contains(Modifiers::BOLD));
1788    }
1789
1790    #[test]
1791    fn padding_all_sets_all_sides() {
1792        let p = Padding::all(3);
1793        assert_eq!(p.top, 3);
1794        assert_eq!(p.right, 3);
1795        assert_eq!(p.bottom, 3);
1796        assert_eq!(p.left, 3);
1797    }
1798
1799    #[test]
1800    fn padding_xy_sets_axis_values() {
1801        let p = Padding::xy(4, 2);
1802        assert_eq!(p.top, 2);
1803        assert_eq!(p.bottom, 2);
1804        assert_eq!(p.left, 4);
1805        assert_eq!(p.right, 4);
1806    }
1807
1808    #[test]
1809    fn padding_new_and_totals_are_correct() {
1810        let p = Padding::new(1, 2, 3, 4);
1811        assert_eq!(p.top, 1);
1812        assert_eq!(p.right, 2);
1813        assert_eq!(p.bottom, 3);
1814        assert_eq!(p.left, 4);
1815        assert_eq!(p.horizontal(), 6);
1816        assert_eq!(p.vertical(), 4);
1817    }
1818
1819    #[test]
1820    fn margin_all_and_xy_are_correct() {
1821        let all = Margin::all(5);
1822        assert_eq!(all, Margin::new(5, 5, 5, 5));
1823
1824        let xy = Margin::xy(7, 1);
1825        assert_eq!(xy.top, 1);
1826        assert_eq!(xy.bottom, 1);
1827        assert_eq!(xy.left, 7);
1828        assert_eq!(xy.right, 7);
1829    }
1830
1831    #[test]
1832    fn margin_new_and_totals_are_correct() {
1833        let m = Margin::new(2, 4, 6, 8);
1834        assert_eq!(m.horizontal(), 12);
1835        assert_eq!(m.vertical(), 8);
1836    }
1837
1838    #[test]
1839    fn constraints_min_max_builder_sets_values() {
1840        let c = Constraints::default()
1841            .min_w(10)
1842            .max_w(40)
1843            .min_h(5)
1844            .max_h(20);
1845        assert_eq!(c.min_width(), Some(10));
1846        assert_eq!(c.max_width(), Some(40));
1847        assert_eq!(c.min_height(), Some(5));
1848        assert_eq!(c.max_height(), Some(20));
1849        assert_eq!(c.width, WidthSpec::MinMax { min: 10, max: 40 });
1850    }
1851
1852    #[test]
1853    fn constraints_percentage_builder_sets_values() {
1854        let c = Constraints::default().w_pct(50).h_pct(80);
1855        assert_eq!(c.width_pct(), Some(50));
1856        assert_eq!(c.height_pct(), Some(80));
1857        assert_eq!(c.width, WidthSpec::Pct(50));
1858        assert_eq!(c.height, HeightSpec::Pct(80));
1859    }
1860
1861    #[test]
1862    fn constraints_default_is_auto() {
1863        let c = Constraints::default();
1864        assert_eq!(c.width, WidthSpec::Auto);
1865        assert_eq!(c.height, HeightSpec::Auto);
1866    }
1867
1868    #[test]
1869    fn constraints_fixed_w_h() {
1870        let c = Constraints::default().w(20).h(10);
1871        assert_eq!(c.width, WidthSpec::Fixed(20));
1872        assert_eq!(c.height, HeightSpec::Fixed(10));
1873        assert_eq!(c.min_width(), Some(20));
1874        assert_eq!(c.max_width(), Some(20));
1875    }
1876
1877    #[test]
1878    fn constraints_size_24_bytes() {
1879        assert_eq!(std::mem::size_of::<Constraints>(), 24);
1880    }
1881
1882    #[test]
1883    fn constraints_set_min_width_promotes_to_minmax() {
1884        let mut c = Constraints::default();
1885        c.set_min_width(Some(10));
1886        assert_eq!(
1887            c.width,
1888            WidthSpec::MinMax {
1889                min: 10,
1890                max: u32::MAX,
1891            }
1892        );
1893        c.set_max_width(Some(40));
1894        assert_eq!(c.width, WidthSpec::MinMax { min: 10, max: 40 });
1895    }
1896
1897    #[test]
1898    fn constraints_w_ratio_builder() {
1899        let c = Constraints::default().w_ratio(1, 3);
1900        assert_eq!(c.width, WidthSpec::Ratio(1, 3));
1901    }
1902
1903    #[test]
1904    fn border_sides_all_has_both_axes() {
1905        let sides = BorderSides::all();
1906        assert!(sides.top && sides.right && sides.bottom && sides.left);
1907        assert!(sides.has_horizontal());
1908        assert!(sides.has_vertical());
1909    }
1910
1911    #[test]
1912    fn border_sides_none_has_no_axes() {
1913        let sides = BorderSides::none();
1914        assert!(!sides.top && !sides.right && !sides.bottom && !sides.left);
1915        assert!(!sides.has_horizontal());
1916        assert!(!sides.has_vertical());
1917    }
1918
1919    #[test]
1920    fn border_sides_horizontal_only() {
1921        let sides = BorderSides::horizontal();
1922        assert!(sides.top);
1923        assert!(sides.bottom);
1924        assert!(!sides.left);
1925        assert!(!sides.right);
1926        assert!(sides.has_horizontal());
1927        assert!(!sides.has_vertical());
1928    }
1929
1930    #[test]
1931    fn border_sides_vertical_only() {
1932        let sides = BorderSides::vertical();
1933        assert!(!sides.top);
1934        assert!(!sides.bottom);
1935        assert!(sides.left);
1936        assert!(sides.right);
1937        assert!(!sides.has_horizontal());
1938        assert!(sides.has_vertical());
1939    }
1940
1941    #[test]
1942    fn container_style_new_is_empty() {
1943        let s = ContainerStyle::new();
1944        assert_eq!(s.border, None);
1945        assert_eq!(s.bg, None);
1946        assert_eq!(s.padding, None);
1947        assert_eq!(s.margin, None);
1948        assert_eq!(s.gap, None);
1949        assert_eq!(s.align, None);
1950        assert_eq!(s.justify, None);
1951    }
1952
1953    #[test]
1954    fn container_style_const_construction_and_fields() {
1955        const CARD: ContainerStyle = ContainerStyle::new()
1956            .border(Border::Rounded)
1957            .border_sides(BorderSides::horizontal())
1958            .p(2)
1959            .m(1)
1960            .gap(3)
1961            .align(Align::Center)
1962            .justify(Justify::SpaceBetween)
1963            .w(60)
1964            .h(20);
1965
1966        assert_eq!(CARD.border, Some(Border::Rounded));
1967        assert_eq!(CARD.border_sides, Some(BorderSides::horizontal()));
1968        assert_eq!(CARD.padding, Some(Padding::all(2)));
1969        assert_eq!(CARD.margin, Some(Margin::all(1)));
1970        assert_eq!(CARD.gap, Some(3));
1971        assert_eq!(CARD.align, Some(Align::Center));
1972        assert_eq!(CARD.justify, Some(Justify::SpaceBetween));
1973        assert_eq!(CARD.w, Some(60));
1974        assert_eq!(CARD.h, Some(20));
1975    }
1976
1977    #[test]
1978    fn widget_colors_new_is_empty() {
1979        let colors = WidgetColors::new();
1980        assert_eq!(colors.fg, None);
1981        assert_eq!(colors.bg, None);
1982        assert_eq!(colors.border, None);
1983        assert_eq!(colors.accent, None);
1984
1985        let defaults = WidgetColors::default();
1986        assert_eq!(defaults.fg, None);
1987        assert_eq!(defaults.bg, None);
1988        assert_eq!(defaults.border, None);
1989        assert_eq!(defaults.accent, None);
1990    }
1991
1992    #[test]
1993    fn widget_colors_builder_sets_all_fields() {
1994        let colors = WidgetColors::new()
1995            .fg(Color::White)
1996            .bg(Color::Black)
1997            .border(Color::Cyan)
1998            .accent(Color::Yellow);
1999
2000        assert_eq!(colors.fg, Some(Color::White));
2001        assert_eq!(colors.bg, Some(Color::Black));
2002        assert_eq!(colors.border, Some(Color::Cyan));
2003        assert_eq!(colors.accent, Some(Color::Yellow));
2004    }
2005
2006    #[test]
2007    fn align_default_is_start() {
2008        assert_eq!(Align::default(), Align::Start);
2009    }
2010
2011    #[test]
2012    fn justify_default_is_start() {
2013        assert_eq!(Justify::default(), Justify::Start);
2014    }
2015
2016    #[test]
2017    fn align_and_justify_variants_are_distinct() {
2018        assert_ne!(Align::Start, Align::Center);
2019        assert_ne!(Align::Center, Align::End);
2020
2021        assert_ne!(Justify::Start, Justify::Center);
2022        assert_ne!(Justify::Center, Justify::End);
2023        assert_ne!(Justify::SpaceBetween, Justify::SpaceAround);
2024        assert_ne!(Justify::SpaceAround, Justify::SpaceEvenly);
2025    }
2026}