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::{Theme, ThemeBuilder};
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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
16pub enum Breakpoint {
17    /// Width < 40 columns (phone-sized)
18    Xs,
19    /// Width 40-79 columns (small terminal)
20    Sm,
21    /// Width 80-119 columns (standard terminal)
22    Md,
23    /// Width 120-159 columns (wide terminal)
24    Lg,
25    /// Width >= 160 columns (ultra-wide)
26    Xl,
27}
28
29/// Border style for containers.
30///
31/// Pass to `Context::bordered()` to draw a box around a container.
32/// Each variant uses a different set of Unicode box-drawing characters.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
35pub enum Border {
36    /// Single-line box: `┌─┐│└─┘`
37    Single,
38    /// Double-line box: `╔═╗║╚═╝`
39    Double,
40    /// Rounded corners: `╭─╮│╰─╯`
41    Rounded,
42    /// Thick single-line box: `┏━┓┃┗━┛`
43    Thick,
44    /// Dashed border using light dash characters: ┄╌┄╌
45    Dashed,
46    /// Heavy dashed border: ┅╍┅╍
47    DashedThick,
48}
49
50/// Character set for a specific border style.
51///
52/// Returned by [`Border::chars`]. Contains the six box-drawing characters
53/// needed to render a complete border: four corners and two line segments.
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
55pub struct BorderChars {
56    /// Top-left corner character.
57    pub tl: char,
58    /// Top-right corner character.
59    pub tr: char,
60    /// Bottom-left corner character.
61    pub bl: char,
62    /// Bottom-right corner character.
63    pub br: char,
64    /// Horizontal line character.
65    pub h: char,
66    /// Vertical line character.
67    pub v: char,
68}
69
70/// Controls which sides of a border are visible.
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
72#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
73pub struct BorderSides {
74    /// Top border visible.
75    pub top: bool,
76    /// Right border visible.
77    pub right: bool,
78    /// Bottom border visible.
79    pub bottom: bool,
80    /// Left border visible.
81    pub left: bool,
82}
83
84impl BorderSides {
85    /// All four sides visible (default).
86    pub const fn all() -> Self {
87        Self {
88            top: true,
89            right: true,
90            bottom: true,
91            left: true,
92        }
93    }
94
95    /// No sides visible.
96    pub const fn none() -> Self {
97        Self {
98            top: false,
99            right: false,
100            bottom: false,
101            left: false,
102        }
103    }
104
105    /// Top and bottom sides only.
106    pub const fn horizontal() -> Self {
107        Self {
108            top: true,
109            right: false,
110            bottom: true,
111            left: false,
112        }
113    }
114
115    /// Left and right sides only.
116    pub const fn vertical() -> Self {
117        Self {
118            top: false,
119            right: true,
120            bottom: false,
121            left: true,
122        }
123    }
124
125    /// Returns true if top or bottom is visible.
126    pub fn has_horizontal(&self) -> bool {
127        self.top || self.bottom
128    }
129
130    /// Returns true if left or right is visible.
131    pub fn has_vertical(&self) -> bool {
132        self.left || self.right
133    }
134}
135
136impl Default for BorderSides {
137    fn default() -> Self {
138        Self::all()
139    }
140}
141
142impl Border {
143    /// Return the [`BorderChars`] for this border style.
144    pub const fn chars(self) -> BorderChars {
145        match self {
146            Self::Single => BorderChars {
147                tl: '┌',
148                tr: '┐',
149                bl: '└',
150                br: '┘',
151                h: '─',
152                v: '│',
153            },
154            Self::Double => BorderChars {
155                tl: '╔',
156                tr: '╗',
157                bl: '╚',
158                br: '╝',
159                h: '═',
160                v: '║',
161            },
162            Self::Rounded => BorderChars {
163                tl: '╭',
164                tr: '╮',
165                bl: '╰',
166                br: '╯',
167                h: '─',
168                v: '│',
169            },
170            Self::Thick => BorderChars {
171                tl: '┏',
172                tr: '┓',
173                bl: '┗',
174                br: '┛',
175                h: '━',
176                v: '┃',
177            },
178            Self::Dashed => BorderChars {
179                tl: '┌',
180                tr: '┐',
181                bl: '└',
182                br: '┘',
183                h: '┄',
184                v: '┆',
185            },
186            Self::DashedThick => BorderChars {
187                tl: '┏',
188                tr: '┓',
189                bl: '┗',
190                br: '┛',
191                h: '┅',
192                v: '┇',
193            },
194        }
195    }
196}
197
198/// Padding inside a container border.
199///
200/// Shrinks the content area inward from each edge. All values are in terminal
201/// columns/rows.
202#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
203#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
204pub struct Padding {
205    /// Padding on the top edge.
206    pub top: u32,
207    /// Padding on the right edge.
208    pub right: u32,
209    /// Padding on the bottom edge.
210    pub bottom: u32,
211    /// Padding on the left edge.
212    pub left: u32,
213}
214
215impl Padding {
216    /// Create uniform padding on all four sides.
217    pub const fn all(v: u32) -> Self {
218        Self::new(v, v, v, v)
219    }
220
221    /// Create padding with `x` on left/right and `y` on top/bottom.
222    pub const fn xy(x: u32, y: u32) -> Self {
223        Self::new(y, x, y, x)
224    }
225
226    /// Create padding with explicit values for each side.
227    pub const fn new(top: u32, right: u32, bottom: u32, left: u32) -> Self {
228        Self {
229            top,
230            right,
231            bottom,
232            left,
233        }
234    }
235
236    /// Total horizontal padding (`left + right`).
237    pub const fn horizontal(self) -> u32 {
238        self.left + self.right
239    }
240
241    /// Total vertical padding (`top + bottom`).
242    pub const fn vertical(self) -> u32 {
243        self.top + self.bottom
244    }
245}
246
247/// Margin outside a container.
248///
249/// Adds space around the outside of a container's border. All values are in
250/// terminal columns/rows.
251#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
252#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
253pub struct Margin {
254    /// Margin on the top edge.
255    pub top: u32,
256    /// Margin on the right edge.
257    pub right: u32,
258    /// Margin on the bottom edge.
259    pub bottom: u32,
260    /// Margin on the left edge.
261    pub left: u32,
262}
263
264impl Margin {
265    /// Create uniform margin on all four sides.
266    pub const fn all(v: u32) -> Self {
267        Self::new(v, v, v, v)
268    }
269
270    /// Create margin with `x` on left/right and `y` on top/bottom.
271    pub const fn xy(x: u32, y: u32) -> Self {
272        Self::new(y, x, y, x)
273    }
274
275    /// Create margin with explicit values for each side.
276    pub const fn new(top: u32, right: u32, bottom: u32, left: u32) -> Self {
277        Self {
278            top,
279            right,
280            bottom,
281            left,
282        }
283    }
284
285    /// Total horizontal margin (`left + right`).
286    pub const fn horizontal(self) -> u32 {
287        self.left + self.right
288    }
289
290    /// Total vertical margin (`top + bottom`).
291    pub const fn vertical(self) -> u32 {
292        self.top + self.bottom
293    }
294}
295
296/// Size constraints for layout computation.
297///
298/// All fields are optional. Unset constraints are unconstrained. Use the
299/// builder methods to set individual bounds in a fluent style.
300///
301/// # Example
302///
303/// ```
304/// use slt::Constraints;
305///
306/// let c = Constraints::default().min_w(10).max_w(40);
307/// ```
308#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
309#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
310#[must_use = "configure constraints using the returned value"]
311pub struct Constraints {
312    /// Minimum width in terminal columns, if any.
313    pub min_width: Option<u32>,
314    /// Maximum width in terminal columns, if any.
315    pub max_width: Option<u32>,
316    /// Minimum height in terminal rows, if any.
317    pub min_height: Option<u32>,
318    /// Maximum height in terminal rows, if any.
319    pub max_height: Option<u32>,
320    /// Width as a percentage (1-100) of the parent container.
321    pub width_pct: Option<u8>,
322    /// Height as a percentage (1-100) of the parent container.
323    pub height_pct: Option<u8>,
324}
325
326impl Constraints {
327    /// Set the minimum width constraint.
328    pub const fn min_w(mut self, min_width: u32) -> Self {
329        self.min_width = Some(min_width);
330        self
331    }
332
333    /// Set the maximum width constraint.
334    pub const fn max_w(mut self, max_width: u32) -> Self {
335        self.max_width = Some(max_width);
336        self
337    }
338
339    /// Set the minimum height constraint.
340    pub const fn min_h(mut self, min_height: u32) -> Self {
341        self.min_height = Some(min_height);
342        self
343    }
344
345    /// Set the maximum height constraint.
346    pub const fn max_h(mut self, max_height: u32) -> Self {
347        self.max_height = Some(max_height);
348        self
349    }
350
351    /// Set width as a percentage (1-100) of the parent container.
352    pub const fn w_pct(mut self, pct: u8) -> Self {
353        self.width_pct = Some(pct);
354        self
355    }
356
357    /// Set height as a percentage (1-100) of the parent container.
358    pub const fn h_pct(mut self, pct: u8) -> Self {
359        self.height_pct = Some(pct);
360        self
361    }
362}
363
364/// Cross-axis alignment within a container.
365///
366/// Controls how children are positioned along the axis perpendicular to the
367/// container's main axis. For a `row()`, this is vertical alignment; for a
368/// `col()`, this is horizontal alignment.
369#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
370#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
371pub enum Align {
372    /// Align children to the start of the cross axis (default).
373    #[default]
374    Start,
375    /// Center children on the cross axis.
376    Center,
377    /// Align children to the end of the cross axis.
378    End,
379}
380
381/// Main-axis content distribution within a container.
382///
383/// Controls how children are distributed along the main axis. For a `row()`,
384/// this is horizontal distribution; for a `col()`, this is vertical.
385///
386/// When children have `grow > 0`, they consume remaining space before justify
387/// distribution applies. Justify modes only affect the leftover space after
388/// flex-grow allocation.
389#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
390#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
391pub enum Justify {
392    /// Pack children at the start (default). Uses `gap` for spacing.
393    #[default]
394    Start,
395    /// Center children along the main axis with `gap` spacing.
396    Center,
397    /// Pack children at the end with `gap` spacing.
398    End,
399    /// First child at start, last at end, equal space between.
400    SpaceBetween,
401    /// Equal space around each child (half-size space at edges).
402    SpaceAround,
403    /// Equal space between all children and at both edges.
404    SpaceEvenly,
405}
406
407/// Text modifier bitflags stored as a `u8`.
408///
409/// Combine modifiers with `|` or [`Modifiers::insert`]. Check membership with
410/// [`Modifiers::contains`].
411#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
412#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
413#[cfg_attr(feature = "serde", serde(transparent))]
414pub struct Modifiers(pub u8);
415
416impl Modifiers {
417    /// No modifiers set.
418    pub const NONE: Self = Self(0);
419    /// Enable bold text.
420    pub const BOLD: Self = Self(1 << 0);
421    /// Enable dimmed/faint text.
422    pub const DIM: Self = Self(1 << 1);
423    /// Enable italic text.
424    pub const ITALIC: Self = Self(1 << 2);
425    /// Enable underlined text.
426    pub const UNDERLINE: Self = Self(1 << 3);
427    /// Enable reversed foreground/background colors.
428    pub const REVERSED: Self = Self(1 << 4);
429    /// Enable strikethrough text.
430    pub const STRIKETHROUGH: Self = Self(1 << 5);
431
432    /// Returns `true` if all bits in `other` are set in `self`.
433    #[inline]
434    pub fn contains(self, other: Self) -> bool {
435        (self.0 & other.0) == other.0
436    }
437
438    /// Set all bits from `other` into `self`.
439    #[inline]
440    pub fn insert(&mut self, other: Self) {
441        self.0 |= other.0;
442    }
443
444    /// Returns `true` if no modifiers are set.
445    #[inline]
446    pub fn is_empty(self) -> bool {
447        self.0 == 0
448    }
449}
450
451impl std::ops::BitOr for Modifiers {
452    type Output = Self;
453    #[inline]
454    fn bitor(self, rhs: Self) -> Self {
455        Self(self.0 | rhs.0)
456    }
457}
458
459impl std::ops::BitOrAssign for Modifiers {
460    #[inline]
461    fn bitor_assign(&mut self, rhs: Self) {
462        self.0 |= rhs.0;
463    }
464}
465
466/// Visual style for a terminal cell (foreground, background, modifiers).
467///
468/// Styles are applied to text via the builder methods on `Context` widget
469/// calls (e.g., `.bold()`, `.fg(Color::Cyan)`). All fields are optional;
470/// `None` means "inherit from the terminal default."
471///
472/// # Example
473///
474/// ```
475/// use slt::{Style, Color};
476///
477/// let style = Style::new().fg(Color::Cyan).bold();
478/// ```
479#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
480#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
481#[must_use = "build and pass the returned Style value"]
482pub struct Style {
483    /// Foreground color, or `None` to use the terminal default.
484    pub fg: Option<Color>,
485    /// Background color, or `None` to use the terminal default.
486    pub bg: Option<Color>,
487    /// Text modifiers (bold, italic, underline, etc.).
488    pub modifiers: Modifiers,
489}
490
491impl Style {
492    /// Create a new style with no color or modifiers set.
493    pub const fn new() -> Self {
494        Self {
495            fg: None,
496            bg: None,
497            modifiers: Modifiers::NONE,
498        }
499    }
500
501    /// Set the foreground color.
502    pub const fn fg(mut self, color: Color) -> Self {
503        self.fg = Some(color);
504        self
505    }
506
507    /// Set the background color.
508    pub const fn bg(mut self, color: Color) -> Self {
509        self.bg = Some(color);
510        self
511    }
512
513    /// Add the bold modifier.
514    pub fn bold(mut self) -> Self {
515        self.modifiers |= Modifiers::BOLD;
516        self
517    }
518
519    /// Add the dim modifier.
520    pub fn dim(mut self) -> Self {
521        self.modifiers |= Modifiers::DIM;
522        self
523    }
524
525    /// Add the italic modifier.
526    pub fn italic(mut self) -> Self {
527        self.modifiers |= Modifiers::ITALIC;
528        self
529    }
530
531    /// Add the underline modifier.
532    pub fn underline(mut self) -> Self {
533        self.modifiers |= Modifiers::UNDERLINE;
534        self
535    }
536
537    /// Add the reversed (inverted colors) modifier.
538    pub fn reversed(mut self) -> Self {
539        self.modifiers |= Modifiers::REVERSED;
540        self
541    }
542
543    /// Add the strikethrough modifier.
544    pub fn strikethrough(mut self) -> Self {
545        self.modifiers |= Modifiers::STRIKETHROUGH;
546        self
547    }
548}
549
550/// Reusable container style recipe.
551///
552/// Define once, apply anywhere with [`crate::ContainerBuilder::apply`]. All fields
553/// are optional — only set fields override the builder's current values.
554/// Styles compose: apply multiple recipes in sequence, last write wins.
555///
556/// # Example
557///
558/// ```ignore
559/// use slt::{ContainerStyle, Border, Color};
560///
561/// const CARD: ContainerStyle = ContainerStyle::new()
562///     .border(Border::Rounded)
563///     .p(1)
564///     .bg(Color::Indexed(236));
565///
566/// const DANGER: ContainerStyle = ContainerStyle::new()
567///     .bg(Color::Red);
568///
569/// // Apply one or compose multiple:
570/// ui.container().apply(&CARD).col(|ui| { ... });
571/// ui.container().apply(&CARD).apply(&DANGER).col(|ui| { ... });
572/// ```
573#[derive(Debug, Clone, Copy, Default)]
574pub struct ContainerStyle {
575    /// Border style for the container.
576    pub border: Option<Border>,
577    /// Which sides of the border are visible.
578    pub border_sides: Option<BorderSides>,
579    /// Style (color and modifiers) for the border.
580    pub border_style: Option<Style>,
581    /// Background color.
582    pub bg: Option<Color>,
583    /// Foreground (text) color.
584    pub text_color: Option<Color>,
585    /// Background color in dark mode.
586    pub dark_bg: Option<Color>,
587    /// Border style in dark mode.
588    pub dark_border_style: Option<Style>,
589    /// Padding inside the container.
590    pub padding: Option<Padding>,
591    /// Margin outside the container.
592    pub margin: Option<Margin>,
593    /// Gap between children (both row and column).
594    pub gap: Option<u32>,
595    /// Gap between rows.
596    pub row_gap: Option<u32>,
597    /// Gap between columns.
598    pub col_gap: Option<u32>,
599    /// Flex grow factor.
600    pub grow: Option<u16>,
601    /// Cross-axis alignment.
602    pub align: Option<Align>,
603    /// Self alignment (overrides parent align).
604    pub align_self: Option<Align>,
605    /// Main-axis content distribution.
606    pub justify: Option<Justify>,
607    /// Fixed width.
608    pub w: Option<u32>,
609    /// Fixed height.
610    pub h: Option<u32>,
611    /// Minimum width.
612    pub min_w: Option<u32>,
613    /// Maximum width.
614    pub max_w: Option<u32>,
615    /// Minimum height.
616    pub min_h: Option<u32>,
617    /// Maximum height.
618    pub max_h: Option<u32>,
619    /// Width as percentage of parent.
620    pub w_pct: Option<u8>,
621    /// Height as percentage of parent.
622    pub h_pct: Option<u8>,
623}
624
625impl ContainerStyle {
626    /// Create an empty container style with no overrides.
627    pub const fn new() -> Self {
628        Self {
629            border: None,
630            border_sides: None,
631            border_style: None,
632            bg: None,
633            text_color: None,
634            dark_bg: None,
635            dark_border_style: None,
636            padding: None,
637            margin: None,
638            gap: None,
639            row_gap: None,
640            col_gap: None,
641            grow: None,
642            align: None,
643            align_self: None,
644            justify: None,
645            w: None,
646            h: None,
647            min_w: None,
648            max_w: None,
649            min_h: None,
650            max_h: None,
651            w_pct: None,
652            h_pct: None,
653        }
654    }
655
656    /// Set the border style.
657    pub const fn border(mut self, border: Border) -> Self {
658        self.border = Some(border);
659        self
660    }
661
662    /// Set which border sides to render.
663    pub const fn border_sides(mut self, sides: BorderSides) -> Self {
664        self.border_sides = Some(sides);
665        self
666    }
667
668    /// Set the background color.
669    pub const fn bg(mut self, color: Color) -> Self {
670        self.bg = Some(color);
671        self
672    }
673
674    /// Set default text color inherited by child text widgets.
675    pub const fn text_color(mut self, color: Color) -> Self {
676        self.text_color = Some(color);
677        self
678    }
679
680    /// Set the dark-mode background color.
681    pub const fn dark_bg(mut self, color: Color) -> Self {
682        self.dark_bg = Some(color);
683        self
684    }
685
686    /// Set uniform padding on all sides.
687    pub const fn p(mut self, value: u32) -> Self {
688        self.padding = Some(Padding {
689            top: value,
690            bottom: value,
691            left: value,
692            right: value,
693        });
694        self
695    }
696
697    /// Set horizontal padding.
698    pub const fn px(mut self, value: u32) -> Self {
699        let p = match self.padding {
700            Some(p) => Padding {
701                left: value,
702                right: value,
703                ..p
704            },
705            None => Padding {
706                top: 0,
707                bottom: 0,
708                left: value,
709                right: value,
710            },
711        };
712        self.padding = Some(p);
713        self
714    }
715
716    /// Set vertical padding.
717    pub const fn py(mut self, value: u32) -> Self {
718        let p = match self.padding {
719            Some(p) => Padding {
720                top: value,
721                bottom: value,
722                ..p
723            },
724            None => Padding {
725                top: value,
726                bottom: value,
727                left: 0,
728                right: 0,
729            },
730        };
731        self.padding = Some(p);
732        self
733    }
734
735    /// Set uniform margin on all sides.
736    pub const fn m(mut self, value: u32) -> Self {
737        self.margin = Some(Margin {
738            top: value,
739            bottom: value,
740            left: value,
741            right: value,
742        });
743        self
744    }
745
746    /// Set the gap between children.
747    pub const fn gap(mut self, value: u32) -> Self {
748        self.gap = Some(value);
749        self
750    }
751
752    /// Set row gap for column layouts.
753    pub const fn row_gap(mut self, value: u32) -> Self {
754        self.row_gap = Some(value);
755        self
756    }
757
758    /// Set column gap for row layouts.
759    pub const fn col_gap(mut self, value: u32) -> Self {
760        self.col_gap = Some(value);
761        self
762    }
763
764    /// Set the flex-grow factor.
765    pub const fn grow(mut self, value: u16) -> Self {
766        self.grow = Some(value);
767        self
768    }
769
770    /// Set fixed width.
771    pub const fn w(mut self, value: u32) -> Self {
772        self.w = Some(value);
773        self
774    }
775
776    /// Set fixed height.
777    pub const fn h(mut self, value: u32) -> Self {
778        self.h = Some(value);
779        self
780    }
781
782    /// Set minimum width.
783    pub const fn min_w(mut self, value: u32) -> Self {
784        self.min_w = Some(value);
785        self
786    }
787
788    /// Set maximum width.
789    pub const fn max_w(mut self, value: u32) -> Self {
790        self.max_w = Some(value);
791        self
792    }
793
794    /// Set cross-axis alignment.
795    pub const fn align(mut self, value: Align) -> Self {
796        self.align = Some(value);
797        self
798    }
799
800    /// Set per-child cross-axis alignment override.
801    pub const fn align_self(mut self, value: Align) -> Self {
802        self.align_self = Some(value);
803        self
804    }
805
806    /// Set main-axis justification.
807    pub const fn justify(mut self, value: Justify) -> Self {
808        self.justify = Some(value);
809        self
810    }
811
812    /// Set minimum height.
813    pub const fn min_h(mut self, value: u32) -> Self {
814        self.min_h = Some(value);
815        self
816    }
817
818    /// Set maximum height.
819    pub const fn max_h(mut self, value: u32) -> Self {
820        self.max_h = Some(value);
821        self
822    }
823
824    /// Set width as percentage of parent (1-100).
825    pub const fn w_pct(mut self, value: u8) -> Self {
826        self.w_pct = Some(value);
827        self
828    }
829
830    /// Set height as percentage of parent (1-100).
831    pub const fn h_pct(mut self, value: u8) -> Self {
832        self.h_pct = Some(value);
833        self
834    }
835}
836
837#[derive(Debug, Clone, Copy, Default)]
838#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
839/// Per-widget color overrides that fall back to the active theme.
840pub struct WidgetColors {
841    /// Foreground color override.
842    pub fg: Option<Color>,
843    /// Background color override.
844    pub bg: Option<Color>,
845    /// Border color override.
846    pub border: Option<Color>,
847    /// Accent color override.
848    pub accent: Option<Color>,
849}
850
851impl WidgetColors {
852    /// Create a new WidgetColors with all fields set to None (theme defaults).
853    pub const fn new() -> Self {
854        Self {
855            fg: None,
856            bg: None,
857            border: None,
858            accent: None,
859        }
860    }
861
862    /// Set the foreground color override.
863    pub const fn fg(mut self, color: Color) -> Self {
864        self.fg = Some(color);
865        self
866    }
867
868    /// Set the background color override.
869    pub const fn bg(mut self, color: Color) -> Self {
870        self.bg = Some(color);
871        self
872    }
873
874    /// Set the border color override.
875    pub const fn border(mut self, color: Color) -> Self {
876        self.border = Some(color);
877        self
878    }
879
880    /// Set the accent color override.
881    pub const fn accent(mut self, color: Color) -> Self {
882        self.accent = Some(color);
883        self
884    }
885}
886
887#[cfg(test)]
888mod tests {
889    use super::*;
890
891    #[test]
892    fn style_new_is_default() {
893        let style = Style::new();
894        assert_eq!(style.fg, None);
895        assert_eq!(style.bg, None);
896        assert_eq!(style.modifiers, Modifiers::NONE);
897        assert_eq!(style, Style::default());
898    }
899
900    #[test]
901    fn style_bold_and_fg_set_expected_fields() {
902        let style = Style::new().bold().fg(Color::Red);
903        assert_eq!(style.fg, Some(Color::Red));
904        assert_eq!(style.bg, None);
905        assert!(style.modifiers.contains(Modifiers::BOLD));
906    }
907
908    #[test]
909    fn style_multiple_modifiers_accumulate() {
910        let style = Style::new().italic().underline().dim();
911        assert!(style.modifiers.contains(Modifiers::ITALIC));
912        assert!(style.modifiers.contains(Modifiers::UNDERLINE));
913        assert!(style.modifiers.contains(Modifiers::DIM));
914    }
915
916    #[test]
917    fn style_repeated_fg_overrides_previous_color() {
918        let style = Style::new().fg(Color::Blue).fg(Color::Green);
919        assert_eq!(style.fg, Some(Color::Green));
920    }
921
922    #[test]
923    fn style_repeated_bg_overrides_previous_color() {
924        let style = Style::new().bg(Color::Blue).bg(Color::Green);
925        assert_eq!(style.bg, Some(Color::Green));
926    }
927
928    #[test]
929    fn style_override_preserves_existing_modifiers() {
930        let style = Style::new().bold().fg(Color::Red).fg(Color::Yellow);
931        assert_eq!(style.fg, Some(Color::Yellow));
932        assert!(style.modifiers.contains(Modifiers::BOLD));
933    }
934
935    #[test]
936    fn padding_all_sets_all_sides() {
937        let p = Padding::all(3);
938        assert_eq!(p.top, 3);
939        assert_eq!(p.right, 3);
940        assert_eq!(p.bottom, 3);
941        assert_eq!(p.left, 3);
942    }
943
944    #[test]
945    fn padding_xy_sets_axis_values() {
946        let p = Padding::xy(4, 2);
947        assert_eq!(p.top, 2);
948        assert_eq!(p.bottom, 2);
949        assert_eq!(p.left, 4);
950        assert_eq!(p.right, 4);
951    }
952
953    #[test]
954    fn padding_new_and_totals_are_correct() {
955        let p = Padding::new(1, 2, 3, 4);
956        assert_eq!(p.top, 1);
957        assert_eq!(p.right, 2);
958        assert_eq!(p.bottom, 3);
959        assert_eq!(p.left, 4);
960        assert_eq!(p.horizontal(), 6);
961        assert_eq!(p.vertical(), 4);
962    }
963
964    #[test]
965    fn margin_all_and_xy_are_correct() {
966        let all = Margin::all(5);
967        assert_eq!(all, Margin::new(5, 5, 5, 5));
968
969        let xy = Margin::xy(7, 1);
970        assert_eq!(xy.top, 1);
971        assert_eq!(xy.bottom, 1);
972        assert_eq!(xy.left, 7);
973        assert_eq!(xy.right, 7);
974    }
975
976    #[test]
977    fn margin_new_and_totals_are_correct() {
978        let m = Margin::new(2, 4, 6, 8);
979        assert_eq!(m.horizontal(), 12);
980        assert_eq!(m.vertical(), 8);
981    }
982
983    #[test]
984    fn constraints_min_max_builder_sets_values() {
985        let c = Constraints::default()
986            .min_w(10)
987            .max_w(40)
988            .min_h(5)
989            .max_h(20);
990        assert_eq!(c.min_width, Some(10));
991        assert_eq!(c.max_width, Some(40));
992        assert_eq!(c.min_height, Some(5));
993        assert_eq!(c.max_height, Some(20));
994    }
995
996    #[test]
997    fn constraints_percentage_builder_sets_values() {
998        let c = Constraints::default().w_pct(50).h_pct(80);
999        assert_eq!(c.width_pct, Some(50));
1000        assert_eq!(c.height_pct, Some(80));
1001    }
1002
1003    #[test]
1004    fn border_sides_all_has_both_axes() {
1005        let sides = BorderSides::all();
1006        assert!(sides.top && sides.right && sides.bottom && sides.left);
1007        assert!(sides.has_horizontal());
1008        assert!(sides.has_vertical());
1009    }
1010
1011    #[test]
1012    fn border_sides_none_has_no_axes() {
1013        let sides = BorderSides::none();
1014        assert!(!sides.top && !sides.right && !sides.bottom && !sides.left);
1015        assert!(!sides.has_horizontal());
1016        assert!(!sides.has_vertical());
1017    }
1018
1019    #[test]
1020    fn border_sides_horizontal_only() {
1021        let sides = BorderSides::horizontal();
1022        assert!(sides.top);
1023        assert!(sides.bottom);
1024        assert!(!sides.left);
1025        assert!(!sides.right);
1026        assert!(sides.has_horizontal());
1027        assert!(!sides.has_vertical());
1028    }
1029
1030    #[test]
1031    fn border_sides_vertical_only() {
1032        let sides = BorderSides::vertical();
1033        assert!(!sides.top);
1034        assert!(!sides.bottom);
1035        assert!(sides.left);
1036        assert!(sides.right);
1037        assert!(!sides.has_horizontal());
1038        assert!(sides.has_vertical());
1039    }
1040
1041    #[test]
1042    fn container_style_new_is_empty() {
1043        let s = ContainerStyle::new();
1044        assert_eq!(s.border, None);
1045        assert_eq!(s.bg, None);
1046        assert_eq!(s.padding, None);
1047        assert_eq!(s.margin, None);
1048        assert_eq!(s.gap, None);
1049        assert_eq!(s.align, None);
1050        assert_eq!(s.justify, None);
1051    }
1052
1053    #[test]
1054    fn container_style_const_construction_and_fields() {
1055        const CARD: ContainerStyle = ContainerStyle::new()
1056            .border(Border::Rounded)
1057            .border_sides(BorderSides::horizontal())
1058            .p(2)
1059            .m(1)
1060            .gap(3)
1061            .align(Align::Center)
1062            .justify(Justify::SpaceBetween)
1063            .w(60)
1064            .h(20);
1065
1066        assert_eq!(CARD.border, Some(Border::Rounded));
1067        assert_eq!(CARD.border_sides, Some(BorderSides::horizontal()));
1068        assert_eq!(CARD.padding, Some(Padding::all(2)));
1069        assert_eq!(CARD.margin, Some(Margin::all(1)));
1070        assert_eq!(CARD.gap, Some(3));
1071        assert_eq!(CARD.align, Some(Align::Center));
1072        assert_eq!(CARD.justify, Some(Justify::SpaceBetween));
1073        assert_eq!(CARD.w, Some(60));
1074        assert_eq!(CARD.h, Some(20));
1075    }
1076
1077    #[test]
1078    fn widget_colors_new_is_empty() {
1079        let colors = WidgetColors::new();
1080        assert_eq!(colors.fg, None);
1081        assert_eq!(colors.bg, None);
1082        assert_eq!(colors.border, None);
1083        assert_eq!(colors.accent, None);
1084
1085        let defaults = WidgetColors::default();
1086        assert_eq!(defaults.fg, None);
1087        assert_eq!(defaults.bg, None);
1088        assert_eq!(defaults.border, None);
1089        assert_eq!(defaults.accent, None);
1090    }
1091
1092    #[test]
1093    fn widget_colors_builder_sets_all_fields() {
1094        let colors = WidgetColors::new()
1095            .fg(Color::White)
1096            .bg(Color::Black)
1097            .border(Color::Cyan)
1098            .accent(Color::Yellow);
1099
1100        assert_eq!(colors.fg, Some(Color::White));
1101        assert_eq!(colors.bg, Some(Color::Black));
1102        assert_eq!(colors.border, Some(Color::Cyan));
1103        assert_eq!(colors.accent, Some(Color::Yellow));
1104    }
1105
1106    #[test]
1107    fn align_default_is_start() {
1108        assert_eq!(Align::default(), Align::Start);
1109    }
1110
1111    #[test]
1112    fn justify_default_is_start() {
1113        assert_eq!(Justify::default(), Justify::Start);
1114    }
1115
1116    #[test]
1117    fn align_and_justify_variants_are_distinct() {
1118        assert_ne!(Align::Start, Align::Center);
1119        assert_ne!(Align::Center, Align::End);
1120
1121        assert_ne!(Justify::Start, Justify::Center);
1122        assert_ne!(Justify::Center, Justify::End);
1123        assert_ne!(Justify::SpaceBetween, Justify::SpaceAround);
1124        assert_ne!(Justify::SpaceAround, Justify::SpaceEvenly);
1125    }
1126}