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