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