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