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